mirror of
https://github.com/DrBeef/Raze.git
synced 2024-12-12 21:52:23 +00:00
282 lines
8.4 KiB
C++
282 lines
8.4 KiB
C++
/*
|
|
**
|
|
** 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;
|
|
TArray<FString> specialmusic;
|
|
|
|
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 LookupMusic(const char* fn, bool onlyextended)
|
|
{
|
|
if (mus_extendedlookup || onlyextended)
|
|
{
|
|
FString name = StripExtension(fn);
|
|
int l = fileSystem.FindFileWithExtensions(name, knownMusicExts, countof(knownMusicExts));
|
|
if (l >= 0 || onlyextended) 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 = LookupMusic(musicname);
|
|
if (mus_extendedlookup || lumpnum < 0)
|
|
{
|
|
if (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);
|
|
int newlumpnum = LookupMusic(aliasMusicname);
|
|
if (newlumpnum >= 0) lumpnum = newlumpnum;
|
|
}
|
|
|
|
// Always look in the 'music' subfolder as well. This gets used by multiple setups to store ripped CD tracks.
|
|
FStringf aliasMusicname("music/%s", musicname);
|
|
int newlumpnum = LookupMusic(aliasMusicname, lumpnum >= 0);
|
|
if (newlumpnum >= 0) lumpnum = newlumpnum;
|
|
}
|
|
|
|
if (lumpnum == -1 && (g_gameType & GAMEFLAG_SW))
|
|
{
|
|
// Some Shadow Warrior 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 LookupMusicCB(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 1;
|
|
}
|
|
|
|
// 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 =
|
|
{
|
|
LookupMusicCB,
|
|
OpenMusic
|
|
};
|
|
S_SetMusicCallbacks(&mus_cb);
|
|
}
|