From 8dbeded3590b2149606f36e12380b1c06e8ac5c6 Mon Sep 17 00:00:00 2001 From: Roman Fomin Date: Tue, 12 Nov 2024 07:31:42 +0700 Subject: [PATCH] implement loudness normalization using libebur128 (#1997) * rename "MIDI Settings"->"Music Settings", add "Auto Gain" menu option * Drop native midi menu items --------- Co-authored-by: ceski <56656010+ceski-1@users.noreply.github.com> --- .github/workflows/main.yml | 4 + CMakeLists.txt | 1 + cmake/Findlibebur128.cmake | 30 +++++++ src/CMakeLists.txt | 1 + src/doomdef.h | 2 +- src/i_flmusic.c | 4 +- src/i_midimusic.c | 8 +- src/i_oalmusic.c | 178 ++++++++++++++++++++++++++++++++++--- src/i_oplmusic.c | 4 +- src/i_sound.h | 2 + src/m_config.h | 8 +- src/mn_setup.c | 79 ++++++++-------- vcpkg.json | 1 + 13 files changed, 251 insertions(+), 71 deletions(-) create mode 100644 cmake/Findlibebur128.cmake diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e724a986..92bc3308 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -49,6 +49,7 @@ jobs: libsdl2-dev \ libsdl2-net-dev \ libopenal-dev \ + libebur128-dev \ libsndfile1-dev \ libfluidsynth-dev \ libxmp-dev \ @@ -62,6 +63,7 @@ jobs: sdl2 \ sdl2_net \ openal-soft \ + libebur128 \ libsndfile \ fluid-synth \ libxmp \ @@ -82,6 +84,7 @@ jobs: ${{ matrix.config.msys-env }}-SDL2 ${{ matrix.config.msys-env }}-SDL2_net ${{ matrix.config.msys-env }}-openal + ${{ matrix.config.msys-env }}-libebur128 ${{ matrix.config.msys-env }}-libsndfile ${{ matrix.config.msys-env }}-fluidsynth ${{ matrix.config.msys-env }}-libxmp @@ -141,6 +144,7 @@ jobs: libsdl2-dev \ libsdl2-net-dev \ libopenal-dev \ + libebur128-dev \ libsndfile1-dev \ libfluidsynth-dev \ libxmp-dev \ diff --git a/CMakeLists.txt b/CMakeLists.txt index 547a35a3..7f592fbd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -85,6 +85,7 @@ set(CMAKE_FIND_FRAMEWORK NEVER) find_package(SDL2 2.0.18 REQUIRED) find_package(SDL2_net REQUIRED) find_package(OpenAL REQUIRED) +find_package(libebur128 REQUIRED) find_package(SndFile 1.0.29 REQUIRED) if(CMAKE_SYSTEM_NAME STREQUAL "Linux") find_package(ALSA REQUIRED) diff --git a/cmake/Findlibebur128.cmake b/cmake/Findlibebur128.cmake new file mode 100644 index 00000000..a4f000e6 --- /dev/null +++ b/cmake/Findlibebur128.cmake @@ -0,0 +1,30 @@ +# Variables defined: +# libebur128_FOUND +# libebur128_INCLUDE_DIR +# libebur128_LIBRARY + +find_package(PkgConfig) +pkg_check_modules(PC_libebur128 libebur128) + +find_library(libebur128_LIBRARY + NAMES ebur128 + HINTS "${PC_libebur128_LIBDIR}") + +find_path(libebur128_INCLUDE_DIR + NAMES ebur128.h + HINTS "${PC_libebur128_INCLUDEDIR}") + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(libebur128 + REQUIRED_VARS libebur128_LIBRARY libebur128_INCLUDE_DIR) + +if(libebur128_FOUND) + if(NOT TARGET libebur128::libebur128) + add_library(libebur128::libebur128 UNKNOWN IMPORTED) + set_target_properties(libebur128::libebur128 PROPERTIES + IMPORTED_LOCATION "${libebur128_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${libebur128_INCLUDE_DIR}") + endif() +endif() + +mark_as_advanced(libebur128_LIBRARY libebur128_INCLUDE_DIR) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 364784e9..d244267a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -188,6 +188,7 @@ target_link_libraries(woof PRIVATE ${SDL2_LIBRARIES} SDL2_net::SDL2_net OpenAL::OpenAL + libebur128::libebur128 SndFile::sndfile opl pffft diff --git a/src/doomdef.h b/src/doomdef.h index 0be4712d..2fb975c8 100644 --- a/src/doomdef.h +++ b/src/doomdef.h @@ -225,7 +225,7 @@ typedef enum { ss_gen, // killough 10/98 ss_comp, // killough 10/98 ss_sfx, - ss_midi, + ss_music, ss_eq, ss_padadv, ss_gyro, diff --git a/src/i_flmusic.c b/src/i_flmusic.c index f474559b..a7c26501 100644 --- a/src/i_flmusic.c +++ b/src/i_flmusic.c @@ -512,9 +512,9 @@ static void I_FL_BindVariables(void) "[FluidSynth] Number of voices that can be played in parallel"); BIND_BOOL(fl_interpolation, false, "[FluidSynth] Interpolation method (0 = Default; 1 = Highest Quality)"); - BIND_BOOL_MIDI(fl_reverb, false, + BIND_BOOL_MUSIC(fl_reverb, false, "[FluidSynth] Enable reverb effects"); - BIND_BOOL_MIDI(fl_chorus, false, + BIND_BOOL_MUSIC(fl_chorus, false, "[FluidSynth] Enable chorus effects"); BIND_NUM(fl_reverb_damp, 30, 0, 100, "[FluidSynth] Reverb damping"); diff --git a/src/i_midimusic.c b/src/i_midimusic.c index 252f6fdf..48971d69 100644 --- a/src/i_midimusic.c +++ b/src/i_midimusic.c @@ -1500,15 +1500,15 @@ static const char *I_MID_MusicFormat(void) static void I_MID_BindVariables(void) { - BIND_NUM_MIDI(midi_complevel, COMP_STANDARD, 0, COMP_NUM - 1, + BIND_NUM(midi_complevel, COMP_STANDARD, 0, COMP_NUM - 1, "[Native MIDI] Compatibility level (0 = Vanilla; 1 = Standard; 2 = Full)"); - BIND_NUM_MIDI(midi_reset_type, RESET_TYPE_GM, 0, RESET_NUM - 1, + BIND_NUM(midi_reset_type, RESET_TYPE_GM, 0, RESET_NUM - 1, "[Native MIDI] Reset type (0 = No SysEx; 1 = GM; 2 = GS; 3 = XG)"); BIND_NUM(midi_reset_delay, -1, -1, 2000, "[Native MIDI] Delay after reset (-1 = Auto; 0 = None; 1-2000 = Milliseconds)"); - BIND_BOOL_MIDI(midi_ctf, true, + BIND_BOOL(midi_ctf, true, "[Native MIDI] Fix invalid instruments by emulating SC-55 capital tone fallback"); - BIND_NUM_MIDI(midi_gain, 0, -20, 0, "[Native MIDI] Gain [dB]"); + BIND_NUM(midi_gain, 0, -20, 0, "[Native MIDI] Gain [dB]"); } music_module_t music_mid_module = diff --git a/src/i_oalmusic.c b/src/i_oalmusic.c index 7b285bf9..a9d538f5 100644 --- a/src/i_oalmusic.c +++ b/src/i_oalmusic.c @@ -18,6 +18,7 @@ #include "al.h" #include "alc.h" #include "alext.h" +#include "ebur128.h" #include @@ -74,10 +75,15 @@ typedef struct byte *data; boolean looping; + ALfloat gain; + ALfloat auto_gain; + // The format of the output stream ALenum format; ALsizei freq; ALsizei frame_size; + int channels; + int total_frames; } stream_player_t; static stream_player_t player; @@ -87,6 +93,134 @@ static SDL_atomic_t player_thread_running; static boolean music_initialized; +static ebur128_state *ebur_state; +boolean auto_gain; + +static void ShutdownAutoGain(void) +{ + if (ebur_state) + { + ebur128_destroy(&ebur_state); + } +} + +static void InitAutoGain(void) +{ + if (player.format == AL_FORMAT_MONO16 || player.format == AL_FORMAT_MONO_FLOAT32) + { + player.channels = 1; + } + else + { + player.channels = 2; + } + + ebur_state = ebur128_init(player.channels, player.freq, EBUR128_MODE_S + | EBUR128_MODE_I | EBUR128_MODE_SAMPLE_PEAK | EBUR128_MODE_HISTOGRAM); + + player.auto_gain = 1.0f; + player.total_frames = 0; +} + +static void AutoGain(uint32_t frames) +{ + if (!auto_gain) + { + return; + } + + if (player.format == AL_FORMAT_MONO16 || player.format == AL_FORMAT_STEREO16) + { + ebur128_add_frames_short(ebur_state, (int16_t *)player.data, frames); + } + else + { + ebur128_add_frames_float(ebur_state, (float *)player.data, frames); + } + + player.total_frames += frames; + + const float target = -23.0f; // UFS + + boolean failed = false; + double momentary = 0.0; + double shortterm = 0.0; + double global = 0.0; + double relative = 0.0; + + if (EBUR128_SUCCESS != ebur128_loudness_momentary(ebur_state, &momentary)) + { + failed = true; + } + + if (EBUR128_SUCCESS != ebur128_loudness_shortterm(ebur_state, &shortterm)) + { + failed = true; + } + + if (EBUR128_SUCCESS != ebur128_loudness_global(ebur_state, &global)) + { + failed = true; + } + + if (EBUR128_SUCCESS != ebur128_relative_threshold(ebur_state, &relative)) + { + failed = true; + } + + if (player.total_frames < 3 * BUFFER_SAMPLES && !failed) + { + if (momentary > -70.0) + { + float diff = target - momentary; + player.auto_gain = DB_TO_GAIN(diff); + } + } + + if (relative > -70.0 && momentary > relative && !failed) + { + double peak_L = 0.0; + double peak_R = 0.0; + + if (EBUR128_SUCCESS != ebur128_prev_sample_peak(ebur_state, 0, &peak_L)) + { + failed = true; + } + + if (player.channels == 2) + { + if (EBUR128_SUCCESS + != ebur128_prev_sample_peak(ebur_state, 1, &peak_R)) + { + failed = true; + } + } + + if (!failed) + { + const float weight_m = 0.1f; + const float weight_s = 1.0f; + const float weight_i = 1.0f; + float loudness = (weight_m * momentary + weight_s * shortterm + + weight_i * global) + / (weight_m + weight_s + weight_i); + + float diff = target - loudness; + + float gain = DB_TO_GAIN(diff); + + double peak = (peak_L > peak_R) ? peak_L : peak_R; + + if (peak >= 0.00001 && gain * peak < 1.0f) + { + player.auto_gain = gain; + } + } + } + + alSourcef(player.source, AL_GAIN, player.gain * player.auto_gain); +} + static boolean UpdatePlayer(void) { ALint processed, state; @@ -114,6 +248,11 @@ static boolean UpdatePlayer(void) // the source. frames = active_module->I_FillStream(player.data, BUFFER_SAMPLES); + if (frames > 0) + { + AutoGain(frames); + } + if (frames > 0) { size = frames * player.frame_size; @@ -176,6 +315,11 @@ static boolean StartPlayer(void) break; } + if (frames > 0) + { + AutoGain(frames); + } + size = frames * player.frame_size; alBufferData(player.buffers[i], player.format, player.data, size, @@ -304,20 +448,23 @@ static void I_OAL_SetMusicVolume(int volume) return; } - ALfloat gain = (ALfloat)volume / 15.0f; + player.gain = (ALfloat)volume / 15.0f; - if (active_module == &stream_opl_module) + if (!auto_gain) { - gain *= (ALfloat)DB_TO_GAIN(opl_gain); - } -#if defined(HAVE_FLUIDSYNTH) - else if (active_module == &stream_fl_module) - { - gain *= (ALfloat)DB_TO_GAIN(fl_gain); - } -#endif + if (active_module == &stream_opl_module) + { + player.gain *= (ALfloat)DB_TO_GAIN(opl_gain); + } + #if defined(HAVE_FLUIDSYNTH) + else if (active_module == &stream_fl_module) + { + player.gain *= (ALfloat)DB_TO_GAIN(fl_gain); + } + #endif - alSourcef(player.source, AL_GAIN, gain); + alSourcef(player.source, AL_GAIN, player.gain); + } } static void I_OAL_PauseSong(void *handle) @@ -388,9 +535,10 @@ static void I_OAL_UnRegisterSong(void *handle) if (active_module) { active_module->I_CloseStream(); - active_module = NULL; } + ShutdownAutoGain(); + if (player.data) { free(player.data); @@ -429,6 +577,7 @@ static void *I_OAL_RegisterSong(void *data, int len) &player.freq, &player.frame_size)) { active_module = all_modules[i]; + InitAutoGain(); return (void *)1; } } @@ -475,10 +624,11 @@ static midiplayertype_t I_OAL_MidiPlayerType(void) static void I_OAL_BindVariables(void) { + BIND_BOOL_MUSIC(auto_gain, true, "Auto Gain"); #if defined (HAVE_FLUIDSYNTH) - BIND_NUM_MIDI(fl_gain, 0, -20, 20, "[FluidSynth] Gain [dB]"); + BIND_NUM_MUSIC(fl_gain, 0, -20, 20, "[FluidSynth] Gain [dB]"); #endif - BIND_NUM_MIDI(opl_gain, 0, -20, 20, "[OPL3 Emulation] Gain [dB]"); + BIND_NUM_MUSIC(opl_gain, 0, -20, 20, "[OPL3 Emulation] Gain [dB]"); for (int i = 0; i < arrlen(midi_modules); ++i) { midi_modules[i]->BindVariables(); diff --git a/src/i_oplmusic.c b/src/i_oplmusic.c index 0a8ee065..08ca74c5 100644 --- a/src/i_oplmusic.c +++ b/src/i_oplmusic.c @@ -1697,9 +1697,9 @@ static const char *I_OPL_MusicFormat(void) static void I_OPL_BindVariables(void) { - BIND_NUM_MIDI(num_opl_chips, 1, 1, OPL_MAX_CHIPS, + BIND_NUM_MUSIC(num_opl_chips, 1, 1, OPL_MAX_CHIPS, "[OPL3 Emulation] Number of chips to emulate (1-6)"); - BIND_BOOL_MIDI(opl_stereo_correct, false, + BIND_BOOL_MUSIC(opl_stereo_correct, false, "[OPL3 Emulation] Use MIDI-correct stereo channel polarity"); } diff --git a/src/i_sound.h b/src/i_sound.h index b1de0da5..f1784895 100644 --- a/src/i_sound.h +++ b/src/i_sound.h @@ -143,6 +143,8 @@ int I_SoundID(int handle); // MUSIC I/O // +extern boolean auto_gain; + typedef enum { midiplayer_none, diff --git a/src/m_config.h b/src/m_config.h index d10b9fe5..4e6e7a0b 100644 --- a/src/m_config.h +++ b/src/m_config.h @@ -45,8 +45,8 @@ void M_BindNum(const char *name, void *location, void *current, #define BIND_NUM_SFX(name, v, a, b, help) \ M_BindNum(#name, &name, NULL, (v), (a), (b), ss_sfx, wad_no, help) -#define BIND_NUM_MIDI(name, v, a, b, help) \ - M_BindNum(#name, &name, NULL, (v), (a), (b), ss_midi, wad_no, help) +#define BIND_NUM_MUSIC(name, v, a, b, help) \ + M_BindNum(#name, &name, NULL, (v), (a), (b), ss_music, wad_no, help) void M_BindBool(const char *name, boolean *location, boolean *current, boolean default_val, ss_types screen, wad_allowed_t wad, @@ -61,8 +61,8 @@ void M_BindBool(const char *name, boolean *location, boolean *current, #define BIND_BOOL_SFX(name, v, help) \ M_BindBool(#name, &name, NULL, (v), ss_sfx, wad_no, help) -#define BIND_BOOL_MIDI(name, v, help) \ - M_BindBool(#name, &name, NULL, (v), ss_midi, wad_no, help) +#define BIND_BOOL_MUSIC(name, v, help) \ + M_BindBool(#name, &name, NULL, (v), ss_music, wad_no, help) void M_BindStr(char *name, const char **location, char *default_val, wad_allowed_t wad, const char *help); diff --git a/src/mn_setup.c b/src/mn_setup.c index c35ecdb8..962892b7 100644 --- a/src/mn_setup.c +++ b/src/mn_setup.c @@ -31,7 +31,6 @@ #include "i_oalsound.h" #include "i_rumble.h" #include "i_sound.h" -#include "i_timer.h" #include "i_video.h" #include "m_argv.h" #include "m_array.h" @@ -341,8 +340,6 @@ enum str_sound_module, str_resampler, str_equalizer_preset, - str_midi_complevel, - str_midi_reset_type, str_mouse_accel, @@ -2482,14 +2479,6 @@ static void SetMidiPlayer(void) S_RestartMusic(); } -static void SetMidiPlayerNative(void) -{ - if (I_MidiPlayerType() == midiplayer_native) - { - SetMidiPlayer(); - } -} - static void SetMidiPlayerOpl(void) { if (I_MidiPlayerType() == midiplayer_opl) @@ -2506,8 +2495,15 @@ static void SetMidiPlayerFluidSynth(void) } } +static void RestartMusic(void) +{ + S_StopMusic(); + S_SetMusicVolume(snd_MusicVolume); + S_RestartMusic(); +} + static void MN_Sfx(void); -static void MN_Midi(void); +static void MN_Music(void); static void MN_Equalizer(void); static setup_menu_t gen_settings2[] = { @@ -2537,7 +2533,7 @@ static setup_menu_t gen_settings2[] = { {"Sound Options", S_FUNC, CNTR_X, M_SPC, .action = MN_Sfx}, - {"MIDI Options", S_FUNC, CNTR_X, M_SPC, .action = MN_Midi}, + {"Music Options", S_FUNC, CNTR_X, M_SPC, .action = MN_Music}, {"Equalizer Options", S_FUNC, CNTR_X, M_SPC, .action = MN_Equalizer}, @@ -2593,6 +2589,7 @@ static void MN_Sfx(void) current_tabs = sfx_tabs; SetupMenuSecondary(); } + void MN_DrawSfx(void) { DrawBackground("FLOOR4_6"); @@ -2602,29 +2599,18 @@ void MN_DrawSfx(void) DrawScreenItems(current_menu); } -static const char *midi_complevel_strings[] = { - "Vanilla", "Standard", "Full" -}; +static void UpdateGainItems(void); -static const char *midi_reset_type_strings[] = { - "No SysEx", "General MIDI", "Roland GS", "Yamaha XG" -}; +static void ResetAutoGain(void) +{ + RestartMusic(); + UpdateGainItems(); +} -static setup_menu_t midi_settings1[] = { +static setup_menu_t music_settings1[] = { - {"Native MIDI Gain", S_THERMO, CNTR_X, M_THRM_SPC, - {"midi_gain"}, .action = UpdateMusicVolume, .append = "dB"}, - - {"Native MIDI Reset", S_CHOICE | S_ACTION, CNTR_X, M_SPC, - {"midi_reset_type"}, .strings_id = str_midi_reset_type, - .action = SetMidiPlayerNative}, - - {"Compatibility Level", S_CHOICE | S_ACTION, CNTR_X, M_SPC, - {"midi_complevel"}, .strings_id = str_midi_complevel, - .action = SetMidiPlayerNative}, - - {"SC-55 CTF Emulation", S_ONOFF, CNTR_X, M_SPC, {"midi_ctf"}, - .action = SetMidiPlayerNative}, + {"Auto Gain", S_ONOFF, CNTR_X, M_SPC, {"auto_gain"}, + .action = ResetAutoGain}, MI_GAP, @@ -2653,19 +2639,25 @@ static setup_menu_t midi_settings1[] = { MI_END }; -static setup_menu_t *midi_settings[] = {midi_settings1, NULL}; +static void UpdateGainItems(void) +{ + DisableItem(auto_gain, music_settings1, "fl_gain"); + DisableItem(auto_gain, music_settings1, "opl_gain"); +} -static setup_tab_t midi_tabs[] = {{"MIDI"}, {NULL}}; +static setup_menu_t *music_settings[] = {music_settings1, NULL}; -static void MN_Midi(void) +static setup_tab_t midi_tabs[] = {{"Music"}, {NULL}}; + +static void MN_Music(void) { SetItemOn(set_item_on); SetPageIndex(current_page); - MN_SetNextMenuAlt(ss_midi); - setup_screen = ss_midi; - current_page = GetPageIndex(midi_settings); - current_menu = midi_settings[current_page]; + MN_SetNextMenuAlt(ss_music); + setup_screen = ss_music; + current_page = GetPageIndex(music_settings); + current_menu = music_settings[current_page]; current_tabs = midi_tabs; SetupMenuSecondary(); } @@ -3423,7 +3415,7 @@ static setup_menu_t **setup_screens[] = { gen_settings, // killough 10/98 comp_settings, sfx_settings, - midi_settings, + music_settings, eq_settings, padadv_settings, gyro_settings, @@ -3554,7 +3546,7 @@ static void ResetDefaultsSecondary(void) if (setup_screen == ss_gen) { ResetDefaults(ss_sfx); - ResetDefaults(ss_midi); + ResetDefaults(ss_music); ResetDefaults(ss_eq); ResetDefaults(ss_padadv); ResetDefaults(ss_gyro); @@ -4786,8 +4778,6 @@ static const char **selectstrings[] = { sound_module_strings, NULL, // str_resampler equalizer_preset_strings, - midi_complevel_strings, - midi_reset_type_strings, NULL, // str_mouse_accel gyro_space_strings, gyro_action_strings, @@ -4855,6 +4845,7 @@ void MN_SetupResetMenu(void) UpdateGyroItems(); UpdateWeaponSlotItems(); MN_UpdateEqualizerItems(); + UpdateGainItems(); } void MN_BindMenuVariables(void) diff --git a/vcpkg.json b/vcpkg.json index 65d23200..f109fd44 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -32,6 +32,7 @@ } ] }, + "libebur128", "libsndfile", { "name": "fluidsynth",