mirror of
https://github.com/ZDoom/Raze.git
synced 2024-12-12 21:52:01 +00:00
d8dfe752b5
After merging the CD enabling CVAR they had the same default (off) as Blood which is wrong. This also addresses other music related issues, like not properly cycling through the RR music.
665 lines
16 KiB
C++
665 lines
16 KiB
C++
/*
|
|
**
|
|
** music.cpp
|
|
**
|
|
** music engine - borrowed from GZDoom
|
|
**
|
|
** Copyright 1999-2016 Randy Heit
|
|
** Copyright 2002-2016 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 "zmusic/zmusic.h"
|
|
#include "z_music.h"
|
|
#include "zstring.h"
|
|
#include "backend/i_sound.h"
|
|
#include "name.h"
|
|
#include "s_music.h"
|
|
#include "i_music.h"
|
|
#include "printf.h"
|
|
#include "files.h"
|
|
#include "filesystem.h"
|
|
#include "cmdlib.h"
|
|
#include "gamecvars.h"
|
|
#include "c_dispatch.h"
|
|
#include "gamecontrol.h"
|
|
#include "filereadermusicinterface.h"
|
|
#include "savegamehelp.h"
|
|
#include "sjson.h"
|
|
|
|
MusPlayingInfo mus_playing;
|
|
MusicAliasMap MusicAliases;
|
|
MidiDeviceMap MidiDevices;
|
|
MusicVolumeMap MusicVolumes;
|
|
MusicAliasMap LevelMusicAliases;
|
|
bool MusicPaused;
|
|
static bool mus_blocked;
|
|
static FString lastStartedMusic;
|
|
EXTERN_CVAR(Float, mus_volume)
|
|
|
|
//==========================================================================
|
|
//
|
|
//
|
|
//
|
|
// 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();
|
|
}
|
|
}
|
|
|
|
|
|
//==========================================================================
|
|
//
|
|
// 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
|
|
mus_volume.Callback();
|
|
}
|
|
|
|
|
|
//==========================================================================
|
|
//
|
|
// 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 ()
|
|
{
|
|
mus_blocked = false;
|
|
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))
|
|
{
|
|
S_StopMusic(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
// S_ChangeCDMusic
|
|
//
|
|
// Starts a CD track as music.
|
|
//==========================================================================
|
|
|
|
bool S_ChangeCDMusic (int track, unsigned int id, bool looping)
|
|
{
|
|
char temp[32];
|
|
|
|
if (id != 0)
|
|
{
|
|
snprintf (temp, countof(temp), ",CD,%d,%x", track, id);
|
|
}
|
|
else
|
|
{
|
|
snprintf (temp, countof(temp), ",CD,%d", track);
|
|
}
|
|
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.
|
|
//==========================================================================
|
|
|
|
bool S_ChangeMusic(const char* musicname, int order, bool looping, bool force)
|
|
{
|
|
lastStartedMusic = musicname; // remember the last piece of music that was requested to be played.
|
|
if (musicname == nullptr || musicname[0] == 0)
|
|
{
|
|
// Don't choke if the map doesn't have a song attached
|
|
S_StopMusic (true);
|
|
mus_playing.name = "";
|
|
mus_playing.LastSong = "";
|
|
return true;
|
|
}
|
|
if (*musicname == '/') musicname++;
|
|
|
|
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);
|
|
S_CreateStream();
|
|
}
|
|
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);
|
|
}
|
|
S_StopMusic (true);
|
|
mus_playing.handle = ZMusic_OpenCDSong (track, id);
|
|
}
|
|
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;
|
|
if (!FileExists(musicname))
|
|
{
|
|
if ((lumpnum = fileSystem.FindFile(musicname)) == -1)
|
|
{
|
|
// Always look in the 'music' subfolder as well.
|
|
FStringf aliasMusicname("music/%s", musicname);
|
|
if ((lumpnum = fileSystem.FindFile(aliasMusicname)) == -1)
|
|
{
|
|
Printf("Music \"%s\" not found\n", musicname);
|
|
return false;
|
|
}
|
|
}
|
|
if (handle == nullptr)
|
|
{
|
|
if (fileSystem.FileLength(lumpnum) == 0)
|
|
{
|
|
return false;
|
|
}
|
|
reader = fileSystem.ReopenFileReader(lumpnum);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Load an external file.
|
|
if (!reader.OpenFile(musicname))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// shutdown old music
|
|
S_StopMusic (true);
|
|
|
|
// Just record it if volume is 0 or music was disabled
|
|
if (mus_volume <= 0 || !mus_enabled)
|
|
{
|
|
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
|
|
{
|
|
auto vol = MusicVolumes.CheckKey(musicname);
|
|
S_StartMusicPlaying(mus_playing.handle, looping, vol? *vol : 1.f, order);
|
|
S_CreateStream();
|
|
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;
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
// S_RestartMusic
|
|
//
|
|
//==========================================================================
|
|
|
|
void S_RestartMusic ()
|
|
{
|
|
if (!mus_playing.LastSong.IsEmpty() && mus_volume > 0 && mus_enabled)
|
|
{
|
|
FString song = mus_playing.LastSong;
|
|
mus_playing.LastSong = "";
|
|
S_ChangeMusic (song, mus_playing.baseorder, mus_playing.loop, true);
|
|
}
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
// 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;
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
// S_StopMusic
|
|
//
|
|
//==========================================================================
|
|
|
|
void S_StopMusic (bool force)
|
|
{
|
|
try
|
|
{
|
|
// [RH] Don't stop if a playlist is active.
|
|
if (!mus_playing.name.IsEmpty())
|
|
{
|
|
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)
|
|
{
|
|
if (MusicEnabled())
|
|
{
|
|
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
|
|
}
|
|
|
|
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.
|
|
// 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();
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
S_ChangeMusic(fn, 0, loop, true);
|
|
return mus_playing.handle != nullptr;
|
|
}
|
|
|
|
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_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;
|
|
}
|
|
|
|
void Mus_ResumeSaved()
|
|
{
|
|
S_RestartMusic();
|
|
}
|