IOS/USB: Implement a bare-bones Wii Speak loudness level

Add a volume modifier to the UI which relies on gain.
This commit is contained in:
Sepalani 2024-10-02 13:53:02 +04:00
parent 1f0ff42aa0
commit 74a875e9d6
6 changed files with 238 additions and 8 deletions

View File

@ -601,6 +601,8 @@ const Info<std::string> MAIN_WII_SPEAK_MICROPHONE{
{System::Main, "EmulatedUSBDevices", "WiiSpeakMicrophone"}, ""};
const Info<bool> MAIN_WII_SPEAK_MUTED{{System::Main, "EmulatedUSBDevices", "WiiSpeakMuted"}, true};
const Info<s16> 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.

View File

@ -365,6 +365,7 @@ extern const Info<bool> MAIN_EMULATE_INFINITY_BASE;
extern const Info<bool> MAIN_EMULATE_WII_SPEAK;
extern const Info<std::string> MAIN_WII_SPEAK_MICROPHONE;
extern const Info<bool> MAIN_WII_SPEAK_MUTED;
extern const Info<s16> MAIN_WII_SPEAK_VOLUME_MODIFIER;
// GameCube path utility functions

View File

@ -4,6 +4,9 @@
#include "Core/IOS/USB/Emulated/Microphone.h"
#include <algorithm>
#include <cmath>
#include <ranges>
#include <span>
#ifdef HAVE_CUBEB
#include <cubeb/cubeb.h>
@ -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<Microphone*>(user_data);
return mic->DataCallback(static_cast<const s16*>(input_buffer), nframes);
return mic->DataCallback(static_cast<const SampleType*>(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<const s16*>(input_buffer);
for (long i = 0; i < nframes; i++)
std::span<const SampleType> 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<SampleType>(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<u16>(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<u16>((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

View File

@ -4,8 +4,11 @@
#pragma once
#include <array>
#include <atomic>
#include <limits>
#include <memory>
#include <mutex>
#include <type_traits>
#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<SampleType>;
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<u16> 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<UnsignedSampleType>::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;

View File

@ -398,7 +398,9 @@ void WiiSpeak::GetRegister(const std::unique_ptr<CtrlMessage>& 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;

View File

@ -3,8 +3,11 @@
#include "DolphinQt/EmulatedUSB/WiiSpeakWindow.h"
#include <algorithm>
#include <QCheckBox>
#include <QComboBox>
#include <QGridLayout>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QLabel>
@ -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<int>(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")),