mirror of
https://github.com/DrBeef/Raze.git
synced 2024-12-14 22:51:42 +00:00
f28aa8f06c
This is not relevant for any of the stock movies as they use separate sound files, we need to be aware of mods using the streaming sound capabilities of MVE and SMK.
1016 lines
26 KiB
C++
1016 lines
26 KiB
C++
/*
|
|
**
|
|
** music.cpp
|
|
**
|
|
** music engine
|
|
**
|
|
** 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 <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <stdexcept>
|
|
|
|
#include "i_sound.h"
|
|
#include "i_music.h"
|
|
#include "printf.h"
|
|
#include "s_playlist.h"
|
|
#include "c_dispatch.h"
|
|
#include "filesystem.h"
|
|
#include "cmdlib.h"
|
|
#include "s_music.h"
|
|
#include "filereadermusicinterface.h"
|
|
#include <zmusic.h>
|
|
#include "md5.h"
|
|
#include "gain_analysis.h"
|
|
#include "gameconfigfile.h"
|
|
#include "i_specialpaths.h"
|
|
|
|
// EXTERNAL FUNCTION PROTOTYPES --------------------------------------------
|
|
|
|
extern float S_GetMusicVolume (const char *music);
|
|
|
|
static void S_ActivatePlayList(bool goBack);
|
|
|
|
// PRIVATE DATA DEFINITIONS ------------------------------------------------
|
|
|
|
static bool MusicPaused; // whether music is paused
|
|
MusPlayingInfo mus_playing; // music currently being played
|
|
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;
|
|
MidiDeviceMap MidiDevices;
|
|
|
|
static FileReader DefaultOpenMusic(const char* fn)
|
|
{
|
|
// This is the minimum needed to make the music system functional.
|
|
FileReader fr;
|
|
fr.OpenFile(fn);
|
|
return fr;
|
|
}
|
|
static MusicCallbacks mus_cb = { nullptr, DefaultOpenMusic };
|
|
|
|
|
|
// PUBLIC DATA DEFINITIONS -------------------------------------------------
|
|
EXTERN_CVAR(Int, snd_mididevice)
|
|
EXTERN_CVAR(Float, mod_dumb_mastervolume)
|
|
EXTERN_CVAR(Float, fluid_gain)
|
|
|
|
|
|
CVAR(Bool, mus_calcgain, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) // changing this will only take effect for the next song.
|
|
CVAR(Bool, mus_usereplaygain, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) // changing this will only take effect for the next song.
|
|
CUSTOM_CVAR(Float, mus_gainoffset, 0.f, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) // for customizing the base volume
|
|
{
|
|
if (self > 10.f) self = 10.f;
|
|
mus_playing.replayGainFactor = dBToAmplitude(mus_playing.replayGain + mus_gainoffset);
|
|
}
|
|
|
|
// CODE --------------------------------------------------------------------
|
|
|
|
void S_SetMusicCallbacks(MusicCallbacks* cb)
|
|
{
|
|
mus_cb = *cb;
|
|
if (mus_cb.OpenMusic == nullptr) mus_cb.OpenMusic = DefaultOpenMusic; // without this we are dead in the water.
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
//
|
|
//
|
|
// Create a sound system stream for the currently playing song
|
|
//==========================================================================
|
|
|
|
static std::unique_ptr<SoundStream> musicStream;
|
|
static TArray<SoundStream*> customStreams;
|
|
|
|
SoundStream *S_CreateCustomStream(size_t size, int samplerate, int numchannels, StreamCallback cb, void *userdata)
|
|
{
|
|
int flags = 0;
|
|
if (numchannels < 2) flags |= SoundStream::Mono;
|
|
auto stream = GSnd->CreateStream(cb, int(size), flags, samplerate, userdata);
|
|
if (stream)
|
|
{
|
|
stream->Play(true, 1);
|
|
customStreams.Push(stream);
|
|
}
|
|
return stream;
|
|
}
|
|
|
|
void S_StopCustomStream(SoundStream *stream)
|
|
{
|
|
if (stream)
|
|
{
|
|
stream->Stop();
|
|
auto f = customStreams.Find(stream);
|
|
if (f < customStreams.Size()) customStreams.Delete(f);
|
|
delete stream;
|
|
}
|
|
}
|
|
|
|
void S_PauseAllCustomStreams(bool on)
|
|
{
|
|
for (auto s : customStreams)
|
|
{
|
|
s->SetPaused(on);
|
|
}
|
|
}
|
|
|
|
static TArray<int16_t> convert;
|
|
static bool FillStream(SoundStream* stream, void* buff, int len, void* userdata)
|
|
{
|
|
bool written;
|
|
if (mus_playing.isfloat)
|
|
{
|
|
written = ZMusic_FillStream(mus_playing.handle, buff, len);
|
|
if (mus_playing.replayGainFactor != 1.f)
|
|
{
|
|
float* fbuf = (float*)buff;
|
|
for (int i = 0; i < len / 4; i++)
|
|
{
|
|
fbuf[i] *= mus_playing.replayGainFactor;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// To apply replay gain we need floating point streaming data, so 16 bit input needs to be converted here.
|
|
convert.Resize(len / 2);
|
|
written = ZMusic_FillStream(mus_playing.handle, convert.Data(), len/2);
|
|
float* fbuf = (float*)buff;
|
|
for (int i = 0; i < len / 4; i++)
|
|
{
|
|
fbuf[i] = convert[i] * mus_playing.replayGainFactor * (1.f/32768.f);
|
|
}
|
|
}
|
|
|
|
if (!written)
|
|
{
|
|
memset((char*)buff, 0, len);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
void S_CreateStream()
|
|
{
|
|
if (!mus_playing.handle) return;
|
|
SoundStreamInfo fmt;
|
|
ZMusic_GetStreamInfo(mus_playing.handle, &fmt);
|
|
// always create a floating point streaming buffer so we can apply replay gain without risk of integer overflows.
|
|
mus_playing.isfloat = fmt.mNumChannels > 0;
|
|
if (!mus_playing.isfloat) fmt.mBufferSize *= 2;
|
|
if (fmt.mBufferSize > 0) // if buffer size is 0 the library will play the song itself (e.g. Windows system synth.)
|
|
{
|
|
int flags = 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 bool S_StartMusicPlaying(ZMusic_MusicStream song, bool loop, float rel_vol, int subsong)
|
|
{
|
|
if (rel_vol > 0.f && !mus_usereplaygain)
|
|
{
|
|
float factor = relative_volume / saved_relative_volume;
|
|
saved_relative_volume = rel_vol;
|
|
I_SetRelativeVolume(saved_relative_volume * factor);
|
|
}
|
|
ZMusic_Stop(song);
|
|
// make sure the volume modifiers update properly in case replay gain settings have changed.
|
|
fluid_gain.Callback();
|
|
mod_dumb_mastervolume.Callback();
|
|
if (!ZMusic_Start(song, subsong, loop))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Notify the sound system of the changed relative volume
|
|
snd_musicvolume.Callback();
|
|
return true;
|
|
}
|
|
|
|
|
|
//==========================================================================
|
|
//
|
|
// S_PauseMusic
|
|
//
|
|
// Stop music, during game PAUSE.
|
|
//==========================================================================
|
|
|
|
void S_PauseMusic ()
|
|
{
|
|
if (mus_playing.handle && !MusicPaused)
|
|
{
|
|
ZMusic_Pause(mus_playing.handle);
|
|
S_PauseStream(true);
|
|
MusicPaused = true;
|
|
}
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
// S_ResumeMusic
|
|
//
|
|
// Resume music, 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 ()
|
|
{
|
|
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))
|
|
{
|
|
if (PlayList.GetNumSongs())
|
|
{
|
|
PlayList.Advance();
|
|
S_ActivatePlayList(false);
|
|
}
|
|
else
|
|
{
|
|
S_StopMusic(true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
// Resets the music player if music playback was paused.
|
|
//
|
|
//==========================================================================
|
|
|
|
void S_ResetMusic ()
|
|
{
|
|
// 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);
|
|
|
|
// 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))
|
|
{
|
|
pos = goBack ? PlayList.Backup () : PlayList.Advance ();
|
|
if (pos == startpos)
|
|
{
|
|
PlayList.Clear();
|
|
Printf ("Cannot play anything in the playlist.\n");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
// 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
|
|
//
|
|
// initiates playback of a song
|
|
//
|
|
//==========================================================================
|
|
static TMap<FString, float> gainMap;
|
|
|
|
EXTERN_CVAR(String, fluid_patchset)
|
|
EXTERN_CVAR(String, timidity_config)
|
|
EXTERN_CVAR(String, midi_config)
|
|
EXTERN_CVAR(String, wildmidi_config)
|
|
EXTERN_CVAR(String, adl_custom_bank)
|
|
EXTERN_CVAR(Int, adl_bank)
|
|
EXTERN_CVAR(Bool, adl_use_custom_bank)
|
|
EXTERN_CVAR(String, opn_custom_bank)
|
|
EXTERN_CVAR(Bool, opn_use_custom_bank)
|
|
EXTERN_CVAR(Int, opl_core)
|
|
|
|
static FString ReplayGainHash(ZMusicCustomReader* reader, int flength, int playertype, const char* _playparam)
|
|
{
|
|
std::string playparam = _playparam;
|
|
|
|
uint8_t buffer[50000]; // for performance reasons only hash the start of the file. If we wanted to do this to large waveform songs it'd cause noticable lag.
|
|
uint8_t digest[16];
|
|
char digestout[33];
|
|
auto length = reader->read(reader, buffer, 50000);
|
|
reader->seek(reader, 0, SEEK_SET);
|
|
MD5Context md5;
|
|
md5.Init();
|
|
md5.Update(buffer, (int)length);
|
|
md5.Final(digest);
|
|
|
|
for (size_t j = 0; j < sizeof(digest); ++j)
|
|
{
|
|
sprintf(digestout + (j * 2), "%02X", digest[j]);
|
|
}
|
|
digestout[32] = 0;
|
|
|
|
auto type = ZMusic_IdentifyMIDIType((uint32_t*)buffer, 32);
|
|
if (type == MIDI_NOTMIDI) return FStringf("%d:%s", flength, digestout);
|
|
|
|
// get the default for MIDI synth
|
|
if (playertype == -1)
|
|
{
|
|
switch (snd_mididevice)
|
|
{
|
|
case -1: playertype = MDEV_FLUIDSYNTH; break;
|
|
case -2: playertype = MDEV_TIMIDITY; break;
|
|
case -3: playertype = MDEV_OPL; break;
|
|
case -4: playertype = MDEV_GUS; break;
|
|
case -5: playertype = MDEV_FLUIDSYNTH; break;
|
|
case -6: playertype = MDEV_WILDMIDI; break;
|
|
case -7: playertype = MDEV_ADL; break;
|
|
case -8: playertype = MDEV_OPN; break;
|
|
default: return "";
|
|
}
|
|
}
|
|
else if (playertype == MDEV_SNDSYS) return "";
|
|
|
|
// get the default for used sound font.
|
|
if (playparam.empty())
|
|
{
|
|
switch (playertype)
|
|
{
|
|
case MDEV_FLUIDSYNTH: playparam = fluid_patchset; break;
|
|
case MDEV_TIMIDITY: playparam = timidity_config; break;
|
|
case MDEV_GUS: playparam = midi_config; break;
|
|
case MDEV_WILDMIDI: playparam = wildmidi_config; break;
|
|
case MDEV_ADL: playparam = adl_use_custom_bank ? *adl_custom_bank : std::to_string(adl_bank); break;
|
|
case MDEV_OPN: playparam = opn_use_custom_bank ? *opn_custom_bank : ""; break;
|
|
case MDEV_OPL: playparam = std::to_string(opl_core); break;
|
|
|
|
}
|
|
}
|
|
return FStringf("%d:%s:%d:%s", flength, digestout, playertype, playparam.c_str()).MakeUpper();
|
|
}
|
|
|
|
static void SaveGains()
|
|
{
|
|
auto path = M_GetAppDataPath(true);
|
|
path << "/replaygain.ini";
|
|
FConfigFile gains(path);
|
|
TMap<FString, float>::Iterator it(gainMap);
|
|
TMap<FString, float>::Pair* pair;
|
|
|
|
if (gains.SetSection("Gains", true))
|
|
{
|
|
while (it.NextPair(pair))
|
|
{
|
|
gains.SetValueForKey(pair->Key, std::to_string(pair->Value).c_str());
|
|
}
|
|
}
|
|
gains.WriteConfigFile();
|
|
}
|
|
|
|
static void ReadGains()
|
|
{
|
|
static bool done = false;
|
|
if (done) return;
|
|
done = true;
|
|
auto path = M_GetAppDataPath(true);
|
|
path << "/replaygain.ini";
|
|
FConfigFile gains(path);
|
|
if (gains.SetSection("Gains"))
|
|
{
|
|
const char* key;
|
|
const char* value;
|
|
|
|
while (gains.NextInSection(key, value))
|
|
{
|
|
gainMap.Insert(key, (float)strtod(value, nullptr));
|
|
}
|
|
}
|
|
}
|
|
|
|
CCMD(setreplaygain)
|
|
{
|
|
// sets replay gain for current song to a fixed value
|
|
if (!mus_playing.handle || mus_playing.hash.IsEmpty())
|
|
{
|
|
Printf("setreplaygain needs some music playing\n");
|
|
return;
|
|
}
|
|
if (argv.argc() < 2)
|
|
{
|
|
Printf("Usage: setreplaygain {dB}\n");
|
|
Printf("Current replay gain is %f dB\n", mus_playing.replayGain);
|
|
return;
|
|
}
|
|
float dB = (float)strtod(argv[1], nullptr);
|
|
if (dB > 10) dB = 10; // don't blast the speakers. Values above 2 or 3 are very rare.
|
|
gainMap.Insert(mus_playing.hash, dB);
|
|
SaveGains();
|
|
mus_playing.replayGain = dB;
|
|
mus_playing.replayGainFactor = (float)dBToAmplitude(mus_playing.replayGain + mus_gainoffset);
|
|
}
|
|
|
|
static void CheckReplayGain(const char *musicname, EMidiDevice playertype, const char *playparam)
|
|
{
|
|
mus_playing.replayGain = 0.f;
|
|
mus_playing.replayGainFactor = dBToAmplitude(mus_gainoffset);
|
|
fluid_gain.Callback();
|
|
mod_dumb_mastervolume.Callback();
|
|
if (!mus_usereplaygain) return;
|
|
|
|
FileReader reader = mus_cb.OpenMusic(musicname);
|
|
if (!reader.isOpen()) return;
|
|
int flength = (int)reader.GetLength();
|
|
auto mreader = GetMusicReader(reader); // this passes the file reader to the newly created wrapper.
|
|
|
|
ReadGains();
|
|
auto hash = ReplayGainHash(mreader, flength, playertype, playparam);
|
|
if (hash.IsEmpty()) return; // got nothing to measure.
|
|
mus_playing.hash = hash;
|
|
auto entry = gainMap.CheckKey(hash);
|
|
if (entry)
|
|
{
|
|
mus_playing.replayGain = *entry;
|
|
mus_playing.replayGainFactor = dBToAmplitude(mus_playing.replayGain + mus_gainoffset);
|
|
return;
|
|
}
|
|
if (!mus_calcgain) return;
|
|
|
|
auto handle = ZMusic_OpenSong(mreader, playertype, playparam);
|
|
if (handle == nullptr) return; // not a music file
|
|
|
|
if (!ZMusic_Start(handle, 0, false))
|
|
{
|
|
ZMusic_Close(handle);
|
|
return; // unable to open
|
|
}
|
|
|
|
SoundStreamInfo fmt;
|
|
ZMusic_GetStreamInfo(handle, &fmt);
|
|
if (fmt.mBufferSize == 0)
|
|
{
|
|
ZMusic_Close(handle);
|
|
return; // external player.
|
|
}
|
|
|
|
int flags = SoundStream::Float;
|
|
if (abs(fmt.mNumChannels) < 2) flags |= SoundStream::Mono;
|
|
|
|
TArray<uint8_t> readbuffer(fmt.mBufferSize, true);
|
|
TArray<float> lbuffer;
|
|
TArray<float> rbuffer;
|
|
while (ZMusic_FillStream(handle, readbuffer.Data(), fmt.mBufferSize))
|
|
{
|
|
unsigned index;
|
|
// 4 cases, all with different preparation needs.
|
|
if (fmt.mNumChannels == -2) // 16 bit stereo
|
|
{
|
|
int16_t* sbuf = (int16_t*)readbuffer.Data();
|
|
int numsamples = fmt.mBufferSize / 4;
|
|
index = lbuffer.Reserve(numsamples);
|
|
rbuffer.Reserve(numsamples);
|
|
|
|
for (int i = 0; i < numsamples; i++)
|
|
{
|
|
lbuffer[index + i] = sbuf[i * 2];
|
|
rbuffer[index + i] = sbuf[i * 2 + 1];
|
|
}
|
|
}
|
|
else if (fmt.mNumChannels == -1) // 16 bit mono
|
|
{
|
|
int16_t* sbuf = (int16_t*)readbuffer.Data();
|
|
int numsamples = fmt.mBufferSize / 2;
|
|
index = lbuffer.Reserve(numsamples);
|
|
|
|
for (int i = 0; i < numsamples; i++)
|
|
{
|
|
lbuffer[index + i] = sbuf[i];
|
|
}
|
|
}
|
|
else if (fmt.mNumChannels == 1) // float mono
|
|
{
|
|
float* sbuf = (float*)readbuffer.Data();
|
|
int numsamples = fmt.mBufferSize / 4;
|
|
index = lbuffer.Reserve(numsamples);
|
|
for (int i = 0; i < numsamples; i++)
|
|
{
|
|
lbuffer[index + i] = sbuf[i] * 32768.f;
|
|
}
|
|
}
|
|
else if (fmt.mNumChannels == 2) // float stereo
|
|
{
|
|
float* sbuf = (float*)readbuffer.Data();
|
|
int numsamples = fmt.mBufferSize / 8;
|
|
auto index = lbuffer.Reserve(numsamples);
|
|
rbuffer.Reserve(numsamples);
|
|
|
|
for (int i = 0; i < numsamples; i++)
|
|
{
|
|
lbuffer[index + i] = sbuf[i * 2] * 32768.f;
|
|
rbuffer[index + i] = sbuf[i * 2 + 1] * 32768.f;
|
|
}
|
|
}
|
|
float accTime = lbuffer.Size() / (float)fmt.mSampleRate;
|
|
if (accTime > 8 * 60) break; // do at most 8 minutes, if the song forces a loop.
|
|
}
|
|
ZMusic_Close(handle);
|
|
|
|
GainAnalyzer analyzer;
|
|
analyzer.InitGainAnalysis(fmt.mSampleRate);
|
|
int result = analyzer.AnalyzeSamples(lbuffer.Data(), rbuffer.Size() == 0 ? nullptr : rbuffer.Data(), lbuffer.Size(), rbuffer.Size() == 0? 1: 2);
|
|
if (result == GAIN_ANALYSIS_OK)
|
|
{
|
|
auto gain = analyzer.GetTitleGain();
|
|
Printf("Calculated replay gain for %s at %f dB\n", hash.GetChars(), gain);
|
|
|
|
gainMap.Insert(hash, gain);
|
|
mus_playing.replayGain = gain;
|
|
mus_playing.replayGainFactor = dBToAmplitude(mus_playing.replayGain + mus_gainoffset);
|
|
SaveGains();
|
|
}
|
|
}
|
|
|
|
bool S_ChangeMusic(const char* musicname, int order, bool looping, bool force)
|
|
{
|
|
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 true; // do not report an error here.
|
|
}
|
|
// 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)
|
|
{
|
|
// Don't choke if the map doesn't have a song attached
|
|
S_StopMusic (true);
|
|
mus_playing.name = "";
|
|
mus_playing.LastSong = "";
|
|
return true;
|
|
}
|
|
|
|
if (!mus_playing.name.IsEmpty() &&
|
|
mus_playing.handle != nullptr &&
|
|
stricmp(mus_playing.name, musicname) == 0 &&
|
|
ZMusic_IsLooping(mus_playing.handle) == zmusic_bool(looping))
|
|
{
|
|
if (order != mus_playing.baseorder)
|
|
{
|
|
if (ZMusic_SetSubsong(mus_playing.handle, order))
|
|
{
|
|
mus_playing.baseorder = order;
|
|
}
|
|
}
|
|
else if (!ZMusic_IsPlaying(mus_playing.handle))
|
|
{
|
|
if (!ZMusic_Start(mus_playing.handle, order, looping))
|
|
{
|
|
Printf("Unable to start %s: %s\n", mus_playing.name.GetChars(), ZMusic_GetLastError());
|
|
}
|
|
S_CreateStream();
|
|
|
|
}
|
|
return true;
|
|
}
|
|
|
|
int lumpnum = -1;
|
|
int length = 0;
|
|
ZMusic_MusicStream handle = nullptr;
|
|
MidiDeviceSetting* devp = MidiDevices.CheckKey(musicname);
|
|
|
|
// Strip off any leading file:// component.
|
|
if (strncmp(musicname, "file://", 7) == 0)
|
|
{
|
|
musicname += 7;
|
|
}
|
|
|
|
// 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;
|
|
|
|
// shutdown old music
|
|
S_StopMusic(true);
|
|
|
|
// Just record it if volume is 0 or music was disabled
|
|
if (snd_musicvolume <= 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
|
|
{
|
|
|
|
CheckReplayGain(musicname, devp ? (EMidiDevice)devp->device : MDEV_DEFAULT, devp ? devp->args.GetChars() : "");
|
|
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() : "");
|
|
if (mus_playing.handle == nullptr)
|
|
{
|
|
Printf("Unable to load %s: %s\n", mus_playing.name.GetChars(), ZMusic_GetLastError());
|
|
}
|
|
}
|
|
|
|
mus_playing.loop = looping;
|
|
mus_playing.name = musicname;
|
|
mus_playing.baseorder = 0;
|
|
mus_playing.LastSong = "";
|
|
|
|
if (mus_playing.handle != 0)
|
|
{ // play it
|
|
auto volp = MusicVolumes.CheckKey(musicname);
|
|
float vol = volp ? *volp : 1.f;
|
|
if (!S_StartMusicPlaying(mus_playing.handle, looping, vol, order))
|
|
{
|
|
Printf("Unable to start %s: %s\n", mus_playing.name.GetChars(), ZMusic_GetLastError());
|
|
return false;
|
|
}
|
|
S_CreateStream();
|
|
mus_playing.baseorder = order;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
// S_RestartMusic
|
|
//
|
|
//==========================================================================
|
|
|
|
void S_RestartMusic ()
|
|
{
|
|
if (snd_musicvolume <= 0) return;
|
|
if (!mus_playing.LastSong.IsEmpty() && mus_enabled)
|
|
{
|
|
FString song = mus_playing.LastSong;
|
|
mus_playing.LastSong = "";
|
|
S_ChangeMusic (song, mus_playing.baseorder, mus_playing.loop, true);
|
|
}
|
|
else
|
|
{
|
|
S_StopMusic(true);
|
|
}
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
// S_MIDIDeviceChanged
|
|
//
|
|
//==========================================================================
|
|
|
|
|
|
void S_MIDIDeviceChanged(int newdev)
|
|
{
|
|
auto 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 ((force || PlayList.GetNumSongs() == 0) && !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 (!nomusic)
|
|
{
|
|
if (argv.argc() > 1)
|
|
{
|
|
PlayList.Clear();
|
|
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)
|
|
{
|
|
PlayList.Clear();
|
|
S_StopMusic (false);
|
|
mus_playing.LastSong = ""; // forget the last played song so that it won't get restarted if some volume changes occur
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
// CCMD playlist
|
|
//
|
|
//==========================================================================
|
|
|
|
UNSAFE_CCMD (playlist)
|
|
{
|
|
int argc = argv.argc();
|
|
|
|
if (argc < 2 || argc > 3)
|
|
{
|
|
Printf ("playlist <playlist.m3u> [<position>|shuffle]\n");
|
|
}
|
|
else
|
|
{
|
|
if (!PlayList.ChangeList(argv[1]))
|
|
{
|
|
Printf("Could not open " TEXTCOLOR_BOLD "%s" TEXTCOLOR_NORMAL ": %s\n", argv[1], strerror(errno));
|
|
return;
|
|
}
|
|
if (PlayList.GetNumSongs () > 0)
|
|
{
|
|
if (argc == 3)
|
|
{
|
|
if (stricmp (argv[2], "shuffle") == 0)
|
|
{
|
|
PlayList.Shuffle ();
|
|
}
|
|
else
|
|
{
|
|
PlayList.SetPosition (atoi (argv[2]));
|
|
}
|
|
}
|
|
S_ActivatePlayList (false);
|
|
}
|
|
}
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
// CCMD playlistpos
|
|
//
|
|
//==========================================================================
|
|
|
|
static bool CheckForPlaylist ()
|
|
{
|
|
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");
|
|
}
|
|
}
|