2019-12-08 11:28:51 +00:00
/*
* *
* * music . cpp
* *
* * music engine - borrowed from GZDoom
* *
* * Copyright 1999 - 2016 Randy Heit
* * Copyright 2002 - 2016 Christoph Oelckers
2019-11-10 23:23:52 +00:00
* *
* * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* *
* * 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 .
* * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* *
*/
2019-11-10 22:58:51 +00:00
# include "zmusic/zmusic.h"
# include "z_music.h"
# include "zstring.h"
2019-11-11 20:50:20 +00:00
# include "backend/i_sound.h"
2019-11-10 22:58:51 +00:00
# include "name.h"
# include "s_music.h"
2019-11-11 00:01:18 +00:00
# include "i_music.h"
2019-11-10 22:58:51 +00:00
# include "printf.h"
# include "files.h"
# include "filesystem.h"
# include "cmdlib.h"
# include "gamecvars.h"
2019-11-11 00:01:18 +00:00
# include "c_dispatch.h"
# include "gamecontrol.h"
2019-11-10 22:58:51 +00:00
# include "filereadermusicinterface.h"
2019-11-28 02:18:58 +00:00
# include "savegamehelp.h"
# include "sjson.h"
2020-01-23 18:01:18 +00:00
# include "v_text.h"
2019-11-10 22:58:51 +00:00
MusPlayingInfo mus_playing ;
MusicAliasMap MusicAliases ;
MidiDeviceMap MidiDevices ;
2019-11-11 00:01:18 +00:00
MusicVolumeMap MusicVolumes ;
2019-11-11 23:43:07 +00:00
MusicAliasMap LevelMusicAliases ;
2019-11-11 00:01:18 +00:00
bool MusicPaused ;
2019-11-28 02:18:58 +00:00
static bool mus_blocked ;
2019-11-28 18:35:35 +00:00
static FString lastStartedMusic ;
2019-12-05 18:00:40 +00:00
EXTERN_CVAR ( Float , mus_volume )
2020-01-23 18:01:18 +00:00
CVAR ( Bool , printmusicinfo , false , 0 )
2019-11-28 18:35:35 +00:00
2019-11-11 20:50:20 +00:00
//==========================================================================
//
//
//
// Create a sound system stream for the currently playing song
//==========================================================================
static std : : unique_ptr < SoundStream > musicStream ;
static bool FillStream ( SoundStream * stream , void * buff , int len , void * userdata )
{
bool written = ZMusic_FillStream ( mus_playing . handle , buff , len ) ;
if ( ! written )
{
memset ( ( char * ) buff , 0 , len ) ;
return false ;
}
return true ;
}
void S_CreateStream ( )
{
if ( ! mus_playing . handle ) return ;
auto fmt = ZMusic_GetStreamInfo ( mus_playing . handle ) ;
if ( fmt . mBufferSize > 0 )
{
int flags = fmt . mNumChannels < 0 ? 0 : SoundStream : : Float ;
if ( abs ( fmt . mNumChannels ) < 2 ) flags | = SoundStream : : Mono ;
musicStream . reset ( GSnd - > CreateStream ( FillStream , fmt . mBufferSize , flags , fmt . mSampleRate , nullptr ) ) ;
if ( musicStream ) musicStream - > Play ( true , 1 ) ;
}
}
void S_PauseStream ( bool paused )
{
if ( musicStream ) musicStream - > SetPaused ( paused ) ;
}
void S_StopStream ( )
{
if ( musicStream )
{
musicStream - > Stop ( ) ;
musicStream . reset ( ) ;
}
}
2019-11-10 22:58:51 +00:00
2019-11-10 23:23:52 +00:00
//==========================================================================
//
// starts playing this song
//
//==========================================================================
static void S_StartMusicPlaying ( MusInfo * song , bool loop , float rel_vol , int subsong )
{
if ( rel_vol > 0.f )
{
float factor = relative_volume / saved_relative_volume ;
saved_relative_volume = rel_vol ;
I_SetRelativeVolume ( saved_relative_volume * factor ) ;
}
ZMusic_Stop ( song ) ;
ZMusic_Start ( song , subsong , loop ) ;
// Notify the sound system of the changed relative volume
2019-11-11 00:01:18 +00:00
mus_volume . Callback ( ) ;
2019-11-10 23:23:52 +00:00
}
//==========================================================================
//
// S_PauseSound
//
// Stop music and sound effects, during game PAUSE.
//==========================================================================
void S_PauseMusic ( )
{
if ( mus_playing . handle & & ! MusicPaused )
{
ZMusic_Pause ( mus_playing . handle ) ;
S_PauseStream ( true ) ;
MusicPaused = true ;
}
}
//==========================================================================
//
// S_ResumeSound
//
// Resume music and sound effects, after game PAUSE.
//==========================================================================
void S_ResumeMusic ( )
{
if ( mus_playing . handle & & MusicPaused )
{
ZMusic_Resume ( mus_playing . handle ) ;
S_PauseStream ( false ) ;
MusicPaused = false ;
}
}
//==========================================================================
//
// S_UpdateSound
//
//==========================================================================
void S_UpdateMusic ( )
{
2019-11-28 02:18:58 +00:00
mus_blocked = false ;
2019-11-10 23:23:52 +00:00
if ( mus_playing . handle ! = nullptr )
{
ZMusic_Update ( mus_playing . handle ) ;
// [RH] Update music and/or playlist. IsPlaying() must be called
// to attempt to reconnect to broken net streams and to advance the
// playlist when the current song finishes.
if ( ! ZMusic_IsPlaying ( mus_playing . handle ) )
{
2019-11-11 00:01:18 +00:00
S_StopMusic ( true ) ;
2019-11-10 23:23:52 +00:00
}
}
}
//==========================================================================
//
// S_ChangeCDMusic
//
// Starts a CD track as music.
//==========================================================================
bool S_ChangeCDMusic ( int track , unsigned int id , bool looping )
{
char temp [ 32 ] ;
if ( id ! = 0 )
{
2019-11-11 00:01:18 +00:00
snprintf ( temp , countof ( temp ) , " ,CD,%d,%x " , track , id ) ;
2019-11-10 23:23:52 +00:00
}
else
{
2019-11-11 00:01:18 +00:00
snprintf ( temp , countof ( temp ) , " ,CD,%d " , track ) ;
2019-11-10 23:23:52 +00:00
}
return S_ChangeMusic ( temp , 0 , looping ) ;
}
//==========================================================================
//
// S_StartMusic
//
// Starts some music with the given name.
//==========================================================================
bool S_StartMusic ( const char * m_id )
{
return S_ChangeMusic ( m_id , 0 , false ) ;
}
//==========================================================================
//
// S_ChangeMusic
//
// Starts playing a music, possibly looping.
//
// [RH] If music is a MOD, starts it at position order. If name is of the
// format ",CD,<track>,[cd id]" song is a CD track, and if [cd id] is
// specified, it will only be played if the specified CD is in a drive.
//==========================================================================
2019-11-10 22:58:51 +00:00
bool S_ChangeMusic ( const char * musicname , int order , bool looping , bool force )
{
2019-11-28 18:35:35 +00:00
lastStartedMusic = musicname ; // remember the last piece of music that was requested to be played.
2019-11-10 22:58:51 +00:00
if ( musicname = = nullptr | | musicname [ 0 ] = = 0 )
{
// Don't choke if the map doesn't have a song attached
2019-11-10 23:23:52 +00:00
S_StopMusic ( true ) ;
2019-11-10 22:58:51 +00:00
mus_playing . name = " " ;
mus_playing . LastSong = " " ;
return true ;
}
2019-11-11 18:53:06 +00:00
if ( * musicname = = ' / ' ) musicname + + ;
2019-11-10 22:58:51 +00:00
FString DEH_Music ;
if ( ! mus_playing . name . IsEmpty ( ) & &
mus_playing . handle ! = nullptr & &
stricmp ( mus_playing . name , musicname ) = = 0 & &
ZMusic_IsLooping ( mus_playing . handle ) = = looping )
{
if ( order ! = mus_playing . baseorder )
{
if ( ZMusic_SetSubsong ( mus_playing . handle , order ) )
{
mus_playing . baseorder = order ;
}
}
else if ( ! ZMusic_IsPlaying ( mus_playing . handle ) )
{
try
{
ZMusic_Start ( mus_playing . handle , looping , order ) ;
2019-11-10 23:23:52 +00:00
S_CreateStream ( ) ;
2019-11-10 22:58:51 +00:00
}
catch ( const std : : runtime_error & err )
{
Printf ( " Unable to start %s: %s \n " , mus_playing . name . GetChars ( ) , err . what ( ) ) ;
}
}
return true ;
}
if ( strnicmp ( musicname , " ,CD, " , 4 ) = = 0 )
{
int track = strtoul ( musicname + 4 , nullptr , 0 ) ;
const char * more = strchr ( musicname + 4 , ' , ' ) ;
unsigned int id = 0 ;
if ( more ! = nullptr )
{
id = strtoul ( more + 1 , nullptr , 16 ) ;
}
2019-11-10 23:23:52 +00:00
S_StopMusic ( true ) ;
mus_playing . handle = ZMusic_OpenCDSong ( track , id ) ;
2019-11-10 22:58:51 +00:00
}
else
{
int lumpnum = - 1 ;
MusInfo * handle = nullptr ;
MidiDeviceSetting * devp = MidiDevices . CheckKey ( musicname ) ;
// Strip off any leading file:// component.
if ( strncmp ( musicname , " file:// " , 7 ) = = 0 )
{
musicname + = 7 ;
}
FileReader reader ;
2020-01-23 18:01:18 +00:00
if ( FileExists ( musicname ) )
{
// Load an external file.
reader . OpenFile ( musicname ) ;
}
if ( ! reader . isOpen ( ) )
2019-11-10 22:58:51 +00:00
{
if ( ( lumpnum = fileSystem . FindFile ( musicname ) ) = = - 1 )
{
2019-11-11 00:01:18 +00:00
// Always look in the 'music' subfolder as well.
FStringf aliasMusicname ( " music/%s " , musicname ) ;
2020-01-23 18:01:18 +00:00
if ( ( lumpnum = fileSystem . FindFile ( musicname ) ) = = - 1 & & ( g_gameType & GAMEFLAG_SW ) )
2019-11-11 00:01:18 +00:00
{
2020-01-23 18:01:18 +00:00
// 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 ) ;
2019-11-11 00:01:18 +00:00
}
2019-11-10 22:58:51 +00:00
}
2020-01-23 18:01:18 +00:00
if ( handle = = nullptr & & lumpnum > - 1 )
2019-11-10 22:58:51 +00:00
{
if ( fileSystem . FileLength ( lumpnum ) = = 0 )
{
return false ;
}
reader = fileSystem . ReopenFileReader ( lumpnum ) ;
}
2020-01-23 18:01:18 +00:00
if ( ! reader . isOpen ( ) )
2019-11-10 22:58:51 +00:00
{
2020-01-23 18:01:18 +00:00
Printf ( TEXTCOLOR_RED " Unable to play music " TEXTCOLOR_WHITE " \" %s \" \n " , musicname ) ;
2019-11-10 22:58:51 +00:00
}
2020-01-23 18:01:18 +00:00
else if ( printmusicinfo ) Printf ( " Playing music from file system %s:%s \n " , fileSystem . GetResourceFileFullName ( fileSystem . GetFileContainer ( lumpnum ) ) , musicname ) ;
2019-11-10 22:58:51 +00:00
}
2020-01-23 18:01:18 +00:00
else if ( printmusicinfo ) Printf ( " Playing music from external file %s \n " , musicname ) ;
2019-11-10 22:58:51 +00:00
// shutdown old music
2019-11-10 23:23:52 +00:00
S_StopMusic ( true ) ;
2019-11-10 22:58:51 +00:00
2019-11-28 02:18:58 +00:00
// Just record it if volume is 0 or music was disabled
if ( mus_volume < = 0 | | ! mus_enabled )
2019-11-10 22:58:51 +00:00
{
mus_playing . loop = looping ;
mus_playing . name = musicname ;
mus_playing . baseorder = order ;
mus_playing . LastSong = musicname ;
return true ;
}
// load & register it
if ( handle ! = nullptr )
{
mus_playing . handle = handle ;
}
else
{
try
{
auto mreader = new FileReaderMusicInterface ( reader ) ;
mus_playing . handle = ZMusic_OpenSong ( mreader , devp ? ( EMidiDevice ) devp - > device : MDEV_DEFAULT , devp ? devp - > args . GetChars ( ) : " " ) ;
}
catch ( const std : : runtime_error & err )
{
Printf ( " Unable to load %s: %s \n " , mus_playing . name . GetChars ( ) , err . what ( ) ) ;
}
}
}
mus_playing . loop = looping ;
mus_playing . name = musicname ;
mus_playing . baseorder = 0 ;
mus_playing . LastSong = " " ;
if ( mus_playing . handle ! = 0 )
{ // play it
try
{
2019-11-11 00:01:18 +00:00
auto vol = MusicVolumes . CheckKey ( musicname ) ;
S_StartMusicPlaying ( mus_playing . handle , looping , vol ? * vol : 1.f , order ) ;
2019-11-10 23:23:52 +00:00
S_CreateStream ( ) ;
2019-11-10 22:58:51 +00:00
mus_playing . baseorder = order ;
}
catch ( const std : : runtime_error & err )
{
Printf ( " Unable to start %s: %s \n " , mus_playing . name . GetChars ( ) , err . what ( ) ) ;
}
return true ;
}
return false ;
}
2019-11-10 23:23:52 +00:00
//==========================================================================
//
// S_RestartMusic
//
//==========================================================================
void S_RestartMusic ( )
2019-11-10 22:58:51 +00:00
{
2019-11-28 02:18:58 +00:00
if ( ! mus_playing . LastSong . IsEmpty ( ) & & mus_volume > 0 & & mus_enabled )
2019-11-10 23:23:52 +00:00
{
FString song = mus_playing . LastSong ;
mus_playing . LastSong = " " ;
S_ChangeMusic ( song , mus_playing . baseorder , mus_playing . loop , true ) ;
}
}
2019-11-10 22:58:51 +00:00
2019-11-10 23:23:52 +00:00
//==========================================================================
//
// S_MIDIDeviceChanged
//
//==========================================================================
void S_MIDIDeviceChanged ( int newdev )
{
MusInfo * song = mus_playing . handle ;
if ( song ! = nullptr & & ZMusic_IsMIDI ( song ) & & ZMusic_IsPlaying ( song ) )
{
// Reload the song to change the device
auto mi = mus_playing ;
S_StopMusic ( true ) ;
S_ChangeMusic ( mi . name , mi . baseorder , mi . loop ) ;
}
}
//==========================================================================
//
// S_GetMusic
//
//==========================================================================
int S_GetMusic ( const char * * name )
{
int order ;
if ( mus_playing . name . IsNotEmpty ( ) )
{
* name = mus_playing . name ;
order = mus_playing . baseorder ;
}
else
{
* name = nullptr ;
order = 0 ;
}
return order ;
2019-11-10 22:58:51 +00:00
}
2019-11-10 23:23:52 +00:00
//==========================================================================
//
// S_StopMusic
//
//==========================================================================
void S_StopMusic ( bool force )
{
try
{
// [RH] Don't stop if a playlist is active.
2019-11-11 00:01:18 +00:00
if ( ! mus_playing . name . IsEmpty ( ) )
2019-11-10 23:23:52 +00:00
{
if ( mus_playing . handle ! = nullptr )
{
S_ResumeMusic ( ) ;
S_StopStream ( ) ;
ZMusic_Stop ( mus_playing . handle ) ;
auto h = mus_playing . handle ;
mus_playing . handle = nullptr ;
ZMusic_Close ( h ) ;
}
mus_playing . LastSong = std : : move ( mus_playing . name ) ;
}
}
catch ( const std : : runtime_error & )
{
//Printf("Unable to stop %s: %s\n", mus_playing.name.GetChars(), err.what());
if ( mus_playing . handle ! = nullptr )
{
auto h = mus_playing . handle ;
mus_playing . handle = nullptr ;
ZMusic_Close ( h ) ;
}
mus_playing . name = " " ;
}
}
//==========================================================================
//
// CCMD changemus
//
//==========================================================================
CCMD ( changemus )
{
2019-11-11 00:01:18 +00:00
if ( MusicEnabled ( ) )
2019-11-10 23:23:52 +00:00
{
if ( argv . argc ( ) > 1 )
{
S_ChangeMusic ( argv [ 1 ] , argv . argc ( ) > 2 ? atoi ( argv [ 2 ] ) : 0 ) ;
}
else
{
const char * currentmus = mus_playing . name . GetChars ( ) ;
if ( currentmus ! = nullptr & & * currentmus ! = 0 )
{
Printf ( " currently playing %s \n " , currentmus ) ;
}
else
{
Printf ( " no music playing \n " ) ;
}
}
}
else
{
Printf ( " Music is disabled \n " ) ;
}
}
//==========================================================================
//
// CCMD stopmus
//
//==========================================================================
CCMD ( stopmus )
{
S_StopMusic ( false ) ;
mus_playing . LastSong = " " ; // forget the last played song so that it won't get restarted if some volume changes occur
}
2019-11-11 23:43:07 +00:00
static FString lastMusicLevel , lastMusic ;
int Mus_Play ( const char * mapname , const char * fn , bool loop )
2019-11-10 22:58:51 +00:00
{
2019-12-26 12:04:29 +00:00
if ( mus_blocked ) return 1 ; // Caller should believe it succeeded.
2019-11-11 23:43:07 +00:00
// Store the requested names for resuming.
lastMusicLevel = mapname ;
lastMusic = fn ;
if ( ! MusicEnabled ( ) )
{
return 0 ;
}
2019-11-28 18:35:35 +00:00
2019-11-11 23:43:07 +00:00
// 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.
2019-11-28 18:35:35 +00:00
// 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.
2019-11-11 23:43:07 +00:00
if ( mapname & & * mapname )
{
if ( * mapname = = ' / ' ) mapname + + ;
FName * check = LevelMusicAliases . CheckKey ( FName ( mapname , true ) ) ;
if ( check ) fn = check - > GetChars ( ) ;
}
2019-11-28 18:35:35 +00:00
// 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 ( fn ) ;
if ( aliasp ! = nullptr )
{
if ( * aliasp = = NAME_None )
{
return true ; // flagged to be ignored
}
fn = aliasp - > GetChars ( ) ;
}
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 ( fn ) = = 0 & & mus_playing . loop )
return true ;
}
2019-11-11 00:01:18 +00:00
S_ChangeMusic ( fn , 0 , loop , true ) ;
2019-11-11 23:43:07 +00:00
return mus_playing . handle ! = nullptr ;
2019-11-10 22:58:51 +00:00
}
2019-12-26 12:04:29 +00:00
bool Mus_IsPlaying ( )
{
return mus_playing . handle ! = nullptr ;
}
2019-11-11 00:01:18 +00:00
void Mus_Stop ( )
2019-11-10 22:58:51 +00:00
{
2019-11-28 02:18:58 +00:00
if ( mus_blocked ) return ;
2019-11-11 00:01:18 +00:00
S_StopMusic ( true ) ;
2019-11-10 22:58:51 +00:00
}
2019-12-07 17:28:30 +00:00
void Mus_Fade ( double seconds )
{
// Todo: Blood uses this, but the streamer cannot currently fade the volume.
Mus_Stop ( ) ;
}
2019-11-10 22:58:51 +00:00
void Mus_SetPaused ( bool on )
{
2019-11-11 00:01:18 +00:00
if ( on ) S_PauseMusic ( ) ;
else S_ResumeMusic ( ) ;
2019-11-10 22:58:51 +00:00
}
2019-11-28 02:18:58 +00:00
void MUS_Save ( )
{
FString music = mus_playing . name ;
if ( music . IsEmpty ( ) ) music = mus_playing . LastSong ;
sjson_context * ctx = sjson_create_context ( 0 , 0 , NULL ) ;
if ( ! ctx )
{
return ;
}
sjson_node * root = sjson_mkobject ( ctx ) ;
sjson_put_string ( ctx , root , " music " , music ) ;
sjson_put_int ( ctx , root , " baseorder " , mus_playing . baseorder ) ;
sjson_put_bool ( ctx , root , " loop " , mus_playing . loop ) ;
char * encoded = sjson_stringify ( ctx , root , " " ) ;
FileWriter * fil = WriteSavegameChunk ( " music.json " ) ;
if ( ! fil )
{
sjson_destroy_context ( ctx ) ;
return ;
}
fil - > Write ( encoded , strlen ( encoded ) ) ;
sjson_free_string ( ctx , encoded ) ;
sjson_destroy_context ( ctx ) ;
}
bool MUS_Restore ( )
{
auto fil = ReadSavegameChunk ( " music.json " ) ;
if ( ! fil . isOpen ( ) )
{
return false ;
}
auto text = fil . ReadPadded ( 1 ) ;
fil . Close ( ) ;
if ( text . Size ( ) = = 0 )
{
return false ;
}
sjson_context * ctx = sjson_create_context ( 0 , 0 , NULL ) ;
sjson_node * root = sjson_decode ( ctx , ( const char * ) text . Data ( ) ) ;
mus_playing . LastSong = sjson_get_string ( root , " music " , " " ) ;
mus_playing . baseorder = sjson_get_int ( root , " baseorder " , 0 ) ;
mus_playing . loop = sjson_get_bool ( root , " loop " , true ) ;
sjson_destroy_context ( ctx ) ;
mus_blocked = true ; // this is to prevent scripts from resetting the music after it has been loaded from the savegame.
return true ;
}
2019-12-26 12:04:29 +00:00
void Mus_ResumeSaved ( )
2019-11-28 02:18:58 +00:00
{
S_RestartMusic ( ) ;
}