- update of music code, in particular separating the engine-specific lookup from the backend.

# Conflicts:
#	source/core/music/music.cpp

# Conflicts:
#	source/build/src/palette.cpp
This commit is contained in:
Christoph Oelckers 2020-04-12 08:07:48 +02:00
parent d0406e27b6
commit 6a8efb7520
32 changed files with 891 additions and 347 deletions

View file

@ -698,6 +698,7 @@ set (PCH_SOURCES
core/quotes.cpp core/quotes.cpp
core/screenshot.cpp core/screenshot.cpp
core/serializer.cpp core/serializer.cpp
core/raze_music.cpp
core/2d/v_2ddrawer.cpp core/2d/v_2ddrawer.cpp
core/2d/v_draw.cpp core/2d/v_draw.cpp

View file

@ -48,7 +48,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "view.h" #include "view.h"
#include "nnexts.h" #include "nnexts.h"
#include "savegamehelp.h" #include "savegamehelp.h"
#include "z_music.h" #include "raze_music.h"
#include "mapinfo.h" #include "mapinfo.h"
#include "aibat.h" #include "aibat.h"

View file

@ -30,7 +30,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "resource.h" #include "resource.h"
#include "sound.h" #include "sound.h"
#include "baselayer.h" #include "baselayer.h"
#include "z_music.h" #include "raze_music.h"
#include "sfx.h" #include "sfx.h"
#include "sound/s_soundinternal.h" #include "sound/s_soundinternal.h"

View file

@ -23,7 +23,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#pragma once #pragma once
#include "resource.h" #include "resource.h"
#include "z_music.h" #include "raze_music.h"
BEGIN_BLD_NS BEGIN_BLD_NS

View file

@ -39,7 +39,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "c_console.h" #include "c_console.h"
#include "c_dispatch.h" #include "c_dispatch.h"
#include "i_specialpaths.h" #include "i_specialpaths.h"
#include "z_music.h" #include "raze_music.h"
#include "statistics.h" #include "statistics.h"
#include "menu.h" #include "menu.h"
#include "gstrings.h" #include "gstrings.h"
@ -96,6 +96,8 @@ void InitFileSystem(TArray<GrpEntry>&);
void I_SetWindowTitle(const char* caption); void I_SetWindowTitle(const char* caption);
void InitENet(); void InitENet();
void ShutdownENet(); void ShutdownENet();
void S_ParseSndInfo();
bool AppActive; bool AppActive;
int chatmodeon; // needed by the common console code. int chatmodeon; // needed by the common console code.
@ -273,9 +275,7 @@ void UserConfig::ProcessOptions()
AddArt.reset(Args->GatherFiles("-art")); AddArt.reset(Args->GatherFiles("-art"));
nologo = Args->CheckParm("-nologo") || Args->CheckParm("-quick"); nologo = Args->CheckParm("-nologo") || Args->CheckParm("-quick");
nomusic = Args->CheckParm("-nomusic"); nosound = Args->CheckParm("-nosfx") || Args->CheckParm("-nosound");
nosound = Args->CheckParm("-nosfx");
if (Args->CheckParm("-nosound")) nomusic = nosound = true;
if (Args->CheckParm("-setup")) queryiwad = 1; if (Args->CheckParm("-setup")) queryiwad = 1;
else if (Args->CheckParm("-nosetup")) queryiwad = 0; else if (Args->CheckParm("-nosetup")) queryiwad = 0;
@ -714,7 +714,10 @@ int RunGame()
V_InitFonts(); V_InitFonts();
C_CON_SetAliases(); C_CON_SetAliases();
sfx_empty = fileSystem.FindFile("engine/dsempty.lmp"); // this must be done outside the sound code because it's initialized late. sfx_empty = fileSystem.FindFile("engine/dsempty.lmp"); // this must be done outside the sound code because it's initialized late.
Mus_Init(); I_InitSound();
Mus_InitMusic();
timerSetCallback(Mus_UpdateMusic);
S_ParseSndInfo();
InitStatistics(); InitStatistics();
M_Init(); M_Init();
SetDefaultStrings(); SetDefaultStrings();

View file

@ -63,7 +63,7 @@ struct UserConfig
bool nomonsters = false; bool nomonsters = false;
bool nosound = false; bool nosound = false;
bool nomusic = false; //bool nomusic = false;
bool nologo = false; bool nologo = false;
int setupstate = -1; int setupstate = -1;
@ -77,9 +77,10 @@ struct UserConfig
extern UserConfig userConfig; extern UserConfig userConfig;
extern int nomusic;
inline bool MusicEnabled() inline bool MusicEnabled()
{ {
return !userConfig.nomusic; return !nomusic;
} }
inline bool SoundEnabled() inline bool SoundEnabled()

View file

@ -42,7 +42,7 @@
#include "m_argv.h" #include "m_argv.h"
#include "rts.h" #include "rts.h"
#include "stats.h" #include "stats.h"
#include "z_music.h" #include "raze_music.h"
#include "c_dispatch.h" #include "c_dispatch.h"
#include "gstrings.h" #include "gstrings.h"
#include "quotemgr.h" #include "quotemgr.h"

View file

@ -39,6 +39,7 @@
#include <zlib.h> #include <zlib.h>
#include <zmusic.h>
#include "m_argv.h" #include "m_argv.h"
#include "filesystem.h" #include "filesystem.h"
#include "c_dispatch.h" #include "c_dispatch.h"
@ -48,69 +49,28 @@
#include "c_cvars.h" #include "c_cvars.h"
#include "c_console.h" #include "c_console.h"
#include "v_text.h" #include "v_text.h"
#include "i_sound.h"
#include "i_soundfont.h" #include "i_soundfont.h"
#include "s_music.h" #include "s_music.h"
#include "printf.h" #include "filereadermusicinterface.h"
#include "timer.h"
#include "backend/i_sound.h"
#include <zmusic.h>
void I_InitSoundFonts(); void I_InitSoundFonts();
void S_SetStreamVolume(float);
void S_ParseSndInfo();
EXTERN_CVAR (Int, snd_samplerate) EXTERN_CVAR (Int, snd_samplerate)
EXTERN_CVAR (Int, snd_mididevice) EXTERN_CVAR (Int, snd_mididevice)
EXTERN_CVAR(Float, snd_mastervolume) EXTERN_CVAR(Float, snd_mastervolume)
int nomusic = 0;
float relative_volume = 1.f, saved_relative_volume = 1.f;
#ifdef _WIN32
//========================================================================== //==========================================================================
// //
// CVAR: cd_drive // CVAR snd_musicvolume
//
// Which drive (letter) to use for CD audio. If not a valid drive letter,
// let the operating system decide for us.
//
//==========================================================================
EXTERN_CVAR(Bool, cd_enabled);
CUSTOM_CVAR(String, cd_drive, "", CVAR_ARCHIVE | CVAR_NOINITCALL | CVAR_GLOBALCONFIG)
{
if (cd_enabled && !Args->CheckParm("-nocdaudio")) CD_Enable(self);
}
//==========================================================================
//
// CVAR: cd_enabled
//
// Use the CD device? Can be overridden with -nocdaudio on the command line
//
//==========================================================================
CUSTOM_CVAR(Bool, cd_enabled, true, CVAR_ARCHIVE | CVAR_NOINITCALL | CVAR_GLOBALCONFIG)
{
if (self && !Args->CheckParm("-nocdaudio"))
CD_Enable(cd_drive);
else
CD_Enable(nullptr);
}
#endif
//==========================================================================
//
// CVAR mus_volume
// //
// Maximum volume of MOD/stream music. // Maximum volume of MOD/stream music.
//========================================================================== //==========================================================================
CUSTOM_CVARD(Float, mus_volume, 0.5, CVAR_ARCHIVE|CVAR_GLOBALCONFIG, "controls music volume") CUSTOM_CVARD(Float, snd_musicvolume, 0.5, CVAR_ARCHIVE|CVAR_GLOBALCONFIG, "controls music volume")
{ {
if (self < 0.f) if (self < 0.f)
self = 0.f; self = 0.f;
@ -202,20 +162,68 @@ static void mus_sfclose(void* handle)
reinterpret_cast<FSoundFontReader*>(handle)->close(); reinterpret_cast<FSoundFontReader*>(handle)->close();
} }
#ifndef ZMUSIC_LITE
//========================================================================== //==========================================================================
// //
// // Pass some basic working data to the music backend
// We do this once at startup for everything available.
// //
//========================================================================== //==========================================================================
void Mus_Init(void) static void SetupGenMidi()
{ {
I_InitSound(); // The OPL renderer should not care about where this comes from.
I_InitSoundFonts(); // Note: No I_Error here - this needs to be consistent with the rest of the music code.
S_ParseSndInfo(); auto lump = fileSystem.CheckNumForName("GENMIDI", ns_global);
if (lump < 0)
{
Printf("No GENMIDI lump found. OPL playback not available.\n");
return;
}
auto data = fileSystem.OpenFileReader(lump);
mus_volume.Callback (); auto genmidi = data.Read();
if (genmidi.Size() < 8 + 175 * 36 || memcmp(genmidi.Data(), "#OPL_II#", 8)) return;
ZMusic_SetGenMidi(genmidi.Data()+8);
}
static void SetupWgOpn()
{
int lump = fileSystem.CheckNumForFullName("xg.wopn");
if (lump < 0)
{
return;
}
FileData data = fileSystem.ReadFile(lump);
ZMusic_SetWgOpn(data.GetMem(), (uint32_t)data.GetSize());
}
static void SetupDMXGUS()
{
int lump = fileSystem.CheckNumForFullName("DMXGUS");
if (lump < 0)
{
return;
}
FileData data = fileSystem.ReadFile(lump);
ZMusic_SetDmxGus(data.GetMem(), (uint32_t)data.GetSize());
}
#endif
//==========================================================================
//
//
//
//==========================================================================
void I_InitMusic(void)
{
I_InitSoundFonts();
snd_musicvolume.Callback ();
nomusic = !!Args->CheckParm("-nomusic") || !!Args->CheckParm("-nosound");
snd_mididevice.Callback(); snd_mididevice.Callback();
@ -230,7 +238,11 @@ void Mus_Init(void)
callbacks.SF_Close = mus_sfclose; callbacks.SF_Close = mus_sfclose;
ZMusic_SetCallbacks(&callbacks); ZMusic_SetCallbacks(&callbacks);
timerSetCallback(S_UpdateMusic); #ifndef ZMUSIC_LITE
SetupGenMidi();
SetupDMXGUS();
SetupWgOpn();
#endif
} }
@ -244,7 +256,7 @@ void I_SetRelativeVolume(float vol)
{ {
relative_volume = (float)vol; relative_volume = (float)vol;
ChangeMusicSetting(zmusic_relative_volume, nullptr, (float)vol); ChangeMusicSetting(zmusic_relative_volume, nullptr, (float)vol);
mus_volume.Callback(); snd_musicvolume.Callback();
} }
//========================================================================== //==========================================================================
// //
@ -300,7 +312,8 @@ static ZMusic_MidiSource GetMIDISource(const char *fn)
FString src = fn; FString src = fn;
if (src.Compare("*") == 0) src = mus_playing.name; if (src.Compare("*") == 0) src = mus_playing.name;
auto lump = fileSystem.FindFile(src); auto lump = fileSystem.CheckNumForName(src, ns_music);
if (lump < 0) lump = fileSystem.CheckNumForFullName(src);
if (lump < 0) if (lump < 0)
{ {
Printf("Cannot find MIDI lump %s.\n", src.GetChars()); Printf("Cannot find MIDI lump %s.\n", src.GetChars());
@ -344,7 +357,7 @@ static ZMusic_MidiSource GetMIDISource(const char *fn)
// //
//========================================================================== //==========================================================================
CCMD (writewave) UNSAFE_CCMD (writewave)
{ {
if (argv.argc() >= 3 && argv.argc() <= 7) if (argv.argc() >= 3 && argv.argc() <= 7)
{ {
@ -352,18 +365,23 @@ CCMD (writewave)
if (source == nullptr) return; if (source == nullptr) return;
EMidiDevice dev = MDEV_DEFAULT; EMidiDevice dev = MDEV_DEFAULT;
#ifndef ZMUSIC_LITE
if (argv.argc() >= 6) if (argv.argc() >= 6)
{ {
if (!stricmp(argv[5], "Timidity") || !stricmp(argv[5], "Timidity++")) dev = MDEV_TIMIDITY; if (!stricmp(argv[5], "WildMidi")) dev = MDEV_WILDMIDI;
else if (!stricmp(argv[5], "GUS")) dev = MDEV_GUS;
else if (!stricmp(argv[5], "Timidity") || !stricmp(argv[5], "Timidity++")) dev = MDEV_TIMIDITY;
else if (!stricmp(argv[5], "FluidSynth")) dev = MDEV_FLUIDSYNTH; else if (!stricmp(argv[5], "FluidSynth")) dev = MDEV_FLUIDSYNTH;
else if (!stricmp(argv[5], "OPL")) dev = MDEV_OPL; else if (!stricmp(argv[5], "OPL")) dev = MDEV_OPL;
else if (!stricmp(argv[5], "OPN")) dev = MDEV_OPN;
else if (!stricmp(argv[5], "ADL")) dev = MDEV_ADL;
else else
{ {
Printf("%s: Unknown MIDI device\n", argv[5]); Printf("%s: Unknown MIDI device\n", argv[5]);
return; return;
} }
} }
#endif
// We must stop the currently playing music to avoid interference between two synths. // We must stop the currently playing music to avoid interference between two synths.
auto savedsong = mus_playing; auto savedsong = mus_playing;
S_StopMusic(true); S_StopMusic(true);
@ -392,7 +410,7 @@ CCMD (writewave)
// //
//========================================================================== //==========================================================================
CCMD(writemidi) UNSAFE_CCMD(writemidi)
{ {
if (argv.argc() != 3) if (argv.argc() != 3)
{ {

View file

@ -34,14 +34,26 @@
#ifndef __I_MUSIC_H__ #ifndef __I_MUSIC_H__
#define __I_MUSIC_H__ #define __I_MUSIC_H__
#include "c_cvars.h"
class FileReader; class FileReader;
struct FOptionValues;
// //
// MUSIC I/O // MUSIC I/O
// //
void I_InitMusic ();
void I_BuildMIDIMenuList (FOptionValues *);
// Volume. // Volume.
void I_SetRelativeVolume(float); void I_SetRelativeVolume(float);
void I_SetMusicVolume (double volume); void I_SetMusicVolume (double volume);
extern int nomusic;
EXTERN_CVAR(Bool, mus_enabled)
EXTERN_CVAR(Float, snd_musicvolume)
#endif //__I_MUSIC_H__ #endif //__I_MUSIC_H__

View file

@ -35,7 +35,9 @@
#include <ctype.h> #include <ctype.h>
#include <assert.h> #include <assert.h>
#include "i_soundfont.h" #include "i_soundfont.h"
#include "i_soundinternal.h"
#include "cmdlib.h" #include "cmdlib.h"
#include "i_system.h"
#include "gameconfigfile.h" #include "gameconfigfile.h"
#include "filereadermusicinterface.h" #include "filereadermusicinterface.h"
#include <zmusic.h> #include <zmusic.h>
@ -292,7 +294,7 @@ FileReader FPatchSetReader::OpenFile(const char *name)
FLumpPatchSetReader::FLumpPatchSetReader(const char *filename) FLumpPatchSetReader::FLumpPatchSetReader(const char *filename)
{ {
mLumpIndex = fileSystem.FindFile(filename); mLumpIndex = fileSystem.CheckNumForFullName(filename);
mBasePath = filename; mBasePath = filename;
FixPathSeperator(mBasePath); FixPathSeperator(mBasePath);
@ -310,7 +312,7 @@ FileReader FLumpPatchSetReader::OpenFile(const char *name)
FString path; FString path;
if (IsAbsPath(name)) return FileReader(); // no absolute paths in the lump directory. if (IsAbsPath(name)) return FileReader(); // no absolute paths in the lump directory.
path = mBasePath + name; path = mBasePath + name;
auto index = fileSystem.FindFile(path); auto index = fileSystem.CheckNumForFullName(path);
if (index < 0) return FileReader(); if (index < 0) return FileReader();
return fileSystem.ReopenFileReader(index); return fileSystem.ReopenFileReader(index);
} }
@ -470,7 +472,7 @@ FSoundFontReader *FSoundFontManager::OpenSoundFont(const char *name, int allowed
if (name != nullptr) if (name != nullptr)
{ {
const char *p = name + strlen(name) - 4; const char *p = name + strlen(name) - 4;
if (p > name && !stricmp(p, ".cfg") && fileSystem.FindFile(name) >= 0) if (p > name && !stricmp(p, ".cfg") && fileSystem.CheckNumForFullName(name) >= 0)
{ {
return new FLumpPatchSetReader(name); return new FLumpPatchSetReader(name);
} }

View file

@ -2,7 +2,7 @@
** **
** music.cpp ** music.cpp
** **
** music engine - borrowed from GZDoom ** music engine
** **
** Copyright 1999-2016 Randy Heit ** Copyright 1999-2016 Randy Heit
** Copyright 2002-2016 Christoph Oelckers ** Copyright 2002-2016 Christoph Oelckers
@ -35,51 +35,35 @@
** **
*/ */
#include <zmusic.h> #include <stdio.h>
#include "z_music.h" #include <stdlib.h>
#include "zstring.h"
#include "backend/i_sound.h"
#include "name.h" #include "i_sound.h"
#include "s_music.h"
#include "i_music.h" #include "i_music.h"
#include "printf.h" #include "printf.h"
#include "files.h" #include "s_playlist.h"
#include "c_dispatch.h"
#include "filesystem.h" #include "filesystem.h"
#include "cmdlib.h" #include "cmdlib.h"
#include "gamecvars.h" #include "s_music.h"
#include "c_dispatch.h"
#include "gamecontrol.h"
#include "filereadermusicinterface.h" #include "filereadermusicinterface.h"
#include "v_text.h" #include <zmusic.h>
#include "mapinfo.h"
#include "serializer.h"
MusPlayingInfo mus_playing; static bool MusicPaused; // whether music is paused
MusicAliasMap MusicAliases; MusPlayingInfo mus_playing; // music currently being played
MidiDeviceMap MidiDevices; static FPlayList PlayList;
float relative_volume = 1.f;
float saved_relative_volume = 1.0f; // this could be used to implement an ACS FadeMusic function
MusicVolumeMap MusicVolumes; MusicVolumeMap MusicVolumes;
MusicAliasMap LevelMusicAliases; MidiDeviceMap MidiDevices;
bool MusicPaused; // EXTERNAL FUNCTION PROTOTYPES --------------------------------------------
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 extern float S_GetMusicVolume (const char *music);
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"
};
static void S_ActivatePlayList(bool goBack);
FString G_SetupFilenameBasedMusic(const char* fn, const char* defmusic) // PRIVATE DATA DEFINITIONS ------------------------------------------------
{
FString name = StripExtension(fn);
FString test;
// Test if a real file with this name exists with all known extensions for music. // Test if a real file with this name exists with all known extensions for music.
for (auto& ext : knownMusicExts) for (auto& ext : knownMusicExts)
@ -94,77 +78,25 @@ FString G_SetupFilenameBasedMusic(const char* fn, const char* defmusic)
return defmusic; return defmusic;
} }
FString MusicFileExists(const char* fn)
static FileReader DefaultOpenMusic(const char* fn)
{ {
if (mus_extendedlookup) return G_SetupFilenameBasedMusic(fn, nullptr); // This is the minimum needed to make the music system functional.
if (FileExists(fn)) return fn; FileReader fr;
return FString(); fr.OpenFile(fn);
return fr;
} }
static MusicCallbacks mus_cb = { nullptr, DefaultOpenMusic };
int LookupMusicLump(const char* fn)
// PUBLIC DATA DEFINITIONS -------------------------------------------------
// CODE --------------------------------------------------------------------
void S_SetMusicCallbacks(MusicCallbacks* cb)
{ {
if (mus_extendedlookup) mus_cb = *cb;
{ if (mus_cb.OpenMusic == nullptr) mus_cb.OpenMusic = DefaultOpenMusic; // without this we are dead in the water.
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;
} }
//========================================================================== //==========================================================================
@ -240,7 +172,7 @@ static bool S_StartMusicPlaying(ZMusic_MusicStream song, bool loop, float rel_vo
} }
// Notify the sound system of the changed relative volume // Notify the sound system of the changed relative volume
mus_volume.Callback(); snd_musicvolume.Callback();
return true; return true;
} }
@ -287,7 +219,6 @@ void S_ResumeMusic ()
void S_UpdateMusic () void S_UpdateMusic ()
{ {
mus_blocked = false;
if (mus_playing.handle != nullptr) if (mus_playing.handle != nullptr)
{ {
ZMusic_Update(mus_playing.handle); ZMusic_Update(mus_playing.handle);
@ -297,31 +228,61 @@ void S_UpdateMusic ()
// playlist when the current song finishes. // playlist when the current song finishes.
if (!ZMusic_IsPlaying(mus_playing.handle)) if (!ZMusic_IsPlaying(mus_playing.handle))
{ {
if (PlayList.GetNumSongs())
{
PlayList.Advance();
S_ActivatePlayList(false);
}
else
{
S_StopMusic(true); S_StopMusic(true);
} }
} }
}
} }
//========================================================================== //==========================================================================
// //
// S_ChangeCDMusic // Resets the music player if music playback was paused.
// //
// Starts a CD track as music.
//========================================================================== //==========================================================================
bool S_ChangeCDMusic (int track, unsigned int id, bool looping) void S_ResetMusic ()
{ {
char temp[32]; // stop the old music if it has been paused.
// This ensures that the new music is started from the beginning
// if it's the same as the last one and it has been paused.
if (MusicPaused) S_StopMusic(true);
if (id != 0) // start new music for the level
MusicPaused = false;
}
//==========================================================================
//
// S_ActivatePlayList
//
// Plays the next song in the playlist. If no songs in the playlist can be
// played, then it is deleted.
//==========================================================================
void S_ActivatePlayList (bool goBack)
{
int startpos, pos;
startpos = pos = PlayList.GetPosition ();
S_StopMusic (true);
while (!S_ChangeMusic (PlayList.GetSong (pos), 0, false, true))
{ {
mysnprintf (temp, countof(temp), ",CD,%d,%x", track, id); pos = goBack ? PlayList.Backup () : PlayList.Advance ();
} if (pos == startpos)
else
{ {
mysnprintf (temp, countof(temp), ",CD,%d", track); PlayList.Clear();
Printf ("Cannot play anything in the playlist.\n");
return;
}
} }
return S_ChangeMusic (temp, 0, looping);
} }
//========================================================================== //==========================================================================
@ -340,16 +301,26 @@ bool S_StartMusic (const char *m_id)
// //
// S_ChangeMusic // S_ChangeMusic
// //
// Starts playing a music, possibly looping. // initiates playback of a song
// //
// [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) 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 (nomusic) return false; // skip the entire procedure if music is globally disabled.
if (!force && PlayList.GetNumSongs())
{ // Don't change if a playlist is active
return false;
}
// Do game specific lookup.
FString musicname_;
if (mus_cb.LookupFileName)
{
musicname_ = mus_cb.LookupFileName(musicname, order);
musicname = musicname_.GetChars();
}
if (musicname == nullptr || musicname[0] == 0) if (musicname == nullptr || musicname[0] == 0)
{ {
// Don't choke if the map doesn't have a song attached // Don't choke if the map doesn't have a song attached
@ -358,13 +329,10 @@ bool S_ChangeMusic(const char* musicname, int order, bool looping, bool force)
mus_playing.LastSong = ""; mus_playing.LastSong = "";
return true; return true;
} }
if (*musicname == '/') musicname++;
FString DEH_Music;
if (!mus_playing.name.IsEmpty() && if (!mus_playing.name.IsEmpty() &&
mus_playing.handle != nullptr && mus_playing.handle != nullptr &&
stricmp (mus_playing.name, musicname) == 0 && stricmp(mus_playing.name, musicname) == 0 &&
ZMusic_IsLooping(mus_playing.handle) == zmusic_bool(looping)) ZMusic_IsLooping(mus_playing.handle) == zmusic_bool(looping))
{ {
if (order != mus_playing.baseorder) if (order != mus_playing.baseorder)
@ -386,29 +354,10 @@ bool S_ChangeMusic(const char* musicname, int order, bool looping, bool force)
return true; 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);
if (mus_playing.handle == nullptr)
{
Printf("Unable to start CD Audio for track #%d, ID %d\n", track, id);
}
}
else
{
int lumpnum = -1; int lumpnum = -1;
int length = 0; int length = 0;
ZMusic_MusicStream handle = nullptr; ZMusic_MusicStream handle = nullptr;
MidiDeviceSetting *devp = MidiDevices.CheckKey(musicname); MidiDeviceSetting* devp = MidiDevices.CheckKey(musicname);
// Strip off any leading file:// component. // Strip off any leading file:// component.
if (strncmp(musicname, "file://", 7) == 0) if (strncmp(musicname, "file://", 7) == 0)
@ -416,14 +365,15 @@ bool S_ChangeMusic(const char* musicname, int order, bool looping, bool force)
musicname += 7; musicname += 7;
} }
FileReader reader = LookupMusic(musicname); // opening the music must be done by the game because it's different depending on the game's file system use.
FileReader reader = mus_cb.OpenMusic(musicname);
if (!reader.isOpen()) return false; if (!reader.isOpen()) return false;
// shutdown old music // shutdown old music
S_StopMusic (true); S_StopMusic(true);
// Just record it if volume is 0 or music was disabled // Just record it if volume is 0 or music was disabled
if (mus_volume <= 0 || !mus_enabled) if (snd_musicvolume <= 0 || !mus_enabled)
{ {
mus_playing.loop = looping; mus_playing.loop = looping;
mus_playing.name = musicname; mus_playing.name = musicname;
@ -440,13 +390,12 @@ bool S_ChangeMusic(const char* musicname, int order, bool looping, bool force)
else else
{ {
auto mreader = GetMusicReader(reader); // this passes the file reader to the newly created wrapper. auto mreader = GetMusicReader(reader); // this passes the file reader to the newly created wrapper.
mus_playing.handle = ZMusic_OpenSong(mreader, devp? (EMidiDevice)devp->device : MDEV_DEFAULT, devp? devp->args.GetChars() : ""); mus_playing.handle = ZMusic_OpenSong(mreader, devp ? (EMidiDevice)devp->device : MDEV_DEFAULT, devp ? devp->args.GetChars() : "");
if (mus_playing.handle == nullptr) if (mus_playing.handle == nullptr)
{ {
Printf("Unable to load %s: %s\n", mus_playing.name.GetChars(), ZMusic_GetLastError()); Printf("Unable to load %s: %s\n", mus_playing.name.GetChars(), ZMusic_GetLastError());
} }
} }
}
mus_playing.loop = looping; mus_playing.loop = looping;
mus_playing.name = musicname; mus_playing.name = musicname;
@ -477,7 +426,8 @@ bool S_ChangeMusic(const char* musicname, int order, bool looping, bool force)
void S_RestartMusic () void S_RestartMusic ()
{ {
if (!mus_playing.LastSong.IsEmpty() && mus_volume > 0 && mus_enabled) if (snd_musicvolume <= 0) return;
if (!mus_playing.LastSong.IsEmpty() && mus_enabled)
{ {
FString song = mus_playing.LastSong; FString song = mus_playing.LastSong;
mus_playing.LastSong = ""; mus_playing.LastSong = "";
@ -542,7 +492,7 @@ void S_StopMusic (bool force)
try try
{ {
// [RH] Don't stop if a playlist is active. // [RH] Don't stop if a playlist is active.
if (!mus_playing.name.IsEmpty()) if ((force || PlayList.GetNumSongs() == 0) && !mus_playing.name.IsEmpty())
{ {
if (mus_playing.handle != nullptr) if (mus_playing.handle != nullptr)
{ {
@ -556,7 +506,7 @@ void S_StopMusic (bool force)
mus_playing.LastSong = std::move(mus_playing.name); mus_playing.LastSong = std::move(mus_playing.name);
} }
} }
catch (const CRecoverableError& ) catch (const std::runtime_error& )
{ {
//Printf("Unable to stop %s: %s\n", mus_playing.name.GetChars(), err.what()); //Printf("Unable to stop %s: %s\n", mus_playing.name.GetChars(), err.what());
if (mus_playing.handle != nullptr) if (mus_playing.handle != nullptr)
@ -577,10 +527,11 @@ void S_StopMusic (bool force)
CCMD (changemus) CCMD (changemus)
{ {
if (MusicEnabled()) if (!nomusic)
{ {
if (argv.argc() > 1) if (argv.argc() > 1)
{ {
PlayList.Clear();
S_ChangeMusic (argv[1], argv.argc() > 2 ? atoi (argv[2]) : 0); S_ChangeMusic (argv[1], argv.argc() > 2 ? atoi (argv[2]) : 0);
} }
else else
@ -610,104 +561,136 @@ CCMD (changemus)
CCMD (stopmus) CCMD (stopmus)
{ {
PlayList.Clear();
S_StopMusic (false); S_StopMusic (false);
mus_playing.LastSong = ""; // forget the last played song so that it won't get restarted if some volume changes occur 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) //
// CCMD playlist
//
//==========================================================================
UNSAFE_CCMD (playlist)
{ {
if (mus_blocked) return 1; // Caller should believe it succeeded. int argc = argv.argc();
// Store the requested names for resuming.
lastMusicLevel = mapname;
lastMusic = fn;
if (!MusicEnabled())
{
return 0;
}
// Allow per level music substitution. if (argc < 2 || argc > 3)
// 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++; Printf ("playlist <playlist.m3u> [<position>|shuffle]\n");
FName *check = LevelMusicAliases.CheckKey(FName(mapname, true));
if (check) fn = check->GetChars();
} }
else
// 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) if (!PlayList.ChangeList(argv[1]))
{ {
return true; // flagged to be ignored Printf("Could not open " TEXTCOLOR_BOLD "%s" TEXTCOLOR_NORMAL ": %s\n", argv[1], strerror(errno));
} return;
fn = aliasp->GetChars();
} }
if (PlayList.GetNumSongs () > 0)
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 (argc == 3)
if (mus_playing.handle != nullptr && lastStartedMusic.CompareNoCase(fn) == 0 && mus_playing.loop)
return true;
}
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_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_Serialize(FSerializer &arc)
{
if (arc.BeginObject("music"))
{
if (arc.isWriting())
{ {
FString music = mus_playing.name; if (stricmp (argv[2], "shuffle") == 0)
if (music.IsEmpty()) music = mus_playing.LastSong; {
PlayList.Shuffle ();
arc.AddString("music", music); }
else
{
PlayList.SetPosition (atoi (argv[2]));
}
}
S_ActivatePlayList (false);
} }
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() //==========================================================================
//
// CCMD playlistpos
//
//==========================================================================
static bool CheckForPlaylist ()
{ {
S_RestartMusic(); if (PlayList.GetNumSongs() == 0)
{
Printf ("No playlist is playing.\n");
return false;
}
return true;
} }
CCMD (playlistpos)
{
if (CheckForPlaylist() && argv.argc() > 1)
{
PlayList.SetPosition (atoi (argv[1]) - 1);
S_ActivatePlayList (false);
}
}
//==========================================================================
//
// CCMD playlistnext
//
//==========================================================================
CCMD (playlistnext)
{
if (CheckForPlaylist())
{
PlayList.Advance ();
S_ActivatePlayList (false);
}
}
//==========================================================================
//
// CCMD playlistprev
//
//==========================================================================
CCMD (playlistprev)
{
if (CheckForPlaylist())
{
PlayList.Backup ();
S_ActivatePlayList (true);
}
}
//==========================================================================
//
// CCMD playliststatus
//
//==========================================================================
CCMD (playliststatus)
{
if (CheckForPlaylist ())
{
Printf ("Song %d of %d:\n%s\n",
PlayList.GetPosition () + 1,
PlayList.GetNumSongs (),
PlayList.GetSong (PlayList.GetPosition ()));
}
}
//==========================================================================
//
//
//
//==========================================================================
CCMD(currentmusic)
{
if (mus_playing.name.IsNotEmpty())
{
Printf("Currently playing music '%s'\n", mus_playing.name.GetChars());
}
else
{
Printf("Currently no music playing\n");
}
}

View file

@ -43,7 +43,7 @@
//========================================================================== //==========================================================================
// //
// // ADL Midi device
// //
//========================================================================== //==========================================================================
@ -61,8 +61,49 @@
#define FORWARD_STRING_CVAR(key) \ #define FORWARD_STRING_CVAR(key) \
auto ret = ChangeMusicSetting(zmusic_##key, mus_playing.handle,*self); \ auto ret = ChangeMusicSetting(zmusic_##key, mus_playing.handle,*self); \
if (ret) S_MIDIDeviceChanged(-1); if (ret) S_MIDIDeviceChanged(-1);
#ifndef ZMUSIC_LITE
CUSTOM_CVAR(Int, adl_chips_count, 6, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{
FORWARD_CVAR(adl_chips_count);
}
CUSTOM_CVAR(Int, adl_emulator_id, 0, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{
FORWARD_CVAR(adl_emulator_id);
}
CUSTOM_CVAR(Bool, adl_run_at_pcm_rate, 0, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{
FORWARD_BOOL_CVAR(adl_run_at_pcm_rate);
}
CUSTOM_CVAR(Bool, adl_fullpan, 1, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{
FORWARD_BOOL_CVAR(adl_fullpan);
}
CUSTOM_CVAR(Int, adl_bank, 14, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{
FORWARD_CVAR(adl_bank);
}
CUSTOM_CVAR(Bool, adl_use_custom_bank, 0, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{
FORWARD_BOOL_CVAR(adl_use_custom_bank);
}
CUSTOM_CVAR(String, adl_custom_bank, "", CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{
FORWARD_STRING_CVAR(adl_custom_bank);
}
CUSTOM_CVAR(Int, adl_volume_model, 3/*ADLMIDI_VolumeModel_DMX*/, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{
FORWARD_CVAR(adl_bank);
}
#endif
//========================================================================== //==========================================================================
// //
// Fluidsynth MIDI device // Fluidsynth MIDI device
@ -182,10 +223,194 @@ CUSTOM_CVAR(Bool, opl_fullpan, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIR
FORWARD_BOOL_CVAR(opl_fullpan); FORWARD_BOOL_CVAR(opl_fullpan);
} }
#ifndef ZMUSIC_LITE
//==========================================================================
//
// OPN MIDI device
//
//==========================================================================
CUSTOM_CVAR(Int, opn_chips_count, 8, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{
FORWARD_CVAR(opn_chips_count);
}
CUSTOM_CVAR(Int, opn_emulator_id, 0, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{
FORWARD_CVAR(opn_emulator_id);
}
CUSTOM_CVAR(Bool, opn_run_at_pcm_rate, 0, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{
FORWARD_BOOL_CVAR(opn_run_at_pcm_rate);
}
CUSTOM_CVAR(Bool, opn_fullpan, 1, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{
FORWARD_BOOL_CVAR(opn_fullpan);
}
CUSTOM_CVAR(Bool, opn_use_custom_bank, 0, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{
FORWARD_BOOL_CVAR(opn_use_custom_bank);
}
CUSTOM_CVAR(String, opn_custom_bank, "", CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{
FORWARD_STRING_CVAR(opn_custom_bank);
}
//==========================================================================
//
// GUS MIDI device
//
//==========================================================================
CUSTOM_CVAR(String, midi_config, GAMENAMELOWERCASE, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{
FORWARD_STRING_CVAR(gus_config);
}
CUSTOM_CVAR(Bool, midi_dmxgus, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL) // This was 'true' but since it requires special setup that's not such a good idea.
{
FORWARD_BOOL_CVAR(gus_dmxgus);
}
CUSTOM_CVAR(String, gus_patchdir, "", CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{
FORWARD_STRING_CVAR(gus_patchdir);
}
CUSTOM_CVAR(Int, midi_voices, 32, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{
FORWARD_CVAR(gus_midi_voices);
}
CUSTOM_CVAR(Int, gus_memsize, 0, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{
FORWARD_CVAR(gus_memsize);
}
//==========================================================================
//
// Timidity++ device
//
//==========================================================================
CUSTOM_CVAR(Bool, timidity_modulation_wheel, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{
FORWARD_BOOL_CVAR(timidity_modulation_wheel);
}
CUSTOM_CVAR(Bool, timidity_portamento, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{
FORWARD_BOOL_CVAR(timidity_portamento);
}
CUSTOM_CVAR(Int, timidity_reverb, 0, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{
FORWARD_CVAR(timidity_reverb);
}
CUSTOM_CVAR(Int, timidity_reverb_level, 0, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{
FORWARD_CVAR(timidity_reverb_level);
}
CUSTOM_CVAR(Int, timidity_chorus, 0, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{
FORWARD_CVAR(timidity_chorus);
}
CUSTOM_CVAR(Bool, timidity_surround_chorus, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{
FORWARD_BOOL_CVAR(timidity_surround_chorus);
}
CUSTOM_CVAR(Bool, timidity_channel_pressure, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{
FORWARD_BOOL_CVAR(timidity_channel_pressure);
}
CUSTOM_CVAR(Int, timidity_lpf_def, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{
FORWARD_CVAR(timidity_lpf_def);
}
CUSTOM_CVAR(Bool, timidity_temper_control, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{
FORWARD_BOOL_CVAR(timidity_temper_control);
}
CUSTOM_CVAR(Bool, timidity_modulation_envelope, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{
FORWARD_BOOL_CVAR(timidity_modulation_envelope);
}
CUSTOM_CVAR(Bool, timidity_overlap_voice_allow, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{
FORWARD_BOOL_CVAR(timidity_overlap_voice_allow);
}
CUSTOM_CVAR(Bool, timidity_drum_effect, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{
FORWARD_BOOL_CVAR(timidity_drum_effect);
}
CUSTOM_CVAR(Bool, timidity_pan_delay, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{
FORWARD_BOOL_CVAR(timidity_pan_delay);
}
CUSTOM_CVAR(Float, timidity_drum_power, 1.0, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL) /* coef. of drum amplitude */
{
FORWARD_CVAR(timidity_drum_power);
}
CUSTOM_CVAR(Int, timidity_key_adjust, 0, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{
FORWARD_CVAR(timidity_key_adjust);
}
CUSTOM_CVAR(Float, timidity_tempo_adjust, 1, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{
FORWARD_CVAR(timidity_tempo_adjust);
}
CUSTOM_CVAR(Float, timidity_min_sustain_time, 5000, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{
FORWARD_CVAR(timidity_min_sustain_time);
}
#endif
CUSTOM_CVAR(String, timidity_config, GAMENAMELOWERCASE, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL) CUSTOM_CVAR(String, timidity_config, GAMENAMELOWERCASE, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{ {
FORWARD_STRING_CVAR(timidity_config); FORWARD_STRING_CVAR(timidity_config);
} }
#ifndef ZMUSIC_LITE
//==========================================================================
//
// WildMidi
//
//==========================================================================
CUSTOM_CVAR(String, wildmidi_config, "", CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{
FORWARD_STRING_CVAR(wildmidi_config);
}
CUSTOM_CVAR(Bool, wildmidi_reverb, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{
FORWARD_BOOL_CVAR(wildmidi_reverb);
}
CUSTOM_CVAR(Bool, wildmidi_enhanced_resampling, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{
FORWARD_BOOL_CVAR(wildmidi_enhanced_resampling);
}
#endif
//========================================================================== //==========================================================================
// //

View file

@ -43,6 +43,8 @@
#include "sc_man.h" #include "sc_man.h"
#include <zmusic.h> #include <zmusic.h>
#include "raze_music.h"
// MACROS ------------------------------------------------------------------ // MACROS ------------------------------------------------------------------
enum SICommands enum SICommands

View file

@ -7,6 +7,15 @@
#include "name.h" #include "name.h"
#include <zmusic.h> #include <zmusic.h>
class FileReader;
struct MusicCallbacks
{
FString(*LookupFileName)(const char* fn, int &order);
FileReader(*OpenMusic)(const char* fn);
};
void S_SetMusicCallbacks(MusicCallbacks* cb);
void S_CreateStream(); void S_CreateStream();
void S_PauseStream(bool pause); void S_PauseStream(bool pause);
void S_StopStream(); void S_StopStream();
@ -15,7 +24,7 @@ void S_SetStreamVolume(float vol);
// //
void S_InitMusic (); void S_InitMusic ();
void S_StartMusic (); void S_ResetMusic ();
// Start music using <music_name> // Start music using <music_name>
@ -24,9 +33,6 @@ bool S_StartMusic (const char *music_name);
// Start music using <music_name>, and set whether looping // Start music using <music_name>, and set whether looping
bool S_ChangeMusic (const char *music_name, int order=0, bool looping=true, bool force=false); bool S_ChangeMusic (const char *music_name, int order=0, bool looping=true, bool force=false);
// Start playing a cd track as music
bool S_ChangeCDMusic (int track, unsigned int id=0, bool looping=true);
void S_RestartMusic (); void S_RestartMusic ();
void S_MIDIDeviceChanged(int newdev); void S_MIDIDeviceChanged(int newdev);
@ -50,11 +56,9 @@ struct MidiDeviceSetting
FString args; FString args;
}; };
typedef TMap<FName, FName> MusicAliasMap;
typedef TMap<FName, MidiDeviceSetting> MidiDeviceMap; typedef TMap<FName, MidiDeviceSetting> MidiDeviceMap;
typedef TMap<FName, float> MusicVolumeMap; typedef TMap<FName, float> MusicVolumeMap;
extern MusicAliasMap MusicAliases;
extern MidiDeviceMap MidiDevices; extern MidiDeviceMap MidiDevices;
extern MusicVolumeMap MusicVolumes; extern MusicVolumeMap MusicVolumes;
@ -71,8 +75,5 @@ extern MusPlayingInfo mus_playing;
extern float relative_volume, saved_relative_volume; extern float relative_volume, saved_relative_volume;
// Note for later when the OPL player is ported.
// DN3D and related games use "d3dtimbr.tmb"
#endif #endif

282
source/core/raze_music.cpp Normal file
View file

@ -0,0 +1,282 @@
/*
**
** 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.GetChars()).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.
// 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_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_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);
}

View file

@ -1,10 +1,15 @@
#pragma once #pragma once
#include "zstring.h" #include "zstring.h"
#include "tarray.h"
#include "name.h"
typedef TMap<FName, FName> MusicAliasMap;
extern MusicAliasMap MusicAliases;
// Totally minimalistic interface - should be all the game modules need. // Totally minimalistic interface - should be all the game modules need.
void Mus_InitMusic();
void Mus_Init(); void Mus_UpdateMusic();
int Mus_Play(const char *mapname, const char *fn, bool loop); int Mus_Play(const char *mapname, const char *fn, bool loop);
void Mus_Stop(); void Mus_Stop();
bool Mus_IsPlaying(); bool Mus_IsPlaying();

View file

@ -49,7 +49,7 @@
#include "m_argv.h" #include "m_argv.h"
#include "serializer.h" #include "serializer.h"
#include "version.h" #include "version.h"
#include "z_music.h" #include "raze_music.h"
#include "s_soundinternal.h" #include "s_soundinternal.h"
static CompositeSavegameWriter savewriter; static CompositeSavegameWriter savewriter;

View file

@ -48,13 +48,13 @@
#include "c_cvars.h" #include "c_cvars.h"
#include "stats.h" #include "stats.h"
#include "s_music.h" #include "s_music.h"
#include "z_music.h" #include "raze_music.h"
#include "gamecvars.h" #include "gamecvars.h"
#include "gamecontrol.h" #include "gamecontrol.h"
#include <zmusic.h> #include <zmusic.h>
EXTERN_CVAR (Float, snd_sfxvolume) EXTERN_CVAR (Float, snd_sfxvolume)
EXTERN_CVAR(Float, mus_volume) EXTERN_CVAR(Float, snd_musicvolume)
CVAR (Int, snd_samplerate, 0, CVAR_ARCHIVE|CVAR_GLOBALCONFIG) CVAR (Int, snd_samplerate, 0, CVAR_ARCHIVE|CVAR_GLOBALCONFIG)
CVAR (Int, snd_buffersize, 0, CVAR_ARCHIVE|CVAR_GLOBALCONFIG) CVAR (Int, snd_buffersize, 0, CVAR_ARCHIVE|CVAR_GLOBALCONFIG)
CVAR (Int, snd_hrtf, -1, CVAR_ARCHIVE|CVAR_GLOBALCONFIG) CVAR (Int, snd_hrtf, -1, CVAR_ARCHIVE|CVAR_GLOBALCONFIG)
@ -95,7 +95,7 @@ CUSTOM_CVAR(Float, snd_mastervolume, 1.f, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVA
ChangeMusicSetting(zmusic_snd_mastervolume, nullptr, self); ChangeMusicSetting(zmusic_snd_mastervolume, nullptr, self);
snd_sfxvolume.Callback(); snd_sfxvolume.Callback();
mus_volume.Callback(); snd_musicvolume.Callback();
} }
//========================================================================== //==========================================================================

View file

@ -33,7 +33,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "savegamehelp.h" #include "savegamehelp.h"
#include "menu/menu.h" #include "menu/menu.h"
#include "mapinfo.h" #include "mapinfo.h"
#include "z_music.h" #include "raze_music.h"
BEGIN_DUKE_NS BEGIN_DUKE_NS

View file

@ -46,6 +46,13 @@ double g_moveActorsTime, g_moveWorldTime; // in ms, smoothed
int32_t g_noLogoAnim = 0; int32_t g_noLogoAnim = 0;
int32_t g_noLogo = 0; int32_t g_noLogo = 0;
void PlayBonusMusic()
{
if (MusicEnabled() && mus_enabled)
S_PlaySound(BONUSMUSIC, CHAN_AUTO, CHANF_UI);
}
////////// OFTEN-USED FEW-LINERS ////////// ////////// OFTEN-USED FEW-LINERS //////////
#ifndef EDUKE32_STANDALONE #ifndef EDUKE32_STANDALONE
static void G_HandleEventsWhileNoInput(void) static void G_HandleEventsWhileNoInput(void)
@ -1921,8 +1928,7 @@ void G_BonusScreen(int32_t bonusonly)
videoClearScreen(0); videoClearScreen(0);
G_DisplayMPResultsScreen(); G_DisplayMPResultsScreen();
if (MusicEnabled() && mus_enabled) PlayBonusMusic();
S_PlaySound(BONUSMUSIC, CHAN_AUTO, CHANF_UI);
videoNextPage(); videoNextPage();
inputState.ClearAllInput(); inputState.ClearAllInput();
@ -1960,8 +1966,7 @@ void G_BonusScreen(int32_t bonusonly)
gametext_center_shade(192, GStrings("PRESSKEY"), quotepulseshade); gametext_center_shade(192, GStrings("PRESSKEY"), quotepulseshade);
if (MusicEnabled() && mus_enabled) PlayBonusMusic();
S_PlaySound(BONUSMUSIC, CHAN_AUTO, CHANF_UI);
videoNextPage(); videoNextPage();
inputState.ClearAllInput(); inputState.ClearAllInput();

View file

@ -24,7 +24,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "compat.h" #include "compat.h"
#include "duke3d.h" #include "duke3d.h"
#include "z_music.h" #include "raze_music.h"
#include "mapinfo.h" #include "mapinfo.h"
#include "sound/s_soundinternal.h" #include "sound/s_soundinternal.h"

View file

@ -31,7 +31,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "sounds_common.h" #include "sounds_common.h"
#include "sound/s_soundinternal.h" #include "sound/s_soundinternal.h"
#include "z_music.h" #include "raze_music.h"
BEGIN_DUKE_NS BEGIN_DUKE_NS

View file

@ -24,7 +24,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "exhumed.h" #include "exhumed.h"
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include "z_music.h" #include "raze_music.h"
BEGIN_PS_NS BEGIN_PS_NS

View file

@ -28,7 +28,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "i_specialpaths.h" #include "i_specialpaths.h"
#include "gamecontrol.h" #include "gamecontrol.h"
#include "version.h" #include "version.h"
#include "z_music.h" #include "raze_music.h"
#include "mapinfo.h" #include "mapinfo.h"
#include "savegamehelp.h" #include "savegamehelp.h"

View file

@ -46,6 +46,13 @@ double g_moveActorsTime, g_moveWorldTime; // in ms, smoothed
int32_t g_noLogoAnim = 0; int32_t g_noLogoAnim = 0;
int32_t g_noLogo = 0; int32_t g_noLogo = 0;
void PlayBonusMusic()
{
if (MusicEnabled() && mus_enabled)
S_PlaySound(BONUSMUSIC, CHAN_AUTO, CHANF_UI);
}
////////// OFTEN-USED FEW-LINERS ////////// ////////// OFTEN-USED FEW-LINERS //////////
static void G_HandleEventsWhileNoInput(void) static void G_HandleEventsWhileNoInput(void)
{ {
@ -1930,8 +1937,7 @@ void G_BonusScreen(int32_t bonusonly)
videoClearScreen(0); videoClearScreen(0);
G_DisplayMPResultsScreen(); G_DisplayMPResultsScreen();
if (MusicEnabled() && mus_enabled) PlayBonusMusic();
S_PlaySound(BONUSMUSIC, CHAN_AUTO, CHANF_UI);
videoNextPage(); videoNextPage();
inputState.ClearAllInput(); inputState.ClearAllInput();
@ -1970,10 +1976,9 @@ void G_BonusScreen(int32_t bonusonly)
menutext_center(20-6, lastmapname); menutext_center(20-6, lastmapname);
menutext_center(36-6, GStrings("Completed")); menutext_center(36-6, GStrings("Completed"));
gametext_center_shade(192, GStrings("PRESSKEY"), quotepulseshade); gametext_center_shade(192, GStrings("PRESSKEY"), quotepulseshade);
if (MusicEnabled() && mus_enabled) PlayBonusMusic();
S_PlaySound(BONUSMUSIC, CHAN_AUTO, CHANF_UI);
} }
else else
{ {
@ -2504,8 +2509,7 @@ void G_BonusScreenRRRA(int32_t bonusonly)
videoClearScreen(0); videoClearScreen(0);
G_DisplayMPResultsScreen(); G_DisplayMPResultsScreen();
if (MusicEnabled() && mus_enabled) PlayBonusMusic();
S_PlaySound(BONUSMUSIC, CHAN_AUTO, CHANF_UI);
videoNextPage(); videoNextPage();
inputState.ClearAllInput(); inputState.ClearAllInput();

View file

@ -24,7 +24,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "compat.h" #include "compat.h"
#include "duke3d.h" #include "duke3d.h"
#include "z_music.h" #include "raze_music.h"
#include "mapinfo.h" #include "mapinfo.h"
#include "sound/s_soundinternal.h" #include "sound/s_soundinternal.h"

View file

@ -31,7 +31,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "sounds_common.h" #include "sounds_common.h"
#include "sound/s_soundinternal.h" #include "sound/s_soundinternal.h"
#include "z_music.h" #include "raze_music.h"
BEGIN_RR_NS BEGIN_RR_NS

View file

@ -94,7 +94,7 @@ Things required to make savegames work:
#include "m_argv.h" #include "m_argv.h"
#include "debugbreak.h" #include "debugbreak.h"
#include "menu/menu.h" #include "menu/menu.h"
#include "z_music.h" #include "raze_music.h"
#include "statistics.h" #include "statistics.h"
#include "gstrings.h" #include "gstrings.h"
#include "mapinfo.h" #include "mapinfo.h"

View file

@ -65,7 +65,7 @@ Prepared for public release: 03/28/2005 - Charlie Wiederhold, 3D Realms
#include "interpso.h" #include "interpso.h"
#include "menu/menu.h" #include "menu/menu.h"
#include "gstrings.h" #include "gstrings.h"
#include "z_music.h" #include "raze_music.h"
BEGIN_SW_NS BEGIN_SW_NS

View file

@ -54,7 +54,7 @@ Prepared for public release: 03/28/2005 - Charlie Wiederhold, 3D Realms
#include "player.h" #include "player.h"
#include "i_specialpaths.h" #include "i_specialpaths.h"
#include "savegamehelp.h" #include "savegamehelp.h"
#include "z_music.h" #include "raze_music.h"
#include "mapinfo.h" #include "mapinfo.h"
//void TimerFunc(task * Task); //void TimerFunc(task * Task);

View file

@ -49,7 +49,7 @@ Prepared for public release: 03/28/2005 - Charlie Wiederhold, 3D Realms
#include "menus.h" #include "menus.h"
#include "config.h" #include "config.h"
#include "menu/menu.h" #include "menu/menu.h"
#include "z_music.h" #include "raze_music.h"
#include "sound/s_soundinternal.h" #include "sound/s_soundinternal.h"
#include "filesystem.h" #include "filesystem.h"
#include "serializer.h" #include "serializer.h"

View file

@ -1387,7 +1387,7 @@ OptionMenu SoundOptions //protected
Option "$SNDMNU_SNDENABLED", "snd_enabled", "YesNo" Option "$SNDMNU_SNDENABLED", "snd_enabled", "YesNo"
Slider "$SNDMNU_SFXVOLUME", "snd_sfxvolume", 0, 1, 0.05, 2 Slider "$SNDMNU_SFXVOLUME", "snd_sfxvolume", 0, 1, 0.05, 2
Option "$SNDMNU_MUSENABLED", "mus_enabled", "YesNo" Option "$SNDMNU_MUSENABLED", "mus_enabled", "YesNo"
Slider "$SNDMNU_MUSICVOLUME", "mus_volume", 0, 1, 0.05, 2 Slider "$SNDMNU_MUSICVOLUME", "snd_musicvolume", 0, 1, 0.05, 2
Option "$SNDMNU_MENUSOUND", "menu_sounds", "OnOff" // placeholder until the slider can be made to work Option "$SNDMNU_MENUSOUND", "menu_sounds", "OnOff" // placeholder until the slider can be made to work
//Slider "$SNDMNU_MENUVOLUME", "snd_menuvolume", 0, 1, 0.05, 2 //Slider "$SNDMNU_MENUVOLUME", "snd_menuvolume", 0, 1, 0.05, 2
StaticText " " StaticText " "