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
2d963123a7
commit
0fa236a67e
|
@ -581,6 +581,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.
|
||||
|
|
|
@ -358,6 +358,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
|
||||
|
||||
|
|
|
@ -3,12 +3,13 @@
|
|||
|
||||
#include "Core/IOS/USB/Emulated/Microphone.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <span>
|
||||
|
||||
#include <cubeb/cubeb.h>
|
||||
|
||||
#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<const s16> buff_in(static_cast<const s16*>(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<s16>(sample * gain);
|
||||
});
|
||||
|
||||
const s16* buff_in = static_cast<const s16*>(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<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(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<u16>((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
|
||||
|
|
|
@ -3,9 +3,15 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <numeric>
|
||||
#include <ranges>
|
||||
#include <type_traits>
|
||||
|
||||
#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<SampleType>;
|
||||
|
||||
void Update(const auto& samples)
|
||||
{
|
||||
samples_count += static_cast<u16>(samples.size());
|
||||
|
||||
const auto [min_element, max_element] = std::ranges::minmax_element(samples);
|
||||
peak_min = std::min<SampleType>(*min_element, peak_min);
|
||||
peak_max = std::max<SampleType>(*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<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;
|
||||
|
||||
std::mutex m_ring_lock;
|
||||
std::shared_ptr<cubeb> m_cubeb_ctx = nullptr;
|
||||
cubeb_stream* m_cubeb_stream = nullptr;
|
||||
|
|
|
@ -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,12 @@
|
|||
|
||||
#include "DolphinQt/EmulatedUSB/WiiSpeakWindow.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QGridLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
|
@ -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<int>(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{});
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue