/* ** ** 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" #include "v_text.h" #include "mapinfo.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) 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 FName knownMusicExts[] = { NAME_OGG, NAME_FLAC, NAME_MP3, NAME_MP2, NAME_XA, NAME_XM, NAME_MOD, NAME_IT, NAME_S3M, NAME_MTM, NAME_STM, NAME_669, NAME_PTM, NAME_AMF, NAME_OKT, NAME_DSM, NAME_AMFF, NAME_SPC, NAME_VGM, NAME_VGZ, NAME_AY, NAME_GBS, NAME_GYM, NAME_HES, NAME_KSS, NAME_NSF, NAME_NSFE, NAME_SAP, NAME_MID, NAME_HMP, NAME_HMI, NAME_XMI, NAME_VOC, }; FString G_SetupFilenameBasedMusic(const char* fn, const char* defmusic) { FString name = StripExtension(fn); // Test if a real file with this name exists with all known extensions for music. for (auto& ext : knownMusicExts) { FStringf test("%s.%s", name.GetChars(), ext.GetChars()); if (FileExists(test)) return test; #ifdef __unix__ test.Format("%s.%s", name.GetChars(), FString(ext).MakeLower().GetChars()); 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.FindFile(fn); } //========================================================================== // // Music lookup in various places. // //========================================================================== FileReader LookupMusic(const char* musicname) { FileReader reader; 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; } //========================================================================== // // // // Create a sound system stream for the currently playing song //========================================================================== static std::unique_ptr 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,,[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 = LookupMusic(musicname); if (!reader.isOpen()) 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(); }