Browse Source

Add loudness normalization

Clément Wolf 3 tuần trước cách đây
mục cha
commit
c668ce1611

+ 2 - 2
Makefile

@@ -8,10 +8,10 @@ LIBS = blkid freetype2 glew libenet libjpeg liblzma OpenCL xi zlib
 CXXFLAGS = -std=c++17 -fmessage-length=0 -Wno-sign-compare -Wno-unused-local-typedefs -Wno-reorder -Wno-switch -Wall
 CXXFLAGS += `pkgconf --static --cflags $(LIBS)` `curl-config --cflags`
 CXXFLAGS += -Isrc/App -Isrc/App/Osu -Isrc/Engine -Isrc/GUI -Isrc/GUI/Windows -Isrc/GUI/Windows/VinylScratcher -Isrc/Engine/Input -Isrc/Engine/Platform -Isrc/Engine/Main -Isrc/Engine/Renderer -Isrc/Util
-CXXFLAGS += -Ilibraries/bassasio/include -Ilibraries/bassmix/include -Ilibraries/basswasapi/include
+CXXFLAGS += -Ilibraries/bassasio/include -Ilibraries/bassmix/include -Ilibraries/basswasapi/include -Ilibraries/bassloud/include
 CXXFLAGS += -g3
 
-LDFLAGS = -ldiscord-rpc -lbass -lbassmix -lbass_fx -lpthread -lstdc++
+LDFLAGS = -ldiscord-rpc -lbass -lbassmix -lbass_fx -lbassloud -lpthread -lstdc++
 LDFLAGS += `pkgconf --static --libs $(LIBS)` `curl-config --static-libs --libs`
 
 

+ 1 - 0
README.md

