/* ** ** 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__ 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.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. if (*fn == '/') fn++; // 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_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); }