diff --git a/Source/Core/Core/Config/MainSettings.cpp b/Source/Core/Core/Config/MainSettings.cpp index 742ca15ffd..5774f6cbdb 100644 --- a/Source/Core/Core/Config/MainSettings.cpp +++ b/Source/Core/Core/Config/MainSettings.cpp @@ -581,6 +581,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 10f91f25c3..d7c3689b9d 100644 --- a/Source/Core/Core/Config/MainSettings.h +++ b/Source/Core/Core/Config/MainSettings.h @@ -358,6 +358,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 4894f3b770..fcd31cfae9 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp +++ b/Source/Core/Core/IOS/USB/Emulated/Microphone.cpp @@ -3,12 +3,13 @@ #include "Core/IOS/USB/Emulated/Microphone.h" -#include +#include #include #include "AudioCommon/CubebUtils.h" #include "Common/Logging/Log.h" +#include "Common/MathUtil.h" #include "Common/Swap.h" #include "Core/Config/MainSettings.h" #include "Core/Core.h" @@ -143,11 +144,17 @@ long Microphone::DataCallback(cubeb_stream* stream, void* user_data, const void* return nframes; std::lock_guard lk(mic->m_ring_lock); + std::span buff_in(static_cast(input_buffer), nframes); + const auto gain = mic->ComputeGain(Config::Get(Config::MAIN_WII_SPEAK_VOLUME_MODIFIER)); + auto processed_buff_in = buff_in | std::views::transform([gain](s16 sample) { + return MathUtil::SaturatingCast(sample * gain); + }); - const s16* buff_in = static_cast(input_buffer); - for (long i = 0; i < nframes; i++) + mic->UpdateLoudness(processed_buff_in); + + for (s16 le_sample : processed_buff_in) { - mic->m_stream_buffer[mic->m_stream_wpos] = Common::swap16(buff_in[i]); + mic->m_stream_buffer[mic->m_stream_wpos] = Common::swap16(le_sample); mic->m_stream_wpos = (mic->m_stream_wpos + 1) % mic->STREAM_SIZE; } @@ -187,6 +194,45 @@ 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(std::ranges::input_range auto&& samples) +{ + // Based on MH3 graphical cues, let's use a 0x4000 window + static const u32 WINDOW = 0x4000; + static const float UNIT = (m_loudness.DB_MAX - m_loudness.DB_MIN) / WINDOW; + + m_loudness.Update(samples); + + if (m_loudness.samples_count >= m_loudness.SAMPLES_NEEDED) + { + const float 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(); +#else + DEBUG_LOG_FMT(IOS_USB, + "Wii Speak loudness stats (sample count: {}/{}):\n" + " - min={} max={} amplitude={} dB\n" + " - level={:04x}", + m_loudness.samples_count, m_loudness.SAMPLES_NEEDED, m_loudness.peak_min, + m_loudness.peak_max, amp_db, m_loudness_level); +#endif + + m_loudness.Reset(); + } +} + bool Microphone::HasData(u32 sample_count = BUFF_SIZE_SAMPLES) const { return m_samples_avail >= sample_count; @@ -196,4 +242,101 @@ const WiiSpeakState& Microphone::GetSampler() const { return m_sampler; } + +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_AMPLTIUDE); +const Microphone::FloatType Microphone::Loudness::DB_MAX = 20 * std::log10(FloatType(1)); + +Microphone::Loudness::SampleType Microphone::Loudness::GetPeak() const +{ + return std::max(std::abs(peak_min), std::abs(peak_max)); +} + +Microphone::FloatType Microphone::Loudness::GetDecibel(FloatType value) const +{ + return 20 * std::log10(value); +} + +Microphone::FloatType Microphone::Loudness::GetAmplitude() const +{ + return GetPeak() / MAX_AMPLTIUDE; +} + +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) const +{ + 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 a7e0cc4a80..f1bf890d87 100644 --- a/Source/Core/Core/IOS/USB/Emulated/Microphone.h +++ b/Source/Core/Core/IOS/USB/Emulated/Microphone.h @@ -3,9 +3,15 @@ #pragma once +#include #include +#include +#include #include #include +#include +#include +#include #include "AudioCommon/CubebUtils.h" #include "Common/CommonTypes.h" @@ -20,12 +26,17 @@ struct WiiSpeakState; class Microphone final { public: + using FloatType = float; + Microphone(const WiiSpeakState& sampler); ~Microphone(); bool HasData(u32 sample_count) const; u16 ReadIntoBuffer(u8* ptr, u32 size); + u16 GetLoudnessLevel() const; + void UpdateLoudness(std::ranges::input_range auto&& samples); const WiiSpeakState& GetSampler() const; + FloatType ComputeGain(FloatType relative_db) const; private: static long DataCallback(cubeb_stream* stream, void* user_data, const void* input_buffer, @@ -46,6 +57,61 @@ private: u32 m_stream_rpos = 0; u32 m_samples_avail = 0; + // TODO: Find how this level is calculated on real hardware + u16 m_loudness_level = 0; + struct Loudness + { + using SampleType = s16; + using UnsignedSampleType = std::make_unsigned_t; + + void Update(const auto& samples) + { + samples_count += static_cast(samples.size()); + + const auto [min_element, max_element] = std::ranges::minmax_element(samples); + peak_min = std::min(*min_element, peak_min); + peak_max = std::max(*max_element, peak_max); + + const auto begin = samples.begin(); + const auto end = samples.end(); + absolute_sum = std::reduce(begin, end, absolute_sum, + [](u32 a, SampleType b) { return a + std::abs(b); }); + square_sum = std::reduce(begin, end, square_sum, [](FloatType a, s16 b) { + return a + std::pow(FloatType(b), FloatType(2)); + }); + } + + SampleType GetPeak() const; + FloatType GetDecibel(FloatType value) const; + FloatType GetAmplitude() const; + FloatType GetAmplitudeDb() const; + FloatType GetAbsoluteMean() const; + FloatType GetAbsoluteMeanDb() const; + FloatType GetRootMeanSquare() const; + FloatType GetRootMeanSquareDb() const; + FloatType GetCrestFactor() const; + FloatType GetCrestFactorDb() const; + FloatType ComputeGain(FloatType db) const; + + 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_AMPLTIUDE = 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; + std::mutex m_ring_lock; std::shared_ptr m_cubeb_ctx = nullptr; cubeb_stream* m_cubeb_stream = nullptr; diff --git a/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp b/Source/Core/Core/IOS/USB/Emulated/WiiSpeak.cpp index 5aef863b04..31881f23d3 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 2dd95cee1d..0bc182efa1 100644 --- a/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.cpp +++ b/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.cpp @@ -3,8 +3,12 @@ #include "DolphinQt/EmulatedUSB/WiiSpeakWindow.h" +#include +#include + #include #include +#include #include #include #include @@ -61,8 +65,34 @@ void WiiSpeakWindow::CreateMainWindow() checkbox_mic_muted->setChecked(Config::Get(Config::MAIN_WII_SPEAK_MUTED)); connect(checkbox_mic_muted, &QCheckBox::toggled, this, &WiiSpeakWindow::SetWiiSpeakConnectionState); + 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); + m_slider_label = new QLabel(tr("Volume modifier (value: %1dB)").arg(volume_modifier)); + connect(filter_slider, &QSlider::valueChanged, this, [this](int value) { + Config::SetBaseOrCurrent(Config::MAIN_WII_SPEAK_VOLUME_MODIFIER, value); + m_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(m_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(); m_combobox_microphones->addItem(QLatin1String("(%1)").arg(tr("Autodetect preferred microphone")), QString{}); diff --git a/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.h b/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.h index 1a6d155583..20a0822d30 100644 --- a/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.h +++ b/Source/Core/DolphinQt/EmulatedUSB/WiiSpeakWindow.h @@ -11,6 +11,7 @@ class QCheckBox; class QComboBox; class QGroupBox; +class QLabel; class WiiSpeakWindow : public QWidget { @@ -29,4 +30,5 @@ private: QCheckBox* m_checkbox_enabled; QComboBox* m_combobox_microphones; QGroupBox* m_config_group; + QLabel* m_slider_label; };