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>
This commit is contained in:
Roman Fomin 2024-11-12 07:31:42 +07:00 committed by GitHub
parent a9c4b09102
commit 8dbeded359
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 251 additions and 71 deletions

View File

@ -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 \

View File

@ -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)

View File

@ -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)

View File

@ -188,6 +188,7 @@ target_link_libraries(woof PRIVATE
${SDL2_LIBRARIES}
SDL2_net::SDL2_net
OpenAL::OpenAL
libebur128::libebur128
SndFile::sndfile
opl
pffft

View File

@ -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,

View File

@ -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");

View File

@ -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 =

View File

@ -18,6 +18,7 @@
#include "al.h"
#include "alc.h"
#include "alext.h"
#include "ebur128.h"
#include <stdlib.h>
@ -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();

View File

@ -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");
}

View File

@ -143,6 +143,8 @@ int I_SoundID(int handle);
// MUSIC I/O
//
extern boolean auto_gain;
typedef enum
{
midiplayer_none,

View File

@ -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);

View File

@ -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)

View File

@ -32,6 +32,7 @@
}
]
},
"libebur128",
"libsndfile",
{
"name": "fluidsynth",