Skip to content

Instantly share code, notes, and snippets.

@ITotalJustice
Last active July 11, 2025 02:19
Show Gist options
  • Save ITotalJustice/2bd64ffa76d58104dc975a2948a48932 to your computer and use it in GitHub Desktop.
Save ITotalJustice/2bd64ffa76d58104dc975a2948a48932 to your computer and use it in GitHub Desktop.
dolphin mixer standalone
// Copyright 2008 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// based on: https://github.com/dolphin-emu/dolphin/blob/8d2a15be3f8f9e3f5bc1620cf11603b2a910bd56/Source/Core/AudioCommon/Mixer.cpp
#include "Mixer.h"
#include <algorithm>
#include <array>
#include <bit>
#include <cmath>
#include <cstring>
#include <limits>
namespace dolphin::audio {
namespace {
// define these if you need log output
#define INFO_LOG_FMT(type, ...)
#define WARN_LOG_FMT(type, ...)
namespace MathUtil
{
// Casts the specified value to a Dest. The value will be clamped to fit in the destination type.
// Warning: The result of SaturatingCast(NaN) is undefined.
template <typename Dest, typename T>
constexpr Dest SaturatingCast(T value)
{
static_assert(std::is_integral<Dest>());
[[maybe_unused]] constexpr Dest lo = std::numeric_limits<Dest>::lowest();
constexpr Dest hi = std::numeric_limits<Dest>::max();
// T being a signed integer and Dest unsigned is a problematic case because the value will
// be converted into an unsigned integer, and u32(...) < 0 is always false.
if constexpr (std::is_integral<T>() && std::is_signed<T>() && std::is_unsigned<Dest>())
{
static_assert(lo == 0);
if (value < 0)
return lo;
// Now that we got rid of negative values, we can safely cast value to an unsigned T
// since unsigned T can represent any positive value signed T could represent.
// The compiler will then promote the LHS or the RHS if necessary.
if (std::make_unsigned_t<T>(value) > hi)
return hi;
}
else if constexpr (std::is_integral<T>() && std::is_unsigned<T>() && std::is_signed<Dest>())
{
// value and hi will never be negative, and hi is representable as an unsigned Dest.
if (value > std::make_unsigned_t<Dest>(hi))
return hi;
}
else
{
// Do not use std::clamp or a similar function here to avoid overflow.
// For example, if Dest = s64 and T = int, we want integer promotion to convert value to a s64
// instead of changing lo or hi into an int.
if (value < lo)
return lo;
if (value > hi)
return hi;
}
return static_cast<Dest>(value);
}
} // namespace MathUtil
} // namespace
Mixer::Mixer(u32 BackendSampleRate, float emulation_speed, bool fill_audio_gaps, int audio_buffer_ms, bool little_endian)
: m_fifo_mixer{this, static_cast<u32>(FIXED_SAMPLE_RATE_DIVIDEND / BackendSampleRate), little_endian}
, m_output_sample_rate(BackendSampleRate)
{
SetEmulationSpeed(emulation_speed);
SetFillAudioGaps(fill_audio_gaps);
SetAudioBufferMs(audio_buffer_ms);
INFO_LOG_FMT(AUDIO_INTERFACE, "Mixer is initialized");
}
// Executed from sound stream thread
void Mixer::MixerFifo::Mix(s16* samples, std::size_t num_samples)
{
constexpr u32 INDEX_HALF = 0x80000000;
constexpr DT_s FADE_IN_RC = DT_s(0.008);
constexpr DT_s FADE_OUT_RC = DT_s(0.064);
// We need at least a double because the index jump has 24 bits of fractional precision.
const double out_sample_rate = m_mixer->m_output_sample_rate;
double in_sample_rate =
static_cast<double>(FIXED_SAMPLE_RATE_DIVIDEND) / m_input_sample_rate_divisor;
const double emulation_speed = m_mixer->m_config_emulation_speed;
if (0 < emulation_speed && emulation_speed != 1.0)
in_sample_rate *= emulation_speed;
const double base = static_cast<double>(1 << GRANULE_FRAC_BITS);
const u32 index_jump = std::lround(base * in_sample_rate / out_sample_rate);
// These fade in / out multiplier are tuned to match a constant
// fade speed regardless of the input or the output sample rate.
const float fade_in_mul = -std::expm1(-DT_s(1.0) / (out_sample_rate * FADE_IN_RC));
const float fade_out_mul = -std::expm1(-DT_s(1.0) / (out_sample_rate * FADE_OUT_RC));
const StereoPair volume{m_LVolume.load() / 256.0f, m_RVolume.load() / 256.0f};
// Calculate the ideal length of the granule queue.
const std::size_t buffer_size_ms = m_mixer->m_config_audio_buffer_ms;
const std::size_t buffer_size_samples = std::llround(buffer_size_ms * in_sample_rate / 1000.0);
// Limit the possible queue sizes to any number between 4 and 64.
const std::size_t buffer_size_granules =
std::clamp((buffer_size_samples) / (GRANULE_SIZE >> 1), static_cast<std::size_t>(4),
static_cast<std::size_t>(MAX_GRANULE_QUEUE_SIZE));
m_granule_queue_size.store(buffer_size_granules, std::memory_order_relaxed);
while (num_samples-- > 0)
{
// The indexes for the front and back buffers are offset by 50% of the granule size.
// We use the modular nature of 32-bit integers to wrap around the granule size.
m_current_index += index_jump;
const u32 front_index = m_current_index;
const u32 back_index = m_current_index + INDEX_HALF;
// If either index is less than the index jump, that means we reached
// the end of the of the buffer and need to load the next granule.
if (front_index < index_jump)
Dequeue(&m_front);
else if (back_index < index_jump)
Dequeue(&m_back);
// The Granules are pre-windowed, so we can just add them together
const std::size_t ft = front_index >> GRANULE_FRAC_BITS;
const std::size_t bt = back_index >> GRANULE_FRAC_BITS;
const StereoPair s0 = m_front[(ft - 2) & GRANULE_MASK] + m_back[(bt - 2) & GRANULE_MASK];
const StereoPair s1 = m_front[(ft - 1) & GRANULE_MASK] + m_back[(bt - 1) & GRANULE_MASK];
const StereoPair s2 = m_front[(ft + 0) & GRANULE_MASK] + m_back[(bt + 0) & GRANULE_MASK];
const StereoPair s3 = m_front[(ft + 1) & GRANULE_MASK] + m_back[(bt + 1) & GRANULE_MASK];
const StereoPair s4 = m_front[(ft + 2) & GRANULE_MASK] + m_back[(bt + 2) & GRANULE_MASK];
const StereoPair s5 = m_front[(ft + 3) & GRANULE_MASK] + m_back[(bt + 3) & GRANULE_MASK];
// Polynomial Interpolators for High-Quality Resampling of
// Over Sampled Audio by Olli Niemitalo, October 2001.
// Page 43 -- 6-point, 3rd-order Hermite:
// https://yehar.com/blog/wp-content/uploads/2009/08/deip.pdf
const u32 t_frac = m_current_index & ((1 << GRANULE_FRAC_BITS) - 1);
const float t1 = t_frac / static_cast<float>(1 << GRANULE_FRAC_BITS);
const float t2 = t1 * t1;
const float t3 = t2 * t1;
StereoPair sample = (s0 * StereoPair{(+0.0f + 1.0f * t1 - 2.0f * t2 + 1.0f * t3) / 12.0f} +
s1 * StereoPair{(+0.0f - 8.0f * t1 + 15.0f * t2 - 7.0f * t3) / 12.0f} +
s2 * StereoPair{(+3.0f + 0.0f * t1 - 7.0f * t2 + 4.0f * t3) / 3.0f} +
s3 * StereoPair{(+0.0f + 2.0f * t1 + 5.0f * t2 - 4.0f * t3) / 3.0f} +
s4 * StereoPair{(+0.0f - 1.0f * t1 - 6.0f * t2 + 7.0f * t3) / 12.0f} +
s5 * StereoPair{(+0.0f + 0.0f * t1 + 1.0f * t2 - 1.0f * t3) / 12.0f});
// Apply Fade In / Fade Out depending on if we are looping
if (m_queue_looping.load(std::memory_order_relaxed))
m_fade_volume += fade_out_mul * (0.0f - m_fade_volume);
else
m_fade_volume += fade_in_mul * (1.0f - m_fade_volume);
// Apply the fade volume and the regular volume to the sample
sample = sample * volume * StereoPair{m_fade_volume};
// This quantization method prevents accumulated error but does not do noise shaping.
sample.l += samples[0] - m_quantization_error.l;
samples[0] = MathUtil::SaturatingCast<s16>(std::lround(sample.l));
m_quantization_error.l = std::clamp(samples[0] - sample.l, -1.0f, 1.0f);
sample.r += samples[1] - m_quantization_error.r;
samples[1] = MathUtil::SaturatingCast<s16>(std::lround(sample.r));
m_quantization_error.r = std::clamp(samples[1] - sample.r, -1.0f, 1.0f);
samples += 2;
}
}
std::size_t Mixer::Mix(s16* samples, std::size_t num_samples)
{
if (!samples)
return 0;
memset(samples, 0, num_samples * 2 * sizeof(s16));
m_fifo_mixer.Mix(samples, num_samples);
return num_samples;
}
void Mixer::MixerFifo::PushSamples(const s16* samples, std::size_t num_samples)
{
while (num_samples-- > 0)
{
const s16 l = m_little_endian ? samples[1] : std::byteswap(samples[1]);
const s16 r = m_little_endian ? samples[0] : std::byteswap(samples[0]);
samples += 2;
m_next_buffer[m_next_buffer_index] = StereoPair(l, r);
m_next_buffer_index = (m_next_buffer_index + 1) & GRANULE_MASK;
// The granules overlap by 50%, so we need to enqueue the
// next buffer every time we fill half of the samples.
if (m_next_buffer_index == 0 || m_next_buffer_index == m_next_buffer.size() / 2)
Enqueue();
}
}
void Mixer::PushSamples(const s16* samples, std::size_t num_samples)
{
m_fifo_mixer.PushSamples(samples, num_samples);
}
void Mixer::SetInputSampleRateDivisor(u32 rate_divisor)
{
m_fifo_mixer.SetInputSampleRateDivisor(rate_divisor);
}
void Mixer::SetVolume(u32 lvolume, u32 rvolume)
{
m_fifo_mixer.SetVolume(std::clamp<u32>(lvolume, 0x00, 0xff),
std::clamp<u32>(rvolume, 0x00, 0xff));
}
void Mixer::SetRunning(bool enable)
{
m_running = enable;
}
void Mixer::SetEmulationSpeed(float emulation_speed)
{
m_config_emulation_speed = emulation_speed;
}
void Mixer::SetFillAudioGaps(bool fill_audio_gaps)
{
m_config_fill_audio_gaps = fill_audio_gaps;
}
void Mixer::SetAudioBufferMs(int audio_buffer_ms)
{
m_config_audio_buffer_ms = audio_buffer_ms;
}
void Mixer::MixerFifo::SetInputSampleRateDivisor(u32 rate_divisor)
{
m_input_sample_rate_divisor = rate_divisor;
}
u32 Mixer::MixerFifo::GetInputSampleRateDivisor() const
{
return m_input_sample_rate_divisor;
}
void Mixer::MixerFifo::SetVolume(u32 lvolume, u32 rvolume)
{
m_LVolume.store(lvolume + (lvolume >> 7));
m_RVolume.store(rvolume + (rvolume >> 7));
}
std::pair<s32, s32> Mixer::MixerFifo::GetVolume() const
{
return std::make_pair(m_LVolume.load(), m_RVolume.load());
}
void Mixer::MixerFifo::Enqueue()
{
// import numpy as np
// import scipy.signal as signal
// window = np.convolve(np.ones(128), signal.windows.dpss(128 + 1, 4))
// window /= (window[:len(window) // 2] + window[len(window) // 2:]).max()
// elements = ", ".join([f"{x:.10f}f" for x in window])
// print(f'constexpr std::array<StereoPair, GRANULE_SIZE> GRANULE_WINDOW = {{ {elements}
// }};')
static constexpr std::array<StereoPair, GRANULE_SIZE> GRANULE_WINDOW = {
0.0000016272f, 0.0000050749f, 0.0000113187f, 0.0000216492f, 0.0000377350f, 0.0000616906f,
0.0000961509f, 0.0001443499f, 0.0002102045f, 0.0002984010f, 0.0004144844f, 0.0005649486f,
0.0007573262f, 0.0010002765f, 0.0013036694f, 0.0016786636f, 0.0021377783f, 0.0026949534f,
0.0033656000f, 0.0041666352f, 0.0051165029f, 0.0062351752f, 0.0075441359f, 0.0090663409f,
0.0108261579f, 0.0128492811f, 0.0151626215f, 0.0177941726f, 0.0207728499f, 0.0241283062f,
0.0278907219f, 0.0320905724f, 0.0367583739f, 0.0419244083f, 0.0476184323f, 0.0538693708f,
0.0607049996f, 0.0681516192f, 0.0762337261f, 0.0849736833f, 0.0943913952f, 0.1045039915f,
0.1153255250f, 0.1268666867f, 0.1391345431f, 0.1521323012f, 0.1658591025f, 0.1803098534f,
0.1954750915f, 0.2113408944f, 0.2278888303f, 0.2450959552f, 0.2629348550f, 0.2813737361f,
0.3003765625f, 0.3199032396f, 0.3399098438f, 0.3603488941f, 0.3811696664f, 0.4023185434f,
0.4237393998f, 0.4453740162f, 0.4671625177f, 0.4890438330f, 0.5109561670f, 0.5328374823f,
0.5546259838f, 0.5762606002f, 0.5976814566f, 0.6188303336f, 0.6396511059f, 0.6600901562f,
0.6800967604f, 0.6996234375f, 0.7186262639f, 0.7370651450f, 0.7549040448f, 0.7721111697f,
0.7886591056f, 0.8045249085f, 0.8196901466f, 0.8341408975f, 0.8478676988f, 0.8608654569f,
0.8731333133f, 0.8846744750f, 0.8954960085f, 0.9056086048f, 0.9150263167f, 0.9237662739f,
0.9318483808f, 0.9392950004f, 0.9461306292f, 0.9523815677f, 0.9580755917f, 0.9632416261f,
0.9679094276f, 0.9721092781f, 0.9758716938f, 0.9792271501f, 0.9822058274f, 0.9848373785f,
0.9871507189f, 0.9891738421f, 0.9909336591f, 0.9924558641f, 0.9937648248f, 0.9948834971f,
0.9958333648f, 0.9966344000f, 0.9973050466f, 0.9978622217f, 0.9983213364f, 0.9986963306f,
0.9989997235f, 0.9992426738f, 0.9994350514f, 0.9995855156f, 0.9997015990f, 0.9997897955f,
0.9998556501f, 0.9999038491f, 0.9999383094f, 0.9999622650f, 0.9999783508f, 0.9999886813f,
0.9999949251f, 0.9999983728f, 0.9999983728f, 0.9999949251f, 0.9999886813f, 0.9999783508f,
0.9999622650f, 0.9999383094f, 0.9999038491f, 0.9998556501f, 0.9997897955f, 0.9997015990f,
0.9995855156f, 0.9994350514f, 0.9992426738f, 0.9989997235f, 0.9986963306f, 0.9983213364f,
0.9978622217f, 0.9973050466f, 0.9966344000f, 0.9958333648f, 0.9948834971f, 0.9937648248f,
0.9924558641f, 0.9909336591f, 0.9891738421f, 0.9871507189f, 0.9848373785f, 0.9822058274f,
0.9792271501f, 0.9758716938f, 0.9721092781f, 0.9679094276f, 0.9632416261f, 0.9580755917f,
0.9523815677f, 0.9461306292f, 0.9392950004f, 0.9318483808f, 0.9237662739f, 0.9150263167f,
0.9056086048f, 0.8954960085f, 0.8846744750f, 0.8731333133f, 0.8608654569f, 0.8478676988f,
0.8341408975f, 0.8196901466f, 0.8045249085f, 0.7886591056f, 0.7721111697f, 0.7549040448f,
0.7370651450f, 0.7186262639f, 0.6996234375f, 0.6800967604f, 0.6600901562f, 0.6396511059f,
0.6188303336f, 0.5976814566f, 0.5762606002f, 0.5546259838f, 0.5328374823f, 0.5109561670f,
0.4890438330f, 0.4671625177f, 0.4453740162f, 0.4237393998f, 0.4023185434f, 0.3811696664f,
0.3603488941f, 0.3399098438f, 0.3199032396f, 0.3003765625f, 0.2813737361f, 0.2629348550f,
0.2450959552f, 0.2278888303f, 0.2113408944f, 0.1954750915f, 0.1803098534f, 0.1658591025f,
0.1521323012f, 0.1391345431f, 0.1268666867f, 0.1153255250f, 0.1045039915f, 0.0943913952f,
0.0849736833f, 0.0762337261f, 0.0681516192f, 0.0607049996f, 0.0538693708f, 0.0476184323f,
0.0419244083f, 0.0367583739f, 0.0320905724f, 0.0278907219f, 0.0241283062f, 0.0207728499f,
0.0177941726f, 0.0151626215f, 0.0128492811f, 0.0108261579f, 0.0090663409f, 0.0075441359f,
0.0062351752f, 0.0051165029f, 0.0041666352f, 0.0033656000f, 0.0026949534f, 0.0021377783f,
0.0016786636f, 0.0013036694f, 0.0010002765f, 0.0007573262f, 0.0005649486f, 0.0004144844f,
0.0002984010f, 0.0002102045f, 0.0001443499f, 0.0000961509f, 0.0000616906f, 0.0000377350f,
0.0000216492f, 0.0000113187f, 0.0000050749f, 0.0000016272f};
std::size_t const head = m_queue_head.load(std::memory_order_acquire);
// Check if we run out of space in the circular queue. (rare)
std::size_t const next_head = (head + 1) & GRANULE_QUEUE_MASK;
if (next_head == m_queue_tail.load(std::memory_order_acquire))
{
WARN_LOG_FMT(AUDIO,
"Granule Queue has completely filled and audio samples are being dropped. "
"This should not happen unless the audio backend has stopped requesting audio.");
return;
}
// By preconstructing the granule window, we have the best chance of
// the compiler optimizing this loop using SIMD instructions.
const std::size_t start_index = m_next_buffer_index;
for (std::size_t i = 0; i < GRANULE_SIZE; ++i)
m_queue[head][i] = m_next_buffer[(i + start_index) & GRANULE_MASK] * GRANULE_WINDOW[i];
m_queue_head.store(next_head, std::memory_order_release);
m_queue_looping.store(false, std::memory_order_relaxed);
}
void Mixer::MixerFifo::Dequeue(Granule* granule)
{
const std::size_t granule_queue_size = m_granule_queue_size.load(std::memory_order_relaxed);
const std::size_t head = m_queue_head.load(std::memory_order_acquire);
std::size_t tail = m_queue_tail.load(std::memory_order_acquire);
// Checks to see if the queue has gotten too long.
if (granule_queue_size < ((head - tail) & GRANULE_QUEUE_MASK))
{
// Jump the playhead to half the queue size behind the head.
const std::size_t gap = (granule_queue_size >> 1) + 1;
tail = (head - gap) & GRANULE_QUEUE_MASK;
}
// Checks to see if the queue is empty.
std::size_t next_tail = (tail + 1) & GRANULE_QUEUE_MASK;
if (next_tail == head)
{
// Only fill gaps when running to prevent stutter on pause.
const bool is_running = m_mixer->m_running;
if (m_mixer->m_config_fill_audio_gaps && is_running)
{
// Jump the playhead to half the queue size behind the head.
// This provides smoother audio playback than suddenly stopping.
const std::size_t gap = std::max<std::size_t>(2, granule_queue_size >> 1) - 1;
next_tail = (head - gap) & GRANULE_QUEUE_MASK;
m_queue_looping.store(true, std::memory_order_relaxed);
}
else
{
std::fill(granule->begin(), granule->end(), StereoPair{0.0f, 0.0f});
m_queue_looping.store(false, std::memory_order_relaxed);
return;
}
}
*granule = m_queue[tail];
m_queue_tail.store(next_tail, std::memory_order_release);
}
} // namespace dolphin::audio
// Copyright 2009 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#ifdef _WIN32
#include <tchar.h>
#else
// For using Windows lock code
#define TCHAR char
#define LONG int
#endif
#include <array>
#include <atomic>
#include <bit>
#include <chrono>
#include <cstdint>
namespace dolphin::audio {
using u8 = std::uint8_t;
using u16 = std::uint16_t;
using u32 = std::uint32_t;
using u64 = std::uint64_t;
using s8 = std::int8_t;
using s16 = std::int16_t;
using s32 = std::int32_t;
using s64 = std::int64_t;
using DT_s = std::chrono::duration<double, std::ratio<1>>;
class Mixer final
{
public:
explicit Mixer(u32 BackendSampleRate, float emulation_speed, bool fill_audio_gaps, int audio_buffer_ms, bool little_endian);
// Called from audio threads
std::size_t Mix(s16* samples, std::size_t numSamples);
// Called from main thread
void PushSamples(const s16* samples, std::size_t num_samples);
u32 GetSampleRate() const { return m_output_sample_rate; }
void SetInputSampleRateDivisor(u32 rate_divisor);
void SetVolume(u32 lvolume, u32 rvolume);
void SetRunning(bool enable);
void SetEmulationSpeed(float emulation_speed);
void SetFillAudioGaps(bool fill_audio_gaps);
void SetAudioBufferMs(int audio_buffer_ms);
// 54000000 doesn't work here as it doesn't evenly divide with 32000, but 108000000 does
static constexpr u64 FIXED_SAMPLE_RATE_DIVIDEND = 54000000 * 2;
private:
class MixerFifo final
{
static constexpr std::size_t MAX_GRANULE_QUEUE_SIZE = 256;
static constexpr std::size_t GRANULE_QUEUE_MASK = MAX_GRANULE_QUEUE_SIZE - 1;
struct StereoPair final
{
float l = 0.f;
float r = 0.f;
constexpr StereoPair() = default;
constexpr StereoPair(const StereoPair&) = default;
constexpr StereoPair& operator=(const StereoPair&) = default;
constexpr StereoPair(StereoPair&&) = default;
constexpr StereoPair& operator=(StereoPair&&) = default;
constexpr 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) const
{
return StereoPair(l * other.l, r * other.r);
}
};
static constexpr std::size_t GRANULE_SIZE = 256;
static constexpr std::size_t GRANULE_OVERLAP = GRANULE_SIZE / 2;
static constexpr std::size_t GRANULE_MASK = GRANULE_SIZE - 1;
static constexpr std::size_t GRANULE_BITS = std::countr_one(GRANULE_MASK);
static constexpr std::size_t GRANULE_FRAC_BITS = 32 - GRANULE_BITS;
using Granule = std::array<StereoPair, GRANULE_SIZE>;
public:
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 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;
private:
Mixer* m_mixer;
u32 m_input_sample_rate_divisor;
const bool m_little_endian;
Granule m_next_buffer{};
std::size_t m_next_buffer_index = 0;
u32 m_current_index = 0;
Granule m_front, m_back;
std::atomic<std::size_t> m_granule_queue_size{20};
std::array<Granule, MAX_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};
float m_fade_volume = 1.0;
void Enqueue();
void Dequeue(Granule* granule);
// Volume ranges from 0-256
std::atomic<s32> m_LVolume{256};
std::atomic<s32> m_RVolume{256};
StereoPair m_quantization_error;
};
MixerFifo m_fifo_mixer;
u32 m_output_sample_rate;
float m_config_emulation_speed;
bool m_config_fill_audio_gaps;
int m_config_audio_buffer_ms;
bool m_running = false;
};
} // namespace dolphin::audio
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment