Skip to content

Commit

Permalink
- implemented replay gain calculation and management.
Browse files Browse the repository at this point in the history
This is done entirely on the streamed sound data, unlike the old relative volume which uses the backend's volume setting.
  • Loading branch information
coelckers committed Mar 10, 2021
1 parent ba618d3 commit f117806
Show file tree
Hide file tree
Showing 11 changed files with 861 additions and 26 deletions.
4 changes: 3 additions & 1 deletion bin/windows/zmusic/include/zmusic.h
Expand Up @@ -29,7 +29,8 @@ typedef enum EMIDIType_
MIDI_MIDI,
MIDI_HMI,
MIDI_XMI,
MIDI_MUS
MIDI_MUS,
MIDI_MIDS
} EMIDIType;

typedef enum EMidiDevice_
Expand Down Expand Up @@ -313,6 +314,7 @@ extern "C"
DLL_IMPORT void ZMusic_Close(ZMusic_MusicStream song);
DLL_IMPORT zmusic_bool ZMusic_SetSubsong(ZMusic_MusicStream song, int subsong);
DLL_IMPORT zmusic_bool ZMusic_IsLooping(ZMusic_MusicStream song);
DLL_IMPORT int ZMusic_GetDeviceType(ZMusic_MusicStream song);
DLL_IMPORT zmusic_bool ZMusic_IsMIDI(ZMusic_MusicStream song);
DLL_IMPORT void ZMusic_VolumeChanged(ZMusic_MusicStream song);
DLL_IMPORT zmusic_bool ZMusic_WriteSMF(ZMusic_MidiSource source, const char* fn, int looplimit);
Expand Down
2 changes: 1 addition & 1 deletion src/CMakeLists.txt
Expand Up @@ -992,6 +992,7 @@ set (PCH_SOURCES
common/2d/v_2ddrawer.cpp
common/2d/v_drawtext.cpp
common/2d/v_draw.cpp
common/thirdparty/gain_analysis.cpp
common/thirdparty/sfmt/SFMT.cpp
common/fonts/singlelumpfont.cpp
common/fonts/singlepicfont.cpp
Expand Down Expand Up @@ -1190,7 +1191,6 @@ add_executable( zdoom WIN32 MACOSX_BUNDLE
${PCH_SOURCES}
common/utility/x86.cpp
common/thirdparty/strnatcmp.c
common/thirdparty/gain_analysis.c
common/utility/zstring.cpp
common/utility/findfile.cpp
common/thirdparty/math/asin.c
Expand Down
16 changes: 0 additions & 16 deletions src/common/audio/music/i_music.cpp
Expand Up @@ -270,22 +270,6 @@ void I_SetMusicVolume (double factor)
I_SetRelativeVolume((float)factor);
}

//==========================================================================
//
// test a relative music volume
//
//==========================================================================

CCMD(testmusicvol)
{
if (argv.argc() > 1)
{
I_SetRelativeVolume((float)strtod(argv[1], nullptr));
}
else
Printf("Current relative volume is %1.2f\n", relative_volume);
}

//==========================================================================
//
// STAT music
Expand Down
10 changes: 10 additions & 0 deletions src/common/audio/music/i_music.h
Expand Up @@ -56,4 +56,14 @@ EXTERN_CVAR(Bool, mus_enabled)
EXTERN_CVAR(Float, snd_musicvolume)


inline float AmplitudeTodB(float amplitude)
{
return 20.0f * log10(amplitude);
}

inline float dBToAmplitude(float dB)
{
return pow(10.0f, dB / 20.0f);
}

#endif //__I_MUSIC_H__
252 changes: 249 additions & 3 deletions src/common/audio/music/music.cpp
Expand Up @@ -49,6 +49,10 @@
#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 --------------------------------------------

Expand Down Expand Up @@ -78,6 +82,14 @@ static MusicCallbacks mus_cb = { nullptr, DefaultOpenMusic };

// PUBLIC DATA DEFINITIONS -------------------------------------------------

CVAR(Bool, mus_calcgain, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) // changing this will only take effect for the next song.
CVAR(Bool, mus_usereplaygain, true, 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)
Expand Down Expand Up @@ -115,9 +127,33 @@ void S_StopCustomStream(SoundStream *stream)
}


static TArray<int16_t> convert;
static bool FillStream(SoundStream* stream, void* buff, int len, void* userdata)
{
bool written = ZMusic_FillStream(mus_playing.handle, buff, len);
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)
{
Expand All @@ -133,9 +169,12 @@ 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 = fmt.mNumChannels < 0 ? 0 : SoundStream::Float;
int flags = SoundStream::Float;
if (abs(fmt.mNumChannels) < 2) flags |= SoundStream::Mono;

musicStream.reset(GSnd->CreateStream(FillStream, fmt.mBufferSize, flags, fmt.mSampleRate, nullptr));
Expand Down Expand Up @@ -167,7 +206,7 @@ void S_StopStream()

static bool S_StartMusicPlaying(ZMusic_MusicStream song, bool loop, float rel_vol, int subsong)
{
if (rel_vol > 0.f)
if (rel_vol > 0.f && !mus_usereplaygain)
{
float factor = relative_volume / saved_relative_volume;
saved_relative_volume = rel_vol;
Expand Down Expand Up @@ -312,6 +351,211 @@ bool S_StartMusic (const char *m_id)
// initiates playback of a song
//
//==========================================================================
static TMap<FString, float> gainMap;

static FString ReplayGainHash(ZMusicCustomReader* reader, int flength, int playertype, const char* 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);
if (playertype == -1)
{
// todo: get the defaults for MIDI synth and used sound font.
}
return FStringf("%d:%s:%d:%s", flength, digestout, playertype, playparam);
}

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);
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);
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)
{
Expand Down Expand Up @@ -397,6 +641,8 @@ bool S_ChangeMusic(const char* musicname, int order, bool looping, bool force)
}
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)
Expand Down
5 changes: 5 additions & 0 deletions src/common/audio/music/music_config.cpp
Expand Up @@ -453,6 +453,11 @@ CUSTOM_CVAR(Int, snd_streambuffersize, 64, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CV

CUSTOM_CVAR(Int, mod_samplerate, 0, CVAR_ARCHIVE | CVAR_GLOBALCONFIG | CVAR_VIRTUAL)
{
if (self != 0 && self != 11025 && self != 22050 && self != 44100 && self != 48000)
{
self = 0;
return;
}
FORWARD_CVAR(mod_samplerate);
}

Expand Down

0 comments on commit f117806

Please sign in to comment.