2020-04-12 06:07:48 +00:00
/*
* *
* * raze_music . cpp
* * music player for Build games
* *
* * Copyright 2019 - 2020 Christoph Oelckers
* *
* * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* *
* * Redistribution and use in source and binary forms , with or without
* * modification , are permitted provided that the following conditions
* * are met :
* *
* * 1. Redistributions of source code must retain the above copyright
* * notice , this list of conditions and the following disclaimer .
* * 2. Redistributions in binary form must reproduce the above copyright
* * notice , this list of conditions and the following disclaimer in the
* * documentation and / or other materials provided with the distribution .
* * 3. The name of the author may not be used to endorse or promote products
* * derived from this software without specific prior written permission .
* *
* * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ` ` AS IS ' ' AND ANY EXPRESS OR
* * IMPLIED WARRANTIES , INCLUDING , BUT NOT LIMITED TO , THE IMPLIED WARRANTIES
* * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED .
* * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT , INDIRECT ,
* * INCIDENTAL , SPECIAL , EXEMPLARY , OR CONSEQUENTIAL DAMAGES ( INCLUDING , BUT
* * NOT LIMITED TO , PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES ; LOSS OF USE ,
* * DATA , OR PROFITS ; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY
* * THEORY OF LIABILITY , WHETHER IN CONTRACT , STRICT LIABILITY , OR TORT
* * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF
* * THIS SOFTWARE , EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE .
* * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* *
*/
# include "raze_music.h"
# include "s_music.h"
# include "c_cvars.h"
# include "cmdlib.h"
# include "filesystem.h"
# include "files.h"
# include "i_music.h"
# include "gamecontrol.h"
# include "serializer.h"
static bool mus_blocked ;
static FString lastStartedMusic ;
MusicAliasMap MusicAliases ;
MusicAliasMap LevelMusicAliases ;
CVAR ( Bool , printmusicinfo , false , 0 )
CVAR ( Bool , mus_extendedlookup , false , CVAR_ARCHIVE | CVAR_GLOBALCONFIG )
// Order is: streaming formats, module formats, emulated formats and MIDI formats - for external files the first one found wins so ambiguous names should be avoided
static const char * knownMusicExts [ ] = {
" OGG " , " FLAC " , " MP3 " , " MP2 " , " XA " , " XM " , " MOD " ,
" IT " , " S3M " , " MTM " , " STM " , " 669 " , " PTM " , " AMF " ,
" OKT " , " DSM " , " AMFF " , " SPC " , " VGM " , " VGZ " , " AY " ,
" GBS " , " GYM " , " HES " , " KSS " , " NSF " , " NSFE " , " SAP " ,
" MID " , " HMP " , " HMI " , " XMI " , " VOC "
} ;
//==========================================================================
//
// Music file name lookup
//
//==========================================================================
FString G_SetupFilenameBasedMusic ( const char * fn , const char * defmusic )
{
FString name = StripExtension ( fn ) ;
FString test ;
// Test if a real file with this name exists with all known extensions for music.
for ( auto & ext : knownMusicExts )
{
test . Format ( " %s.%s " , name . GetChars ( ) , ext ) ;
if ( FileExists ( test ) ) return test ;
# ifdef __unix__
2020-05-23 14:17:55 +00:00
test . Format ( " %s.%s " , name . GetChars ( ) , FString ( ext ) . MakeLower ( ) . GetChars ( ) ) ;
2020-04-12 06:07:48 +00:00
if ( FileExists ( test ) ) return test ;
# endif
}
return defmusic ;
}
FString MusicFileExists ( const char * fn )
{
if ( mus_extendedlookup ) return G_SetupFilenameBasedMusic ( fn , nullptr ) ;
if ( FileExists ( fn ) ) return fn ;
return FString ( ) ;
}
int LookupMusicLump ( const char * fn )
{
if ( mus_extendedlookup )
{
FString name = StripExtension ( fn ) ;
int l = fileSystem . FindFileWithExtensions ( name , knownMusicExts , countof ( knownMusicExts ) ) ;
if ( l > = 0 ) return l ;
}
return fileSystem . CheckNumForFullName ( fn , true , ns_music ) ;
}
//==========================================================================
//
// Music lookup in various places.
//
//==========================================================================
FileReader OpenMusic ( const char * musicname )
{
FileReader reader ;
if ( ! mus_restartonload )
{
// If the currently playing piece of music is the same, do not restart. Note that there's still edge cases where this may fail to detect identities.
if ( mus_playing . handle ! = nullptr & & lastStartedMusic . CompareNoCase ( musicname ) = = 0 & & mus_playing . loop )
return reader ;
}
lastStartedMusic = musicname ; // remember the last piece of music that was requested to be played.
FString mus = MusicFileExists ( musicname ) ;
if ( mus . IsNotEmpty ( ) )
{
// Load an external file.
reader . OpenFile ( mus ) ;
}
if ( ! reader . isOpen ( ) )
{
int lumpnum = LookupMusicLump ( musicname ) ;
if ( mus_extendedlookup & & lumpnum > = 0 )
{
// EDuke also looks in a subfolder named after the main game resource. Do this as well if extended lookup is active.
auto rfn = fileSystem . GetResourceFileName ( fileSystem . GetFileContainer ( lumpnum ) ) ;
auto rfbase = ExtractFileBase ( rfn ) ;
FStringf aliasMusicname ( " music/%s/%s " , rfbase . GetChars ( ) , musicname ) ;
lumpnum = LookupMusicLump ( aliasMusicname ) ;
}
if ( lumpnum = = - 1 )
{
// Always look in the 'music' subfolder as well. This gets used by multiple setups to store ripped CD tracks.
FStringf aliasMusicname ( " music/%s " , musicname ) ;
lumpnum = LookupMusicLump ( aliasMusicname ) ;
}
if ( lumpnum = = - 1 & & ( g_gameType & GAMEFLAG_SW ) )
{
// Some Shadow Warrioe distributions have the music in a subfolder named 'classic'. Check that, too.
FStringf aliasMusicname ( " classic/music/%s " , musicname ) ;
lumpnum = fileSystem . FindFile ( aliasMusicname ) ;
}
if ( lumpnum > - 1 )
{
if ( fileSystem . FileLength ( lumpnum ) > = 0 )
{
reader = fileSystem . ReopenFileReader ( lumpnum ) ;
if ( ! reader . isOpen ( ) )
{
Printf ( TEXTCOLOR_RED " Unable to play music " TEXTCOLOR_WHITE " \" %s \" \n " , musicname ) ;
}
else if ( printmusicinfo ) Printf ( " Playing music from file system %s:%s \n " , fileSystem . GetResourceFileFullName ( fileSystem . GetFileContainer ( lumpnum ) ) , fileSystem . GetFileFullPath ( lumpnum ) . GetChars ( ) ) ;
}
}
}
else if ( printmusicinfo ) Printf ( " Playing music from external file %s \n " , musicname ) ;
return reader ;
}
static FString LookupMusic ( const char * musicname , int & order )
{
// Now perform music aliasing. This also needs to be done before checking identities because multiple names can map to the same song.
FName * aliasp = MusicAliases . CheckKey ( musicname ) ;
if ( aliasp ! = nullptr )
{
if ( * aliasp = = NAME_None )
{
return true ; // flagged to be ignored
}
return aliasp - > GetChars ( ) ;
}
return musicname ;
}
static FString lastMusicLevel , lastMusic ;
int Mus_Play ( const char * mapname , const char * fn , bool loop )
{
if ( mus_blocked ) return 1 ; // Caller should believe it succeeded.
2020-06-20 07:38:30 +00:00
if ( * fn = = ' / ' ) fn + + ;
2020-04-12 06:07:48 +00:00
// Store the requested names for resuming.
lastMusicLevel = mapname ;
lastMusic = fn ;
if ( ! MusicEnabled ( ) )
{
return 0 ;
}
// Allow per level music substitution.
// For most cases using $musicalias would be sufficient, but that method only works if a level actually has some music defined at all.
// This way it can be done with an add-on definition lump even in cases like Redneck Rampage where no music definitions exist
// or where music gets reused for multiple levels but replacement is wanted individually.
if ( mapname & & * mapname )
{
if ( * mapname = = ' / ' ) mapname + + ;
FName * check = LevelMusicAliases . CheckKey ( FName ( mapname , true ) ) ;
if ( check ) fn = check - > GetChars ( ) ;
}
return S_ChangeMusic ( fn , 0 , loop , true ) ;
}
bool Mus_IsPlaying ( )
{
return mus_playing . handle ! = nullptr ;
}
void Mus_Stop ( )
{
if ( mus_blocked ) return ;
S_StopMusic ( true ) ;
}
void Mus_Fade ( double seconds )
{
// Todo: Blood uses this, but the streamer cannot currently fade the volume.
Mus_Stop ( ) ;
}
void Mus_SetPaused ( bool on )
{
if ( on ) S_PauseMusic ( ) ;
else S_ResumeMusic ( ) ;
}
void Mus_Serialize ( FSerializer & arc )
{
if ( arc . BeginObject ( " music " ) )
{
if ( arc . isWriting ( ) )
{
FString music = mus_playing . name ;
if ( music . IsEmpty ( ) ) music = mus_playing . LastSong ;
arc . AddString ( " music " , music ) ;
}
else arc ( " music " , mus_playing . LastSong ) ;
arc ( " baseorder " , mus_playing . baseorder )
( " loop " , mus_playing . loop )
. EndObject ( ) ;
// this is to prevent scripts from resetting the music after it has been loaded from the savegame.
if ( arc . isReading ( ) ) mus_blocked = true ;
// Actual music resuming cannot be performed here, it must be done in the game code.
}
}
void Mus_ResumeSaved ( )
{
S_RestartMusic ( ) ;
}
void Mus_UpdateMusic ( )
{
mus_blocked = false ;
S_UpdateMusic ( ) ;
}
void Mus_InitMusic ( )
{
I_InitMusic ( ) ;
static MusicCallbacks mus_cb =
{
LookupMusic ,
OpenMusic
} ;
S_SetMusicCallbacks ( & mus_cb ) ;
}