Last active
July 11, 2025 02:19
-
-
Save ITotalJustice/2bd64ffa76d58104dc975a2948a48932 to your computer and use it in GitHub Desktop.
dolphin mixer standalone
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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