diff --git a/Source/Core/Core/Config/MainSettings.cpp b/Source/Core/Core/Config/MainSettings.cpp index 6e782fdb4e..f1b14be930 100644 --- a/Source/Core/Core/Config/MainSettings.cpp +++ b/Source/Core/Core/Config/MainSettings.cpp @@ -601,6 +601,8 @@ const Info MAIN_WII_SPEAK_MICROPHONE{ {System::Main, "EmulatedUSBDevices", "WiiSpeakMicrophone"}, ""}; const Info MAIN_WII_SPEAK_MUTED{{System::Main, "EmulatedUSBDevices", "WiiSpeakMuted"}, true}; +const Info MAIN_WII_SPEAK_VOLUME_MODIFIER{ + {System::Main, "EmulatedUSBDevices", "WiiSpeakVolumeModifier"}, 0}; // The reason we need this function is because some memory card code // expects to get a non-NTSC-K region even if we're emulating an NTSC-K Wii. diff --git a/Source/Core/Core/Config/MainSettings.h b/Source/Core/Core/Config/MainSettings.h index add214340b..1944b90f87 100644 --- a/Source/Core/Core/Config/MainSettings.h +++ b/Source/Core/Core/Config/MainSettings.h @@ -365,6 +365,7 @@ extern const Info MAIN_EMULATE_INFINITY_BASE; extern const Info MAIN_EMULATE_WII_SPEAK; extern const Info MAIN_WII_SPEAK_MICROPHONE; extern const Info MAIN_WII_SPEAK_MUTED; +extern const Info MAIN_WII_SPEAK_VOLUME_MODIFIER; // GameCube path utility functions diff --git a/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp b/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp index 4b7912ecfe..9ab0aaa0b2 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp +++ b/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp @@ -4,6 +4,9 @@ #include "Core/IOS/USB/Emulated/Microphone.h" #include +#include +#include +#include #ifdef HAVE_CUBEB #include @@ -12,6 +15,7 @@ #endif #include "Common/Logging/Log.h" +#include "Common/MathUtil.h" #include "Common/Swap.h" #include "Core/Config/MainSettings.h" #include "Core/Core.h" @@ -158,10 +162,10 @@ long Microphone::CubebDataCallback(cubeb_stream* stream, void* user_data, const return nframes; auto* mic = static_cast(user_data); - return mic->DataCallback(static_cast(input_buffer), nframes); + return mic->DataCallback(static_cast(input_buffer), nframes); } -long Microphone::DataCallback(const s16* input_buffer, long nframes) +long Microphone::DataCallback(const SampleType* input_buffer, long nframes) { std::lock_guard lock(m_ring_lock); @@ -169,10 +173,16 @@ long Microphone::DataCallback(const s16* input_buffer, long nframes) if (!m_sampler.sample_on || m_sampler.mute) return nframes; - const s16* buff_in = static_cast(input_buffer); - for (long i = 0; i < nframes; i++) + std::span buffer(input_buffer, nframes); + const auto gain = ComputeGain(Config::Get(Config::MAIN_WII_SPEAK_VOLUME_MODIFIER)); + const auto apply_gain = [gain](SampleType sample) { + return MathUtil::SaturatingCast(sample * gain); + }; + + for (const SampleType le_sample : std::ranges::transform_view(buffer, apply_gain)) { - m_stream_buffer[m_stream_wpos] = Common::swap16(buff_in[i]); + UpdateLoudness(le_sample); + m_stream_buffer[m_stream_wpos] = Common::swap16(le_sample); m_stream_wpos = (m_stream_wpos + 1) % STREAM_SIZE; } @@ -213,9 +223,148 @@ u16 Microphone::ReadIntoBuffer(u8* ptr, u32 size) return static_cast(ptr - begin); } +u16 Microphone::GetLoudnessLevel() const +{ + if (m_sampler.mute || Config::Get(Config::MAIN_WII_SPEAK_MUTED)) + return 0; + return m_loudness_level; +} + +// Based on graphical cues on Monster Hunter 3, the level seems properly displayed with values +// between 0 and 0x3a00. +// +// TODO: Proper hardware testing, documentation, formulas... +void Microphone::UpdateLoudness(const SampleType sample) +{ + // Based on MH3 graphical cues, let's use a 0x4000 window + static const u32 WINDOW = 0x4000; + static const FloatType UNIT = (m_loudness.DB_MAX - m_loudness.DB_MIN) / WINDOW; + + m_loudness.Update(sample); + + if (m_loudness.samples_count >= m_loudness.SAMPLES_NEEDED) + { + const FloatType amp_db = m_loudness.GetAmplitudeDb(); + m_loudness_level = static_cast((amp_db - m_loudness.DB_MIN) / UNIT); + +#ifdef WII_SPEAK_LOG_STATS + m_loudness.LogStats(); +#endif + + m_loudness.Reset(); + } +} + bool Microphone::HasData(u32 sample_count = BUFF_SIZE_SAMPLES) const { std::lock_guard lock(m_ring_lock); return m_samples_avail >= sample_count; } + +Microphone::FloatType Microphone::ComputeGain(FloatType relative_db) const +{ + return m_loudness.ComputeGain(relative_db); +} + +const Microphone::FloatType Microphone::Loudness::DB_MIN = + 20 * std::log10(FloatType(1) / MAX_AMPLITUDE); +const Microphone::FloatType Microphone::Loudness::DB_MAX = 20 * std::log10(FloatType(1)); + +void Microphone::Loudness::Update(const SampleType sample) +{ + ++samples_count; + + peak_min = std::min(sample, peak_min); + peak_max = std::max(sample, peak_max); + absolute_sum += std::abs(sample); + square_sum += std::pow(FloatType(sample), FloatType(2)); +} + +Microphone::SampleType Microphone::Loudness::GetPeak() const +{ + return std::max(std::abs(peak_min), std::abs(peak_max)); +} + +Microphone::FloatType Microphone::Loudness::GetDecibel(FloatType value) +{ + return 20 * std::log10(value); +} + +Microphone::FloatType Microphone::Loudness::GetAmplitude() const +{ + return GetPeak() / MAX_AMPLITUDE; +} + +Microphone::FloatType Microphone::Loudness::GetAmplitudeDb() const +{ + return GetDecibel(GetAmplitude()); +} + +Microphone::FloatType Microphone::Loudness::GetAbsoluteMean() const +{ + return FloatType(absolute_sum) / samples_count; +} + +Microphone::FloatType Microphone::Loudness::GetAbsoluteMeanDb() const +{ + return GetDecibel(GetAbsoluteMean()); +} + +Microphone::FloatType Microphone::Loudness::GetRootMeanSquare() const +{ + return std::sqrt(square_sum / samples_count); +} + +Microphone::FloatType Microphone::Loudness::GetRootMeanSquareDb() const +{ + return GetDecibel(GetRootMeanSquare()); +} + +Microphone::FloatType Microphone::Loudness::GetCrestFactor() const +{ + const auto rms = GetRootMeanSquare(); + if (rms == 0) + return FloatType(0); + return GetPeak() / rms; +} + +Microphone::FloatType Microphone::Loudness::GetCrestFactorDb() const +{ + return GetDecibel(GetCrestFactor()); +} + +Microphone::FloatType Microphone::Loudness::ComputeGain(FloatType db) +{ + return std::pow(FloatType(10), db / 20); +} + +void Microphone::Loudness::Reset() +{ + samples_count = 0; + absolute_sum = 0; + square_sum = FloatType(0); + peak_min = 0; + peak_max = 0; +} + +void Microphone::Loudness::LogStats() +{ + const auto amplitude = GetAmplitude(); + const auto amplitude_db = GetDecibel(amplitude); + const auto rms = GetRootMeanSquare(); + const auto rms_db = GetDecibel(rms); + const auto abs_mean = GetAbsoluteMean(); + const auto abs_mean_db = GetDecibel(abs_mean); + const auto crest_factor = GetCrestFactor(); + const auto crest_factor_db = GetDecibel(crest_factor); + + INFO_LOG_FMT(IOS_USB, + "Wii Speak loudness stats (sample count: {}/{}):\n" + " - min={} max={} amplitude={} ({} dB)\n" + " - rms={} ({} dB) \n" + " - abs_mean={} ({} dB)\n" + " - crest_factor={} ({} dB)", + samples_count, SAMPLES_NEEDED, peak_min, peak_max, amplitude, amplitude_db, rms, + rms_db, abs_mean, abs_mean_db, crest_factor, crest_factor_db); +} } // namespace IOS::HLE::USB diff --git a/Source/Core/Core/IOS/USB/Emulated/Microphone.h b/Source/Core/Core/IOS/USB/Emulated/Microphone.h index ac52d74581..568bcd8f83 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Microphone.h +++ b/Source/Core/Core/IOS/USB/Emulated/Microphone.h @@ -4,8 +4,11 @@ #pragma once #include +#include +#include #include #include +#include #include "AudioCommon/CubebUtils.h" #include "Common/CommonTypes.h" @@ -24,11 +27,17 @@ struct WiiSpeakState; class Microphone final { public: + using FloatType = float; + using SampleType = s16; + using UnsignedSampleType = std::make_unsigned_t; + Microphone(const WiiSpeakState& sampler); ~Microphone(); bool HasData(u32 sample_count) const; u16 ReadIntoBuffer(u8* ptr, u32 size); + u16 GetLoudnessLevel() const; + FloatType ComputeGain(FloatType relative_db) const; private: #ifdef HAVE_CUBEB @@ -36,7 +45,8 @@ private: void* output_buffer, long nframes); #endif - long DataCallback(const s16* input_buffer, long nframes); + long DataCallback(const SampleType* input_buffer, long nframes); + void UpdateLoudness(SampleType sample); void StreamInit(); void StreamTerminate(); @@ -44,7 +54,6 @@ private: void StreamStop(); static constexpr u32 SAMPLING_RATE = 8000; - using SampleType = s16; static constexpr u32 BUFF_SIZE_SAMPLES = 16; static constexpr u32 STREAM_SIZE = BUFF_SIZE_SAMPLES * 500; @@ -53,6 +62,44 @@ private: u32 m_stream_rpos = 0; u32 m_samples_avail = 0; + // TODO: Find how this level is calculated on real hardware + std::atomic m_loudness_level = 0; + struct Loudness + { + void Update(SampleType sample); + + SampleType GetPeak() const; + static FloatType GetDecibel(FloatType value); + FloatType GetAmplitude() const; + FloatType GetAmplitudeDb() const; + FloatType GetAbsoluteMean() const; + FloatType GetAbsoluteMeanDb() const; + FloatType GetRootMeanSquare() const; + FloatType GetRootMeanSquareDb() const; + FloatType GetCrestFactor() const; + FloatType GetCrestFactorDb() const; + static FloatType ComputeGain(FloatType db); + + void Reset(); + void LogStats(); + + // Samples used to compute the loudness level + static constexpr u16 SAMPLES_NEEDED = SAMPLING_RATE / 125; + static_assert((SAMPLES_NEEDED % BUFF_SIZE_SAMPLES) == 0); + + static constexpr FloatType MAX_AMPLITUDE = + UnsignedSampleType{std::numeric_limits::max() / 2}; + static const FloatType DB_MIN; + static const FloatType DB_MAX; + + u16 samples_count = 0; + u32 absolute_sum = 0; + FloatType square_sum = FloatType(0); + SampleType peak_min = 0; + SampleType peak_max = 0; + }; + Loudness m_loudness; + mutable std::mutex m_ring_lock; const WiiSpeakState& m_sampler; diff --git a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp index 720640e0e6..1cea22c89b 100644 --- a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp +++ b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp @@ -398,7 +398,9 @@ void WiiSpeak::GetRegister(const std::unique_ptr& cmd) const case SP_SIN: break; case SP_SOUT: - memory.Write_U16(0x39B0, arg2); // 6dB + // TODO: Find how it was measured and how accurate it was + // memory.Write_U16(0x39B0, arg2); // 6dB + memory.Write_U16(m_microphone->GetLoudnessLevel(), arg2); break; case SP_RIN: break; diff --git a/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.cpp b/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.cpp index 23db58601d..0616952680 100644 --- a/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.cpp +++ b/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.cpp @@ -3,8 +3,11 @@ #include "DolphinQt/EmulatedUSB/WiiSpeakWindow.h" +#include + #include #include +#include #include #include #include @@ -60,8 +63,34 @@ void WiiSpeakWindow::CreateMainWindow() auto checkbox_mic_muted = new QCheckBox(tr("Mute"), this); checkbox_mic_muted->setChecked(Config::Get(Config::MAIN_WII_SPEAK_MUTED)); connect(checkbox_mic_muted, &QCheckBox::toggled, this, &WiiSpeakWindow::SetWiiSpeakMuted); + checkbox_mic_muted->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); config_layout->addWidget(checkbox_mic_muted); + auto* volume_layout = new QGridLayout(); + static constexpr int FILTER_MIN = -50; + static constexpr int FILTER_MAX = 50; + const int volume_modifier = + std::clamp(Config::Get(Config::MAIN_WII_SPEAK_VOLUME_MODIFIER), FILTER_MIN, FILTER_MAX); + auto filter_slider = new QSlider(Qt::Horizontal, this); + auto slider_label = new QLabel(tr("Volume modifier (value: %1dB)").arg(volume_modifier)); + connect(filter_slider, &QSlider::valueChanged, this, [slider_label](int value) { + Config::SetBaseOrCurrent(Config::MAIN_WII_SPEAK_VOLUME_MODIFIER, value); + slider_label->setText(tr("Volume modifier (value: %1dB)").arg(value)); + }); + filter_slider->setMinimum(FILTER_MIN); + filter_slider->setMaximum(FILTER_MAX); + filter_slider->setValue(volume_modifier); + filter_slider->setTickPosition(QSlider::TicksBothSides); + filter_slider->setTickInterval(10); + filter_slider->setSingleStep(1); + volume_layout->addWidget(new QLabel(QStringLiteral("%1dB").arg(FILTER_MIN)), 0, 0, Qt::AlignLeft); + volume_layout->addWidget(slider_label, 0, 1, Qt::AlignCenter); + volume_layout->addWidget(new QLabel(QStringLiteral("%1dB").arg(FILTER_MAX)), 0, 2, + Qt::AlignRight); + volume_layout->addWidget(filter_slider, 1, 0, 1, 3); + config_layout->addLayout(volume_layout); + config_layout->setStretch(1, 3); + m_combobox_microphones = new QComboBox(); #ifndef HAVE_CUBEB m_combobox_microphones->addItem(QLatin1String("(%1)").arg(tr("Audio backend unsupported")),