@@ -19,6 +19,7 @@ Required dependencies:
 - [bass](https://www.un4seen.com/download.php?bass24-linux)
 - [bassmix](https://www.un4seen.com/download.php?bassmix24-linux)
 - [bass_fx](https://www.un4seen.com/download.php?z/0/bass_fx24-linux)
+- [BASSloud](https://www.un4seen.com/download.php?bassloud24-linux)
 - blkid
 - freetype2
 - glew

+ 1 - 1
build.bat

@@ -13,7 +13,7 @@ if %errorlevel% equ 0 (
 set CXXFLAGS=-std=c++17 -Wall -fmessage-length=0 -Wno-sign-compare -Wno-unused-local-typedefs -Wno-reorder -Wno-switch -IC:\mingw32\include
 set CXXFLAGS=%CXXFLAGS% -D__GXX_EXPERIMENTAL_CXX0X__
 set CXXFLAGS=%CXXFLAGS% -Isrc/App -Isrc/App/Osu -Isrc/Engine -Isrc/GUI -Isrc/GUI/Windows -Isrc/GUI/Windows/VinylScratcher -Isrc/Engine/Input -Isrc/Engine/Platform -Isrc/Engine/Main -Isrc/Engine/Renderer -Isrc/Util
-set LDFLAGS=-logg -lADLMIDI -lmad -lmodplug -lsmpeg -lgme -lvorbis -lopus -lvorbisfile -ldiscord-rpc -lSDL2_mixer_ext.dll -lSDL2 -ld3dcompiler_47 -ld3d11 -ldxgi -lcurl -llibxinput9_1_0 -lfreetype -lopengl32 -lOpenCL -lvulkan-1 -lglew32 -lglu32 -lgdi32 -lbass -lbassasio -lbass_fx -lbassmix -lbasswasapi -lcomctl32 -lDwmapi -lComdlg32 -lpsapi -lws2_32 -lwinmm -lpthread -llibjpeg -lwbemuuid -lole32 -loleaut32 -llzma
+set LDFLAGS=-logg -lADLMIDI -lmad -lmodplug -lsmpeg -lgme -lvorbis -lopus -lvorbisfile -ldiscord-rpc -lSDL2_mixer_ext.dll -lSDL2 -ld3dcompiler_47 -ld3d11 -ldxgi -lcurl -llibxinput9_1_0 -lfreetype -lopengl32 -lOpenCL -lvulkan-1 -lglew32 -lglu32 -lgdi32 -lbass -lbassasio -lbass_fx -lbassmix -lbasswasapi -lbassloud -lcomctl32 -lDwmapi -lComdlg32 -lpsapi -lws2_32 -lwinmm -lpthread -llibjpeg -lwbemuuid -lole32 -loleaut32 -llzma
 
 set CXXFLAGS=%CXXFLAGS% -g3
 rem set CXXFLAGS=%CXXFLAGS% -O3

+ 0 - 0
libraries/bassloud/2.4.txt


BIN
libraries/bassloud/bin/bassloud.dll


+ 48 - 0
libraries/bassloud/include/bassloud.h

@@ -0,0 +1,48 @@
+/*
+	BASSloud 2.4 C/C++ header file
+	Copyright (c) 2023 Un4seen Developments Ltd.
+
+	See the BASSLOUD.CHM file for more detailed documentation
+*/
+
+#ifndef BASSLOUDNESS_H
+#define BASSLOUDNESS_H
+
+#include "bass.h"
+
+#if BASSVERSION!=0x204
+#error conflicting BASS and BASSLOUDNESS versions
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#ifndef BASSLOUDDEF
+#define BASSLOUDDEF(f) WINAPI f
+#endif
+
+typedef DWORD HLOUDNESS;		// loudness handle
+	
+// BASS_Loudness_Start flags / BASS_Loudness_GetLevel modes
+#define BASS_LOUDNESS_CURRENT		0
+#define BASS_LOUDNESS_INTEGRATED	1
+#define BASS_LOUDNESS_RANGE			2
+#define BASS_LOUDNESS_PEAK			4
+#define BASS_LOUDNESS_TRUEPEAK		8
+#define BASS_LOUDNESS_AUTOFREE		0x8000
+
+DWORD BASSLOUDDEF(BASS_Loudness_GetVersion)(void);
+
+HLOUDNESS BASSLOUDDEF(BASS_Loudness_Start)(DWORD handle, DWORD flags, int priority);
+BOOL BASSLOUDDEF(BASS_Loudness_Stop)(DWORD handle);
+BOOL BASSLOUDDEF(BASS_Loudness_SetChannel)(HLOUDNESS handle, DWORD channel, int priority);
+DWORD BASSLOUDDEF(BASS_Loudness_GetChannel)(HLOUDNESS handle);
+BOOL BASSLOUDDEF(BASS_Loudness_GetLevel)(HLOUDNESS handle, DWORD mode, float *level);
+BOOL BASSLOUDDEF(BASS_Loudness_GetLevelMulti)(HLOUDNESS *handles, DWORD count, DWORD mode, float *level);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif

BIN
libraries/bassloud/lib/windows/bassloud.lib


+ 1 - 0
src/App/Osu/Osu.cpp

@@ -144,6 +144,7 @@ ConVar start_first_main_menu_song_at_preview_point("start_first_main_menu_song_a
 ConVar nightcore_enjoyer("nightcore_enjoyer", false, FCVAR_NONE, "automatically select nightcore when speed modifying");
 ConVar scoreboard_animations("scoreboard_animations", true, FCVAR_NONE, "animate in-game scoreboard");
 ConVar instant_replay_duration("instant_replay_duration", 15.f, FCVAR_NONE, "instant replay (F2) duration, in seconds");
+ConVar normalize_loudness("normalize_loudness", false, FCVAR_NONE, "normalize loudness across songs");
 
 ConVar mp_server("mp_server", "ez-pp.farm", FCVAR_NONE);
 ConVar mp_password("mp_password", "", FCVAR_NONE);

+ 72 - 6
src/App/Osu/OsuBeatmap.cpp

@@ -1111,8 +1111,74 @@ void OsuBeatmap::cancelFailing() {
     if(getSkin()->getFailsound()->isPlaying()) engine->getSound()->stop(getSkin()->getFailsound());
 }
 
-void OsuBeatmap::setVolume(float volume) {
-    if(m_music != NULL) m_music->setVolume(volume);
+float OsuBeatmap::getIdealVolume() {
+    if(m_music == nullptr) return 1.f;
+
+    float volume = m_osu_volume_music_ref->getFloat();
+    if(!convar->getConVarByName("normalize_loudness")->getBool()) {
+        return volume;
+    }
+
+    static std::string last_song = "";
+    static float modifier = 1.f;
+
+    if(m_selectedDifficulty2->getFullSoundFilePath() == last_song) {
+        // We already did the calculation
+        return volume * modifier;
+    }
+
+    auto device_id = BASS_GetDevice();
+    BASS_SetDevice(0);
+
+    auto decoder = BASS_StreamCreateFile(false, m_selectedDifficulty2->getFullSoundFilePath().c_str(), 0, 0,
+                                         BASS_STREAM_DECODE | BASS_SAMPLE_FLOAT);
+    if(!decoder) {
+        debugLog("BASS_StreamCreateFile() returned error %d on file %s\n", BASS_ErrorGetCode(),
+                 m_selectedDifficulty2->getFullSoundFilePath().c_str());
+        BASS_SetDevice(device_id);
+        return volume;
+    }
+
+    float preview_point = m_selectedDifficulty2->getPreviewTime();
+    if(preview_point < 0) preview_point = 0;
+    const QWORD position = BASS_ChannelSeconds2Bytes(decoder, preview_point / 1000.0);
+    if(!BASS_ChannelSetPosition(decoder, position, BASS_POS_BYTE)) {
+        debugLog("BASS_ChannelSetPosition() returned error %d\n", BASS_ErrorGetCode());
+        BASS_ChannelFree(decoder);
+        BASS_SetDevice(device_id);
+        return volume;
+    }
+
+    auto loudness = BASS_Loudness_Start(decoder, MAKELONG(BASS_LOUDNESS_INTEGRATED, 3000), 0);
+    if(!loudness) {
+        debugLog("BASS_Loudness_Start() returned error %d\n", BASS_ErrorGetCode());
+        BASS_ChannelFree(decoder);
+        BASS_SetDevice(device_id);
+        return volume;
+    }
+
+    // Process 4 seconds of audio, should be enough for the loudness measurement
+    float buf[44100 * 4];
+    BASS_ChannelGetData(decoder, buf, sizeof(buf));
+
+    float level = -13.f;
+    BASS_Loudness_GetLevel(loudness, BASS_LOUDNESS_INTEGRATED, &level);
+    if(level == -HUGE_VAL) {
+        debugLog("No loudness information available (silent song?)\n");
+    }
+
+    BASS_Loudness_Stop(loudness);
+    BASS_ChannelFree(decoder);
+    BASS_SetDevice(device_id);
+
+    last_song = m_selectedDifficulty2->getFullSoundFilePath();
+    modifier = (level / -16.f);
+
+    if(Osu::debug->getBool()) {
+        debugLog("Volume set to %.2fx for this song\n", modifier);
+    }
+
+    return volume * modifier;
 }
 
 void OsuBeatmap::setSpeed(float speed) {
@@ -1126,7 +1192,7 @@ void OsuBeatmap::seekPercent(double percent) {
     m_fWaitTime = 0.0f;
 
     m_music->setPosition(percent);
-    m_music->setVolume(m_osu_volume_music_ref->getFloat());
+    m_music->setVolume(getIdealVolume());
     m_music->setSpeed(m_osu->getSpeedMultiplier());
 
     resetHitObjects(m_music->getPositionMS());
@@ -1628,7 +1694,7 @@ void OsuBeatmap::handlePreviewPlay() {
                                            : m_selectedDifficulty2->getPreviewTime());
             }
 
-            m_music->setVolume(m_osu_volume_music_ref->getFloat());
+            m_music->setVolume(getIdealVolume());
             m_music->setSpeed(m_osu->getSpeedMultiplier());
         }
     }
@@ -1659,7 +1725,7 @@ void OsuBeatmap::loadMusic(bool stream, bool prescan) {
             m_selectedDifficulty2->getFullSoundFilePath(), "OSU_BEATMAP_MUSIC", stream, false, false, false,
             m_bForceStreamPlayback &&
                 prescan);  // m_bForceStreamPlayback = prescan necessary! otherwise big mp3s will go out of sync
-        m_music->setVolume(m_osu_volume_music_ref->getFloat());
+        m_music->setVolume(getIdealVolume());
         m_fMusicFrequencyBackup = m_music->getFrequency();
         m_music->setSpeed(m_osu->getSpeedMultiplier());
     }
@@ -2394,7 +2460,7 @@ void OsuBeatmap::update2() {
 
                     engine->getSound()->play(m_music);
                     m_music->setPositionMS(0);
-                    m_music->setVolume(m_osu_volume_music_ref->getFloat());
+                    m_music->setVolume(getIdealVolume());
                     m_music->setSpeed(m_osu->getSpeedMultiplier());
 
                     // if we are quick restarting, jump just before the first hitobject (even if there is a long waiting

+ 1 - 1
src/App/Osu/OsuBeatmap.h

@@ -129,7 +129,7 @@ class OsuBeatmap {
     // music/sound
     void loadMusic(bool stream = true, bool prescan = false);
     void unloadMusic();
-    void setVolume(float volume);
+    float getIdealVolume();
     void setSpeed(float speed);
     void seekPercent(double percent);
     void seekPercentPlayable(double percent);

+ 1 - 0
src/App/Osu/OsuChangelog.cpp

@@ -37,6 +37,7 @@ OsuChangelog::OsuChangelog(Osu *osu) : OsuScreenBackable(osu) {
     latest.title =
         UString::format("%.2f (%s, %s)", convar->getConVarByName("osu_version")->getFloat(), __DATE__, __TIME__);
     latest.changes.push_back("- Renamed 'McOsu Multiplayer' to 'neosu'");
+    latest.changes.push_back("- Added option to normalize loudness across songs");
     latest.changes.push_back("- Added server logo to main menu button");
     latest.changes.push_back("- Added instant_replay_duration convar");
     latest.changes.push_back("- Allowed singleplayer cheats when the server doesn't accept score submissions");

+ 49 - 0
src/App/Osu/OsuDatabaseBeatmap.cpp

@@ -1888,3 +1888,52 @@ void OsuDatabaseBeatmapStarCalculator::setBeatmapDifficulty(OsuDatabaseBeatmap *
     m_bRelax = relax;
     m_bTouchDevice = touchDevice;
 }
+
+std::string OsuDatabaseBeatmap::getFullSoundFilePath() {
+    // On linux, paths are case sensitive, so we retry different variations
+    if(env->getOS() != Environment::OS::OS_LINUX || env->fileExists(m_sFullSoundFilePath)) {
+        return m_sFullSoundFilePath;
+    }
+
+    // try uppercasing file extension
+    for(int s = m_sFullSoundFilePath.size(); s >= 0; s--) {
+        if(m_sFullSoundFilePath[s] == '.') {
+            for(int i = s + 1; i < m_sFullSoundFilePath.size(); i++) {
+                m_sFullSoundFilePath[i] = std::toupper(m_sFullSoundFilePath[i]);
+            }
+            break;
+        }
+    }
+    if(env->fileExists(m_sFullSoundFilePath)) {
+        return m_sFullSoundFilePath;
+    }
+
+    // try lowercasing filename, uppercasing file extension
+    bool foundFilenameStart = false;
+    for(int s = m_sFullSoundFilePath.size(); s >= 0; s--) {
+        if(foundFilenameStart) {
+            if(m_sFullSoundFilePath[s] == '/') break;
+            m_sFullSoundFilePath[s] = std::tolower(m_sFullSoundFilePath[s]);
+        }
+        if(m_sFullSoundFilePath[s] == '.') {
+            foundFilenameStart = true;
+        }
+    }
+    if(env->fileExists(m_sFullSoundFilePath)) {
+        return m_sFullSoundFilePath;
+    }
+
+    // try lowercasing everything
+    for(int s = m_sFullSoundFilePath.size(); s >= 0; s--) {
+        if(m_sFullSoundFilePath[s] == '/') {
+            break;
+        }
+        m_sFullSoundFilePath[s] = std::tolower(m_sFullSoundFilePath[s]);
+    }
+    if(env->fileExists(m_sFullSoundFilePath)) {
+        return m_sFullSoundFilePath;
+    }
+
+    // give up
+    return m_sFullSoundFilePath;
+}

+ 2 - 2
src/App/Osu/OsuDatabaseBeatmap.h

@@ -167,9 +167,9 @@ class OsuDatabaseBeatmap {
 
     inline const std::vector<TIMINGPOINT> &getTimingpoints() const { return m_timingpoints; }
 
-    // redundant data
+    std::string getFullSoundFilePath();
 
-    inline const std::string &getFullSoundFilePath() const { return m_sFullSoundFilePath; }
+    // redundant data
     inline const std::string &getFullBackgroundImageFilePath() const { return m_sFullBackgroundImageFilePath; }
 
     // precomputed data

+ 13 - 0
src/App/Osu/OsuOptionsMenu.cpp

@@ -793,6 +793,10 @@ OsuOptionsMenu::OsuOptionsMenu(Osu *osu) : OsuScreenBackable(osu) {
     }
 
     addSubSection("Volume");
+
+    addCheckbox("Normalize loudness across songs", convar->getConVarByName("normalize_loudness"))
+        ->setChangeCallback(fastdelegate::MakeDelegate(this, &OsuOptionsMenu::onLoudnessNormalizationToggle));
+
     CBaseUISlider *masterVolumeSlider =
         addSlider("Master:", 0.0f, 1.0f, convar->getConVarByName("osu_volume_master"), 70.0f);
     masterVolumeSlider->setChangeCallback(fastdelegate::MakeDelegate(this, &OsuOptionsMenu::onSliderChangePercent));
@@ -3128,6 +3132,15 @@ void OsuOptionsMenu::onWASAPIPeriodChange(CBaseUISlider *slider) {
     }
 }
 
+void OsuOptionsMenu::onLoudnessNormalizationToggle(CBaseUICheckbox *checkbox) {
+    onCheckboxChange(checkbox);
+
+    auto music = m_osu->getSelectedBeatmap()->getMusic();
+    if(music != nullptr) {
+        music->setVolume(m_osu->getSelectedBeatmap()->getIdealVolume());
+    }
+}
+
 void OsuOptionsMenu::onUseSkinsSoundSamplesChange(UString oldValue, UString newValue) { m_osu->reloadSkin(); }
 
 void OsuOptionsMenu::onHighQualitySlidersCheckboxChange(CBaseUICheckbox *checkbox) {

+ 1 - 0
src/App/Osu/OsuOptionsMenu.h

@@ -167,6 +167,7 @@ class OsuOptionsMenu : public OsuScreenBackable, public OsuNotificationOverlayKe
     void onASIOBufferChange(CBaseUISlider *slider);
     void onWASAPIBufferChange(CBaseUISlider *slider);
     void onWASAPIPeriodChange(CBaseUISlider *slider);
+    void onLoudnessNormalizationToggle(CBaseUICheckbox *checkbox);
 
     void onUseSkinsSoundSamplesChange(UString oldValue, UString newValue);
     void onHighQualitySlidersCheckboxChange(CBaseUICheckbox *checkbox);

+ 4 - 1
src/App/Osu/OsuVolumeOverlay.cpp

@@ -325,5 +325,8 @@ void OsuVolumeOverlay::onEffectVolumeChange() {
 }
 
 void OsuVolumeOverlay::onMusicVolumeChange(UString oldValue, UString newValue) {
-    m_osu->getSelectedBeatmap()->setVolume(newValue.toFloat());
+    auto music = m_osu->getSelectedBeatmap()->getMusic();
+    if(music != nullptr) {
+        music->setVolume(m_osu->getSelectedBeatmap()->getIdealVolume());
+    }
 }

+ 1 - 1
src/Engine/Sound.cpp

@@ -235,7 +235,7 @@ void Sound::setPositionMS(unsigned long ms) {
 void Sound::setVolume(float volume) {
     if(!m_bReady) return;
 
-    m_fVolume = clamp<float>(volume, 0.0f, 1.0f);
+    m_fVolume = clamp<float>(volume, 0.0f, 2.0f);
 
     for(auto channel : getActiveChannels()) {
         BASS_ChannelSetAttribute(channel, BASS_ATTRIB_VOL, m_fVolume);

+ 1 - 0
src/Engine/Sound.h

@@ -4,6 +4,7 @@
 #include <bass.h>
 #include <bass_fx.h>
 #include <bassasio.h>
+#include <bassloud.h>
 #include <bassmix.h>
 #include <basswasapi.h>
 

+ 29 - 11
src/Engine/SoundEngine.cpp

@@ -127,6 +127,15 @@ SoundEngine::SoundEngine() {
         return;
     }
 
+    auto loud_version = BASS_Loudness_GetVersion();
+    debugLog("SoundEngine: BASSloud version = 0x%08x\n", loud_version);
+    if(HIWORD(loud_version) != BASSVERSION) {
+        engine->showMessageErrorFatal("Fatal Sound Error",
+                                      "An incorrect version of the BASSloud library file was loaded!");
+        engine->shutdown();
+        return;
+    }
+
 #ifdef _WIN32
     auto asio_version = BASS_ASIO_GetVersion();
     debugLog("SoundEngine: BASSASIO version = 0x%08x\n", asio_version);
@@ -148,7 +157,6 @@ SoundEngine::SoundEngine() {
 #endif
 
     BASS_SetConfig(BASS_CONFIG_BUFFER, 100);
-    BASS_SetConfig(BASS_CONFIG_NET_BUFFER, 500);
 
     // all beatmaps timed to non-iTunesSMPB + 529 sample deletion offsets on old dlls pre 2015
     BASS_SetConfig(BASS_CONFIG_MP3_OLDGAPS, 1);
@@ -325,6 +333,10 @@ bool SoundEngine::initializeOutputDevice(OUTPUT_DEVICE device) {
     debugLog("SoundEngine: initializeOutputDevice( %s ) ...\n", device.name.toUtf8());
 
     if(m_currentOutputDevice.driver == OutputDriver::BASS) {
+        BASS_SetDevice(0);
+        BASS_Free();
+        BASS_SetDevice(m_currentOutputDevice.id);
+
         BASS_Free();
     } else if(m_currentOutputDevice.driver == OutputDriver::BASS_ASIO) {
 #ifdef _WIN32
@@ -376,26 +388,32 @@ bool SoundEngine::initializeOutputDevice(OUTPUT_DEVICE device) {
             BASS_SetConfig(BASS_CONFIG_DEV_PERIOD, snd_dev_period.getInt());
     }
 
-    // ASIO and WASAPI: Initialize BASS on "No sound" device
+    const int freq = snd_freq.getInt();
+    HWND hwnd = NULL;
+#ifdef _WIN32
+    const WinEnvironment *winEnv = dynamic_cast<WinEnvironment *>(env);
+    hwnd = winEnv->getHwnd();
+#endif
+
     int bass_device_id = device.id;
     unsigned int runtimeFlags = BASS_DEVICE_STEREO | BASS_DEVICE_FREQ;
     if(device.driver == OutputDriver::BASS) {
-        runtimeFlags |= BASS_DEVICE_DSOUND;
+        // Regular BASS: we still want a "No sound" device to check for loudness
+        if(!BASS_Init(0, freq, runtimeFlags | BASS_DEVICE_NOSPEAKER, hwnd, NULL)) {
+            m_bReady = false;
+            engine->showMessageError("Sound Error", UString::format("BASS_Init(0) failed (%i)!", BASS_ErrorGetCode()));
+            return false;
+        }
     } else if(device.driver == OutputDriver::BASS_ASIO || device.driver == OutputDriver::BASS_WASAPI) {
+        // ASIO and WASAPI: Initialize BASS on "No sound" device
         runtimeFlags |= BASS_DEVICE_NOSPEAKER;
         bass_device_id = 0;
     }
 
-    const int freq = snd_freq.getInt();
-    HWND hwnd = NULL;
-#ifdef _WIN32
-    const WinEnvironment *winEnv = dynamic_cast<WinEnvironment *>(env);
-    hwnd = winEnv->getHwnd();
-#endif
-
     if(!BASS_Init(bass_device_id, freq, runtimeFlags, hwnd, NULL)) {
         m_bReady = false;
-        engine->showMessageError("Sound Error", UString::format("BASS_Init() failed (%i)!", BASS_ErrorGetCode()));
+        engine->showMessageError("Sound Error",
+                                 UString::format("BASS_Init(%d) failed (%i)!", bass_device_id, BASS_ErrorGetCode()));
         return false;
     }