AudioCommon: Added Granular Synthesis

This commit is contained in:
Sam Belliveau
2025-03-14 01:22:35 -05:00
committed by Jordan Woyak
parent e82f03b825
commit f09ba10daa
51 changed files with 387 additions and 7928 deletions
@@ -1,85 +0,0 @@
// Copyright 2017 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "AudioCommon/AudioStretcher.h"
#include <algorithm>
#include <cmath>
#include <cstddef>
#include "Common/Logging/Log.h"
#include "Core/Config/MainSettings.h"
namespace AudioCommon
{
AudioStretcher::AudioStretcher(unsigned int sample_rate) : m_sample_rate(sample_rate)
{
m_sound_touch.setChannels(2);
m_sound_touch.setSampleRate(sample_rate);
m_sound_touch.setPitch(1.0);
m_sound_touch.setTempo(1.0);
}
void AudioStretcher::Clear()
{
m_sound_touch.clear();
}
void AudioStretcher::ProcessSamples(const short* in, unsigned int num_in, unsigned int num_out)
{
const double time_delta = static_cast<double>(num_out) / m_sample_rate; // seconds
// We were given actual_samples number of samples, and num_samples were requested from us.
double current_ratio = static_cast<double>(num_in) / static_cast<double>(num_out);
const double max_latency = Config::Get(Config::MAIN_AUDIO_STRETCH_LATENCY);
const double max_backlog = m_sample_rate * max_latency / 1000.0 / m_stretch_ratio;
const double backlog_fullness = m_sound_touch.numSamples() / max_backlog;
if (backlog_fullness > 5.0)
{
// Too many samples in backlog: Don't push anymore on
num_in = 0;
}
// We ideally want the backlog to be about 50% full.
// This gives some headroom both ways to prevent underflow and overflow.
// We tweak current_ratio to encourage this.
constexpr double tweak_time_scale = 0.5; // seconds
current_ratio *= 1.0 + 2.0 * (backlog_fullness - 0.5) * (time_delta / tweak_time_scale);
// This low-pass filter smoothes out variance in the calculated stretch ratio.
// The time-scale determines how responsive this filter is.
constexpr double lpf_time_scale = 1.0; // seconds
const double lpf_gain = 1.0 - std::exp(-time_delta / lpf_time_scale);
m_stretch_ratio += lpf_gain * (current_ratio - m_stretch_ratio);
// Place a lower limit of 10% speed. When a game boots up, there will be
// many silence samples. These do not need to be timestretched.
m_stretch_ratio = std::max(m_stretch_ratio, 0.1);
m_sound_touch.setTempo(m_stretch_ratio);
DEBUG_LOG_FMT(AUDIO, "Audio stretching: samples:{}/{} ratio:{} backlog:{} gain: {}", num_in,
num_out, m_stretch_ratio, backlog_fullness, lpf_gain);
m_sound_touch.putSamples(in, num_in);
}
void AudioStretcher::GetStretchedSamples(short* out, unsigned int num_out)
{
const size_t samples_received = m_sound_touch.receiveSamples(out, num_out);
if (samples_received != 0)
{
m_last_stretched_sample[0] = out[samples_received * 2 - 2];
m_last_stretched_sample[1] = out[samples_received * 2 - 1];
}
// Perform padding if we've run out of samples.
for (size_t i = samples_received; i < num_out; i++)
{
out[i * 2 + 0] = m_last_stretched_sample[0];
out[i * 2 + 1] = m_last_stretched_sample[1];
}
}
} // namespace AudioCommon
-27
View File
@@ -1,27 +0,0 @@
// Copyright 2017 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <SoundTouch.h>
namespace AudioCommon
{
class AudioStretcher
{
public:
explicit AudioStretcher(unsigned int sample_rate);
void ProcessSamples(const short* in, unsigned int num_in, unsigned int num_out);
void GetStretchedSamples(short* out, unsigned int num_out);
void Clear();
private:
unsigned int m_sample_rate;
std::array<short, 2> m_last_stretched_sample = {};
soundtouch::SoundTouch m_sound_touch;
double m_stretch_ratio = 1.0;
};
} // namespace AudioCommon
-3
View File
@@ -1,8 +1,6 @@
add_library(audiocommon
AudioCommon.cpp
AudioCommon.h
AudioStretcher.cpp
AudioStretcher.h
CubebStream.h
Enums.h
Mixer.cpp
@@ -90,7 +88,6 @@ PUBLIC
common
PRIVATE
SoundTouch
FreeSurround)
if(ENABLE_CUBEB)
File diff suppressed because it is too large Load Diff
+103 -44
View File
@@ -3,10 +3,12 @@
#pragma once
#include <algorithm>
#include <array>
#include <atomic>
#include <bit>
#include <cmath>
#include "AudioCommon/AudioStretcher.h"
#include "AudioCommon/SurroundDecoder.h"
#include "AudioCommon/WaveFile.h"
#include "Common/CommonTypes.h"
@@ -17,32 +19,32 @@ class PointerWrap;
class Mixer final
{
public:
explicit Mixer(unsigned int BackendSampleRate);
explicit Mixer(u32 BackendSampleRate);
~Mixer();
void DoState(PointerWrap& p);
// Called from audio threads
unsigned int Mix(short* samples, unsigned int numSamples);
unsigned int MixSurround(float* samples, unsigned int num_samples);
std::size_t Mix(s16* samples, std::size_t numSamples);
std::size_t MixSurround(float* samples, std::size_t num_samples);
// Called from main thread
void PushSamples(const short* samples, unsigned int num_samples);
void PushStreamingSamples(const short* samples, unsigned int num_samples);
void PushWiimoteSpeakerSamples(const short* samples, unsigned int num_samples,
unsigned int sample_rate_divisor);
void PushSkylanderPortalSamples(const u8* samples, unsigned int num_samples);
void PushGBASamples(int device_number, const short* samples, unsigned int num_samples);
void PushSamples(const s16* samples, std::size_t num_samples);
void PushStreamingSamples(const s16* samples, std::size_t num_samples);
void PushWiimoteSpeakerSamples(const s16* samples, std::size_t num_samples,
u32 sample_rate_divisor);
void PushSkylanderPortalSamples(const u8* samples, std::size_t num_samples);
void PushGBASamples(std::size_t device_number, const s16* samples, std::size_t num_samples);
unsigned int GetSampleRate() const { return m_sampleRate; }
u32 GetSampleRate() const { return m_output_sample_rate; }
void SetDMAInputSampleRateDivisor(unsigned int rate_divisor);
void SetStreamInputSampleRateDivisor(unsigned int rate_divisor);
void SetGBAInputSampleRateDivisors(int device_number, unsigned int rate_divisor);
void SetDMAInputSampleRateDivisor(u32 rate_divisor);
void SetStreamInputSampleRateDivisor(u32 rate_divisor);
void SetGBAInputSampleRateDivisors(std::size_t device_number, u32 rate_divisor);
void SetStreamingVolume(unsigned int lvolume, unsigned int rvolume);
void SetWiimoteSpeakerVolume(unsigned int lvolume, unsigned int rvolume);
void SetGBAVolume(int device_number, unsigned int lvolume, unsigned int rvolume);
void SetStreamingVolume(u32 lvolume, u32 rvolume);
void SetWiimoteSpeakerVolume(u32 lvolume, u32 rvolume);
void SetGBAVolume(std::size_t device_number, u32 lvolume, u32 rvolume);
void StartLogDTKAudio(const std::string& filename);
void StopLogDTKAudio();
@@ -54,44 +56,105 @@ public:
static constexpr u64 FIXED_SAMPLE_RATE_DIVIDEND = 54000000 * 2;
private:
static constexpr u32 MAX_SAMPLES = 1024 * 4; // 128 ms
static constexpr u32 INDEX_MASK = MAX_SAMPLES * 2 - 1;
static constexpr int MAX_FREQ_SHIFT = 200; // Per 32000 Hz
static constexpr float CONTROL_FACTOR = 0.2f;
static constexpr u32 CONTROL_AVG = 32; // In freq_shift per FIFO size offset
const unsigned int SURROUND_CHANNELS = 6;
const std::size_t SURROUND_CHANNELS = 6;
class MixerFifo final
{
static constexpr std::size_t GRANULE_QUEUE_SIZE = 20;
template <typename T>
static s16 ToShort(const T x)
{
return static_cast<s16>(std::clamp<T>(x, static_cast<T>(std::numeric_limits<s16>::min()),
static_cast<T>(std::numeric_limits<s16>::max())));
}
struct StereoPair final
{
float l = 0.f;
float r = 0.f;
constexpr StereoPair() = default;
constexpr explicit StereoPair(float mono) : l(mono), r(mono) {}
constexpr StereoPair(float left, float right) : l(left), r(right) {}
constexpr StereoPair(s16 left, s16 right) : l(left), r(right) {}
StereoPair operator+(const StereoPair& other) const
{
return StereoPair(l + other.l, r + other.r);
}
StereoPair& operator*=(const StereoPair& other)
{
l *= other.l;
r *= other.r;
return *this;
}
};
static constexpr std::size_t GRANULE_BUFFER_SIZE = 256;
static constexpr std::size_t GRANULE_BUFFER_MASK = GRANULE_BUFFER_SIZE - 1;
static constexpr std::size_t GRANULE_BUFFER_BITS = std::countr_one(GRANULE_BUFFER_MASK);
static constexpr std::size_t GRANULE_BUFFER_FRAC_BITS = 32 - GRANULE_BUFFER_BITS;
using GranuleBuffer = std::array<StereoPair, GRANULE_BUFFER_SIZE>;
class Granule final
{
public:
constexpr Granule() = default;
constexpr Granule(const GranuleBuffer& input, std::size_t start_index);
static StereoPair InterpStereoPair(const Granule& front, const Granule& back, u32 frac);
Granule& operator*=(const StereoPair& x)
{
for (auto& sample : m_buffer)
sample *= x;
return *this;
}
private:
GranuleBuffer m_buffer{};
};
public:
MixerFifo(Mixer* mixer, unsigned sample_rate_divisor, bool little_endian)
MixerFifo(Mixer* mixer, u32 sample_rate_divisor, bool little_endian)
: m_mixer(mixer), m_input_sample_rate_divisor(sample_rate_divisor),
m_little_endian(little_endian)
{
}
void DoState(PointerWrap& p);
void PushSamples(const short* samples, unsigned int num_samples);
unsigned int Mix(short* samples, unsigned int numSamples, bool consider_framelimit,
float emulationspeed, int timing_variance);
void SetInputSampleRateDivisor(unsigned int rate_divisor);
unsigned int GetInputSampleRateDivisor() const;
void SetVolume(unsigned int lvolume, unsigned int rvolume);
void PushSamples(const s16* samples, std::size_t num_samples);
void Mix(s16* samples, std::size_t num_samples);
void SetInputSampleRateDivisor(u32 rate_divisor);
u32 GetInputSampleRateDivisor() const;
void SetVolume(u32 lvolume, u32 rvolume);
std::pair<s32, s32> GetVolume() const;
unsigned int AvailableSamples() const;
private:
Mixer* m_mixer;
unsigned m_input_sample_rate_divisor;
u32 m_input_sample_rate_divisor;
bool m_little_endian;
std::array<short, MAX_SAMPLES * 2> m_buffer{};
std::atomic<u32> m_indexW{0};
std::atomic<u32> m_indexR{0};
std::size_t m_buffer_index = 0;
GranuleBuffer m_buffer{};
u32 m_current_index = 0;
Granule m_front, m_back;
std::array<Granule, GRANULE_QUEUE_SIZE> m_queue;
std::atomic<std::size_t> m_queue_head{0};
std::atomic<std::size_t> m_queue_tail{0};
std::atomic<bool> m_queue_looping{false};
std::size_t m_queue_fade_index = 0;
void Enqueue(const Granule& granule);
void Dequeue(Granule* granule);
// Volume ranges from 0-256
std::atomic<s32> m_LVolume{256};
std::atomic<s32> m_RVolume{256};
float m_numLeftI = 0.0f;
u32 m_frac = 0;
StereoPair m_quantization_error;
};
void RefreshConfig();
@@ -104,12 +167,9 @@ private:
MixerFifo{this, FIXED_SAMPLE_RATE_DIVIDEND / 48000, true},
MixerFifo{this, FIXED_SAMPLE_RATE_DIVIDEND / 48000, true},
MixerFifo{this, FIXED_SAMPLE_RATE_DIVIDEND / 48000, true}};
unsigned int m_sampleRate;
u32 m_output_sample_rate;
bool m_is_stretching = false;
AudioCommon::AudioStretcher m_stretcher;
AudioCommon::SurroundDecoder m_surround_decoder;
std::array<short, MAX_SAMPLES * 2> m_scratch_buffer{};
WaveFileWriter m_wave_writer_dtk;
WaveFileWriter m_wave_writer_dsp;
@@ -118,8 +178,7 @@ private:
bool m_log_dsp_audio = false;
float m_config_emulation_speed;
int m_config_timing_variance;
bool m_config_audio_stretch;
bool m_audio_fill_gaps = true;
Config::ConfigChangedCallbackID m_config_changed_callback_id;
};
+2 -2
View File
@@ -293,7 +293,7 @@ void OpenALStream::SoundLoop()
if (use_surround)
{
std::array<float, OAL_MAX_FRAMES * SURROUND_CHANNELS> dpl2;
u32 rendered_frames = m_mixer->MixSurround(dpl2.data(), min_frames);
u32 rendered_frames = static_cast<u32>(m_mixer->MixSurround(dpl2.data(), min_frames));
if (rendered_frames < min_frames)
continue;
@@ -351,7 +351,7 @@ void OpenALStream::SoundLoop()
}
else
{
u32 rendered_frames = m_mixer->Mix(m_realtime_buffer.data(), min_frames);
u32 rendered_frames = static_cast<u32>(m_mixer->Mix(m_realtime_buffer.data(), min_frames));
if (!rendered_frames)
continue;
+1 -2
View File
@@ -56,8 +56,7 @@ const Info<bool> MAIN_DPL2_DECODER{{System::Main, "Core", "DPL2Decoder"}, false}
const Info<AudioCommon::DPL2Quality> MAIN_DPL2_QUALITY{{System::Main, "Core", "DPL2Quality"},
AudioCommon::GetDefaultDPL2Quality()};
const Info<int> MAIN_AUDIO_LATENCY{{System::Main, "Core", "AudioLatency"}, 20};
const Info<bool> MAIN_AUDIO_STRETCH{{System::Main, "Core", "AudioStretch"}, false};
const Info<int> MAIN_AUDIO_STRETCH_LATENCY{{System::Main, "Core", "AudioStretchMaxLatency"}, 80};
const Info<bool> MAIN_AUDIO_FILL_GAPS{{System::Main, "Core", "AudioFillGaps"}, true};
const Info<std::string> MAIN_MEMCARD_A_PATH{{System::Main, "Core", "MemcardAPath"}, ""};
const Info<std::string> MAIN_MEMCARD_B_PATH{{System::Main, "Core", "MemcardBPath"}, ""};
const Info<std::string>& GetInfoForMemcardPath(ExpansionInterface::Slot slot)
+1 -2
View File
@@ -72,8 +72,7 @@ extern const Info<bool> MAIN_OVERRIDE_REGION_SETTINGS;
extern const Info<bool> MAIN_DPL2_DECODER;
extern const Info<AudioCommon::DPL2Quality> MAIN_DPL2_QUALITY;
extern const Info<int> MAIN_AUDIO_LATENCY;
extern const Info<bool> MAIN_AUDIO_STRETCH;
extern const Info<int> MAIN_AUDIO_STRETCH_LATENCY;
extern const Info<bool> MAIN_AUDIO_FILL_GAPS;
extern const Info<std::string> MAIN_MEMCARD_A_PATH;
extern const Info<std::string> MAIN_MEMCARD_B_PATH;
const Info<std::string>& GetInfoForMemcardPath(ExpansionInterface::Slot slot);
-2
View File
@@ -2,7 +2,6 @@
<Project>
<ItemGroup>
<ClInclude Include="AudioCommon\AudioCommon.h" />
<ClInclude Include="AudioCommon\AudioStretcher.h" />
<ClInclude Include="AudioCommon\CubebStream.h" />
<ClInclude Include="AudioCommon\CubebUtils.h" />
<ClInclude Include="AudioCommon\Enums.h" />
@@ -769,7 +768,6 @@
</ItemGroup>
<ItemGroup>
<ClCompile Include="AudioCommon\AudioCommon.cpp" />
<ClCompile Include="AudioCommon\AudioStretcher.cpp" />
<ClCompile Include="AudioCommon\CubebStream.cpp" />
<ClCompile Include="AudioCommon\CubebUtils.cpp" />
<ClCompile Include="AudioCommon\Mixer.cpp" />
-1
View File
@@ -56,7 +56,6 @@
<Import Project="$(ExternalsDir)rcheevos\exports.props" />
<Import Project="$(ExternalsDir)SDL\exports.props" />
<Import Project="$(ExternalsDir)SFML\exports.props" />
<Import Project="$(ExternalsDir)soundtouch\exports.props" />
<Import Project="$(ExternalsDir)spirv_cross\exports.props" />
<Import Project="$(ExternalsDir)tinygltf\exports.props" />
<Import Project="$(ExternalsDir)xxhash\exports.props" />
-1
View File
@@ -482,7 +482,6 @@
<Import Project="$(ExternalsDir)picojson\exports.props" />
<Import Project="$(ExternalsDir)rcheevos\exports.props" />
<Import Project="$(ExternalsDir)SFML\exports.props" />
<Import Project="$(ExternalsDir)soundtouch\exports.props" />
<Import Project="$(ExternalsDir)xxhash\exports.props" />
<Import Project="$(ExternalsDir)zstd\exports.props" />
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
+12 -47
View File
@@ -25,6 +25,7 @@
#include "Core/Core.h"
#include "Core/System.h"
#include "DolphinQt/Config/ConfigControls/ConfigBool.h"
#include "DolphinQt/Config/SettingsWindow.h"
#include "DolphinQt/Settings.h"
@@ -137,43 +138,29 @@ void AudioPane::CreateWidgets()
backend_layout->addRow(dolby_quality_layout);
backend_layout->addRow(m_dolby_quality_latency_label);
auto* stretching_box = new QGroupBox(tr("Audio Stretching Settings"));
auto* stretching_layout = new QGridLayout;
m_stretching_enable = new QCheckBox(tr("Enable Audio Stretching"));
m_stretching_buffer_slider = new QSlider(Qt::Horizontal);
m_stretching_buffer_indicator = new QLabel();
m_stretching_buffer_label = new QLabel(tr("Buffer Size:"));
stretching_box->setLayout(stretching_layout);
m_stretching_buffer_slider->setMinimum(5);
m_stretching_buffer_slider->setMaximum(300);
m_stretching_enable->setToolTip(tr("Enables stretching of the audio to match emulation speed."));
m_stretching_buffer_slider->setToolTip(tr("Size of stretch buffer in milliseconds. "
"Values too low may cause audio crackling."));
stretching_layout->addWidget(m_stretching_enable, 0, 0, 1, -1);
stretching_layout->addWidget(m_stretching_buffer_label, 1, 0);
stretching_layout->addWidget(m_stretching_buffer_slider, 1, 1);
stretching_layout->addWidget(m_stretching_buffer_indicator, 1, 2);
dsp_box->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
auto* misc_box = new QGroupBox(tr("Miscellaneous Settings"));
auto* misc_layout = new QGridLayout;
misc_box->setLayout(misc_layout);
m_speed_up_mute_enable = new QCheckBox(tr("Mute When Disabling Speed Limit"));
m_speed_up_mute_enable->setToolTip(
m_audio_fill_gaps = new ConfigBool(tr("Fill Audio Gaps"), Config::MAIN_AUDIO_FILL_GAPS);
m_audio_fill_gaps->SetDescription(
tr("Repeat existing audio during lag spikes to prevent stuttering."
"<br><br><dolphin_emphasis>If unsure, leave this checked.</dolphin_emphasis>"));
m_speed_up_mute_enable = new ConfigBool(tr("Mute When Disabling Speed Limit"),
Config::MAIN_AUDIO_MUTE_ON_DISABLED_SPEED_LIMIT);
m_speed_up_mute_enable->SetDescription(
tr("Mutes the audio when overriding the emulation speed limit (default hotkey: Tab)."));
misc_layout->addWidget(m_speed_up_mute_enable, 0, 0, 1, 1);
misc_layout->addWidget(m_audio_fill_gaps, 0, 0, 1, 1);
misc_layout->addWidget(m_speed_up_mute_enable, 1, 0, 1, 1);
auto* const main_vbox_layout = new QVBoxLayout;
main_vbox_layout->addWidget(dsp_box);
main_vbox_layout->addWidget(backend_box);
main_vbox_layout->addWidget(stretching_box);
main_vbox_layout->addWidget(misc_box);
m_main_layout = new QHBoxLayout;
@@ -192,14 +179,11 @@ void AudioPane::ConnectWidgets()
{
connect(m_latency_spin, &QSpinBox::valueChanged, this, &AudioPane::SaveSettings);
}
connect(m_stretching_buffer_slider, &QSlider::valueChanged, this, &AudioPane::SaveSettings);
connect(m_dolby_pro_logic, &QCheckBox::toggled, this, &AudioPane::SaveSettings);
connect(m_dolby_quality_slider, &QSlider::valueChanged, this, &AudioPane::SaveSettings);
connect(m_stretching_enable, &QCheckBox::toggled, this, &AudioPane::SaveSettings);
connect(m_dsp_hle, &QRadioButton::toggled, this, &AudioPane::SaveSettings);
connect(m_dsp_lle, &QRadioButton::toggled, this, &AudioPane::SaveSettings);
connect(m_dsp_interpreter, &QRadioButton::toggled, this, &AudioPane::SaveSettings);
connect(m_speed_up_mute_enable, &QCheckBox::toggled, this, &AudioPane::SaveSettings);
#ifdef _WIN32
connect(m_wasapi_device_combo, &QComboBox::currentIndexChanged, this, &AudioPane::SaveSettings);
@@ -255,17 +239,6 @@ void AudioPane::LoadSettings()
if (m_latency_control_supported)
m_latency_spin->setValue(Config::Get(Config::MAIN_AUDIO_LATENCY));
// Stretch
m_stretching_enable->setChecked(Config::Get(Config::MAIN_AUDIO_STRETCH));
m_stretching_buffer_label->setEnabled(m_stretching_enable->isChecked());
m_stretching_buffer_slider->setValue(Config::Get(Config::MAIN_AUDIO_STRETCH_LATENCY));
m_stretching_buffer_slider->setEnabled(m_stretching_enable->isChecked());
m_stretching_buffer_indicator->setEnabled(m_stretching_enable->isChecked());
m_stretching_buffer_indicator->setText(tr("%1 ms").arg(m_stretching_buffer_slider->value()));
// Misc
m_speed_up_mute_enable->setChecked(Config::Get(Config::MAIN_AUDIO_MUTE_ON_DISABLED_SPEED_LIMIT));
#ifdef _WIN32
if (Config::Get(Config::MAIN_WASAPI_DEVICE) == "default")
{
@@ -326,16 +299,8 @@ void AudioPane::SaveSettings()
if (m_latency_control_supported)
Config::SetBaseOrCurrent(Config::MAIN_AUDIO_LATENCY, m_latency_spin->value());
// Stretch
Config::SetBaseOrCurrent(Config::MAIN_AUDIO_STRETCH, m_stretching_enable->isChecked());
Config::SetBaseOrCurrent(Config::MAIN_AUDIO_STRETCH_LATENCY, m_stretching_buffer_slider->value());
m_stretching_buffer_label->setEnabled(m_stretching_enable->isChecked());
m_stretching_buffer_slider->setEnabled(m_stretching_enable->isChecked());
m_stretching_buffer_indicator->setEnabled(m_stretching_enable->isChecked());
m_stretching_buffer_indicator->setText(
tr("%1 ms").arg(Config::Get(Config::MAIN_AUDIO_STRETCH_LATENCY)));
// Misc
Config::SetBaseOrCurrent(Config::MAIN_AUDIO_FILL_GAPS, m_audio_fill_gaps->isChecked());
Config::SetBaseOrCurrent(Config::MAIN_AUDIO_MUTE_ON_DISABLED_SPEED_LIMIT,
m_speed_up_mute_enable->isChecked());
+3 -7
View File
@@ -18,6 +18,7 @@ class QRadioButton;
class QSlider;
class QSpinBox;
class SettingsWindow;
class ConfigBool;
class AudioPane final : public QWidget
{
@@ -71,12 +72,7 @@ private:
QComboBox* m_wasapi_device_combo;
#endif
// Audio Stretching
QCheckBox* m_stretching_enable;
QLabel* m_stretching_buffer_label;
QSlider* m_stretching_buffer_slider;
QLabel* m_stretching_buffer_indicator;
// Misc Settings
QCheckBox* m_speed_up_mute_enable;
ConfigBool* m_audio_fill_gaps;
ConfigBool* m_speed_up_mute_enable;
};