mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-06-13 10:47:48 +00:00
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:
parent
1f0ff42aa0
commit
74a875e9d6
@ -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.
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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")),
|
||||
|
Loading…
Reference in New Issue
Block a user