AudioStream: Add surround expansion via FreeSurround
This commit is contained in:
@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin <stenzek@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
|
||||
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
|
||||
|
||||
#include "audio_stream.h"
|
||||
@ -6,22 +6,42 @@
|
||||
|
||||
#include "common/align.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/error.h"
|
||||
#include "common/intrin.h"
|
||||
#include "common/log.h"
|
||||
#include "common/settings_interface.h"
|
||||
#include "common/small_string.h"
|
||||
#include "common/timer.h"
|
||||
|
||||
#include "SoundTouch.h"
|
||||
|
||||
#ifndef __ANDROID__
|
||||
#include "freesurround_decoder.h"
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
|
||||
Log_SetChannel(AudioStream);
|
||||
|
||||
static constexpr bool LOG_TIMESTRETCH_STATS = false;
|
||||
|
||||
AudioStream::AudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, AudioStretchMode stretch)
|
||||
: m_sample_rate(sample_rate), m_channels(channels), m_buffer_ms(buffer_ms), m_stretch_mode(stretch)
|
||||
static constexpr const std::array<std::pair<u8, u8>, static_cast<size_t>(AudioExpansionMode::Count)>
|
||||
s_expansion_channel_count = {{
|
||||
{u8(2), u8(2)}, // Disabled
|
||||
{u8(3), u8(3)}, // StereoLFE
|
||||
{u8(5), u8(4)}, // Quadraphonic
|
||||
{u8(5), u8(5)}, // QuadraphonicLFE
|
||||
{u8(6), u8(6)}, // Surround51
|
||||
{u8(8), u8(8)}, // Surround71
|
||||
}};
|
||||
|
||||
AudioStream::AudioStream(u32 sample_rate, const AudioStreamParameters& parameters)
|
||||
: m_sample_rate(sample_rate), m_parameters(parameters),
|
||||
m_internal_channels(s_expansion_channel_count[static_cast<size_t>(parameters.expansion_mode)].first),
|
||||
m_output_channels(s_expansion_channel_count[static_cast<size_t>(parameters.expansion_mode)].second)
|
||||
{
|
||||
}
|
||||
|
||||
@ -30,13 +50,48 @@ AudioStream::~AudioStream()
|
||||
DestroyBuffer();
|
||||
}
|
||||
|
||||
std::unique_ptr<AudioStream> AudioStream::CreateNullStream(u32 sample_rate, u32 channels, u32 buffer_ms)
|
||||
std::unique_ptr<AudioStream> AudioStream::CreateNullStream(u32 sample_rate, u32 buffer_ms)
|
||||
{
|
||||
std::unique_ptr<AudioStream> stream(new AudioStream(sample_rate, channels, buffer_ms, AudioStretchMode::Off));
|
||||
stream->BaseInitialize();
|
||||
// no point stretching with no output
|
||||
AudioStreamParameters params;
|
||||
params.expansion_mode = AudioExpansionMode::Disabled;
|
||||
params.stretch_mode = AudioStretchMode::Off;
|
||||
params.buffer_ms = static_cast<u16>(buffer_ms);
|
||||
|
||||
std::unique_ptr<AudioStream> stream(new AudioStream(sample_rate, params));
|
||||
stream->BaseInitialize(&StereoSampleReaderImpl);
|
||||
return stream;
|
||||
}
|
||||
|
||||
#ifndef __ANDROID__
|
||||
|
||||
std::unique_ptr<AudioStream> AudioStream::CreateStream(AudioBackend backend, u32 sample_rate,
|
||||
const AudioStreamParameters& parameters, Error* error)
|
||||
{
|
||||
switch (backend)
|
||||
{
|
||||
case AudioBackend::Cubeb:
|
||||
return CreateCubebAudioStream(sample_rate, parameters, error);
|
||||
|
||||
case AudioBackend::SDL:
|
||||
return CreateSDLAudioStream(sample_rate, parameters, error);
|
||||
|
||||
#ifdef _WIN32
|
||||
case AudioBackend::XAudio2:
|
||||
return CreateXAudio2Stream(sample_rate, parameters, error);
|
||||
#endif
|
||||
|
||||
case AudioBackend::Null:
|
||||
return CreateNullStream(sample_rate, parameters.buffer_ms);
|
||||
|
||||
default:
|
||||
Error::SetStringView(error, "Unknown audio backend.");
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
u32 AudioStream::GetAlignedBufferSize(u32 size)
|
||||
{
|
||||
static_assert(Common::IsPow2(CHUNK_SIZE));
|
||||
@ -54,10 +109,99 @@ u32 AudioStream::GetMSForBufferSize(u32 sample_rate, u32 buffer_size)
|
||||
return (buffer_size * 1000u) / sample_rate;
|
||||
}
|
||||
|
||||
static constexpr const std::array s_stretch_mode_names = {"None", "Resample", "TimeStretch"};
|
||||
static constexpr const std::array s_stretch_mode_display_names = {TRANSLATE_NOOP("AudioStream", "None"),
|
||||
TRANSLATE_NOOP("AudioStream", "Resampling"),
|
||||
TRANSLATE_NOOP("AudioStream", "Time Stretching")};
|
||||
static constexpr const std::array s_backend_names = {
|
||||
"Null",
|
||||
#ifndef __ANDROID__
|
||||
"Cubeb",
|
||||
"SDL",
|
||||
#else
|
||||
"AAudio",
|
||||
"OpenSLES",
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
"XAudio2",
|
||||
#endif
|
||||
};
|
||||
static constexpr const std::array s_backend_display_names = {
|
||||
TRANSLATE_NOOP("AudioStream", "Null (No Output)"),
|
||||
#ifndef __ANDROID__
|
||||
TRANSLATE_NOOP("AudioStream", "Cubeb"),
|
||||
TRANSLATE_NOOP("AudioStream", "SDL"),
|
||||
#else
|
||||
"AAudio",
|
||||
"OpenSL ES",
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
TRANSLATE_NOOP("AudioStream", "XAudio2"),
|
||||
#endif
|
||||
};
|
||||
|
||||
std::optional<AudioBackend> AudioStream::ParseBackendName(const char* str)
|
||||
{
|
||||
int index = 0;
|
||||
for (const char* name : s_backend_names)
|
||||
{
|
||||
if (std::strcmp(name, str) == 0)
|
||||
return static_cast<AudioBackend>(index);
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const char* AudioStream::GetBackendName(AudioBackend backend)
|
||||
{
|
||||
return s_backend_names[static_cast<int>(backend)];
|
||||
}
|
||||
|
||||
const char* AudioStream::GetBackendDisplayName(AudioBackend backend)
|
||||
{
|
||||
return Host::TranslateToCString("AudioStream", s_backend_display_names[static_cast<int>(backend)]);
|
||||
}
|
||||
|
||||
static constexpr const std::array s_expansion_mode_names = {
|
||||
"Disabled", "StereoLFE", "Quadraphonic", "QuadraphonicLFE", "Surround51", "Surround71",
|
||||
};
|
||||
static constexpr const std::array s_expansion_mode_display_names = {
|
||||
TRANSLATE_NOOP("AudioStream", "Disabled (Stereo)"), TRANSLATE_NOOP("AudioStream", "Stereo with LFE"),
|
||||
TRANSLATE_NOOP("AudioStream", "Quadraphonic"), TRANSLATE_NOOP("AudioStream", "Quadraphonic with LFE"),
|
||||
TRANSLATE_NOOP("AudioStream", "5.1 Surround"), TRANSLATE_NOOP("AudioStream", "7.1 Surround"),
|
||||
};
|
||||
|
||||
const char* AudioStream::GetExpansionModeName(AudioExpansionMode mode)
|
||||
{
|
||||
return (static_cast<u32>(mode) < s_expansion_mode_names.size()) ? s_expansion_mode_names[static_cast<u32>(mode)] : "";
|
||||
}
|
||||
|
||||
const char* AudioStream::GetExpansionModeDisplayName(AudioExpansionMode mode)
|
||||
{
|
||||
return (static_cast<u32>(mode) < s_expansion_mode_display_names.size()) ?
|
||||
Host::TranslateToCString("AudioStream", s_expansion_mode_display_names[static_cast<u32>(mode)]) :
|
||||
"";
|
||||
}
|
||||
|
||||
std::optional<AudioExpansionMode> AudioStream::ParseExpansionMode(const char* name)
|
||||
{
|
||||
for (u8 i = 0; i < static_cast<u8>(AudioExpansionMode::Count); i++)
|
||||
{
|
||||
if (std::strcmp(name, s_expansion_mode_names[i]) == 0)
|
||||
return static_cast<AudioExpansionMode>(i);
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
static constexpr const std::array s_stretch_mode_names = {
|
||||
"None",
|
||||
"Resample",
|
||||
"TimeStretch",
|
||||
};
|
||||
static constexpr const std::array s_stretch_mode_display_names = {
|
||||
TRANSLATE_NOOP("AudioStream", "Off (Noisy)"),
|
||||
TRANSLATE_NOOP("AudioStream", "Resampling (Pitch Shift)"),
|
||||
TRANSLATE_NOOP("AudioStream", "Time Stretch (Tempo Change, Best Sound)"),
|
||||
};
|
||||
|
||||
const char* AudioStream::GetStretchModeName(AudioStretchMode mode)
|
||||
{
|
||||
@ -89,7 +233,7 @@ u32 AudioStream::GetBufferedFramesRelaxed() const
|
||||
return (wpos + m_buffer_size - rpos) % m_buffer_size;
|
||||
}
|
||||
|
||||
void AudioStream::ReadFrames(s16* samples, u32 num_frames)
|
||||
void AudioStream::ReadFrames(SampleType* samples, u32 num_frames)
|
||||
{
|
||||
const u32 available_frames = GetBufferedFramesRelaxed();
|
||||
u32 frames_to_read = num_frames;
|
||||
@ -97,7 +241,7 @@ void AudioStream::ReadFrames(s16* samples, u32 num_frames)
|
||||
|
||||
if (m_filling)
|
||||
{
|
||||
u32 toFill = m_buffer_size / ((m_stretch_mode != AudioStretchMode::TimeStretch) ? 32 : 400);
|
||||
u32 toFill = m_buffer_size / ((m_parameters.stretch_mode != AudioStretchMode::TimeStretch) ? 32 : 400);
|
||||
toFill = GetAlignedBufferSize(toFill);
|
||||
|
||||
if (available_frames < toFill)
|
||||
@ -118,7 +262,7 @@ void AudioStream::ReadFrames(s16* samples, u32 num_frames)
|
||||
frames_to_read = available_frames;
|
||||
m_filling = true;
|
||||
|
||||
if (m_stretch_mode == AudioStretchMode::TimeStretch)
|
||||
if (m_parameters.stretch_mode == AudioStretchMode::TimeStretch)
|
||||
StretchUnderrun();
|
||||
}
|
||||
|
||||
@ -133,7 +277,7 @@ void AudioStream::ReadFrames(s16* samples, u32 num_frames)
|
||||
// towards the end of the buffer
|
||||
if (end > 0)
|
||||
{
|
||||
std::memcpy(samples, &m_buffer[rpos], sizeof(s32) * end);
|
||||
m_sample_reader(samples, &m_buffer[rpos * m_internal_channels], end);
|
||||
rpos += end;
|
||||
rpos = (rpos == m_buffer_size) ? 0 : rpos;
|
||||
}
|
||||
@ -142,7 +286,7 @@ void AudioStream::ReadFrames(s16* samples, u32 num_frames)
|
||||
const u32 start = frames_to_read - end;
|
||||
if (start > 0)
|
||||
{
|
||||
std::memcpy(&samples[end * 2], &m_buffer[0], sizeof(s32) * start);
|
||||
m_sample_reader(&samples[end * m_output_channels], &m_buffer[0], start);
|
||||
rpos = start;
|
||||
}
|
||||
|
||||
@ -158,19 +302,20 @@ void AudioStream::ReadFrames(s16* samples, u32 num_frames)
|
||||
const u32 increment =
|
||||
static_cast<u32>(65536.0f * (static_cast<float>(frames_to_read) / static_cast<float>(num_frames)));
|
||||
|
||||
SampleType* resample_ptr = static_cast<SampleType*>(alloca(frames_to_read * m_channels * sizeof(SampleType)));
|
||||
std::memcpy(resample_ptr, samples, frames_to_read * m_channels * sizeof(SampleType));
|
||||
SampleType* resample_ptr =
|
||||
static_cast<SampleType*>(alloca(frames_to_read * m_output_channels * sizeof(SampleType)));
|
||||
std::memcpy(resample_ptr, samples, frames_to_read * m_output_channels * sizeof(SampleType));
|
||||
|
||||
SampleType* out_ptr = samples;
|
||||
const u32 copy_stride = sizeof(SampleType) * m_channels;
|
||||
const u32 copy_stride = sizeof(SampleType) * m_output_channels;
|
||||
u32 resample_subpos = 0;
|
||||
for (u32 i = 0; i < num_frames; i++)
|
||||
{
|
||||
std::memcpy(out_ptr, resample_ptr, copy_stride);
|
||||
out_ptr += m_channels;
|
||||
out_ptr += m_output_channels;
|
||||
|
||||
resample_subpos += increment;
|
||||
resample_ptr += (resample_subpos >> 16) * m_channels;
|
||||
resample_ptr += (resample_subpos >> 16) * m_output_channels;
|
||||
resample_subpos %= 65536u;
|
||||
}
|
||||
|
||||
@ -179,19 +324,23 @@ void AudioStream::ReadFrames(s16* samples, u32 num_frames)
|
||||
else
|
||||
{
|
||||
// no data, fall back to silence
|
||||
std::memset(samples + (frames_to_read * m_channels), 0, sizeof(s16) * m_channels * silence_frames);
|
||||
std::memset(samples + (frames_to_read * m_output_channels), 0, silence_frames * m_output_channels * sizeof(s16));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AudioStream::ApplyVolume(s16* samples, u32 num_frames)
|
||||
void AudioStream::StereoSampleReaderImpl(SampleType* dest, const SampleType* src, u32 num_frames)
|
||||
{
|
||||
std::memcpy(dest, src, num_frames * 2 * sizeof(SampleType));
|
||||
}
|
||||
|
||||
void AudioStream::ApplyVolume(s16* samples, u32 num_samples)
|
||||
{
|
||||
if (m_volume == 100)
|
||||
return;
|
||||
|
||||
const s32 volume_mult = static_cast<s32>((static_cast<float>(m_volume) / 100.0f) * 32768.0f);
|
||||
|
||||
u32 num_samples = num_frames * m_channels;
|
||||
while (num_samples > 0)
|
||||
{
|
||||
*samples = static_cast<s16>((static_cast<s32>(*samples) * volume_mult) >> 15);
|
||||
@ -200,12 +349,12 @@ void AudioStream::ApplyVolume(s16* samples, u32 num_frames)
|
||||
}
|
||||
}
|
||||
|
||||
void AudioStream::InternalWriteFrames(s32* bData, u32 nSamples)
|
||||
void AudioStream::InternalWriteFrames(s16* data, u32 num_frames)
|
||||
{
|
||||
const u32 free = m_buffer_size - GetBufferedFramesRelaxed();
|
||||
if (free <= nSamples)
|
||||
if (free <= num_frames)
|
||||
{
|
||||
if (m_stretch_mode == AudioStretchMode::TimeStretch)
|
||||
if (m_parameters.stretch_mode == AudioStretchMode::TimeStretch)
|
||||
{
|
||||
StretchOverrun();
|
||||
}
|
||||
@ -219,49 +368,66 @@ void AudioStream::InternalWriteFrames(s32* bData, u32 nSamples)
|
||||
u32 wpos = m_wpos.load(std::memory_order_acquire);
|
||||
|
||||
// wrapping around the end of the buffer?
|
||||
if ((m_buffer_size - wpos) <= nSamples)
|
||||
if ((m_buffer_size - wpos) <= num_frames)
|
||||
{
|
||||
// needs to be written in two parts
|
||||
const u32 end = m_buffer_size - wpos;
|
||||
const u32 start = nSamples - end;
|
||||
const u32 start = num_frames - end;
|
||||
|
||||
// start is zero when this chunk reaches exactly the end
|
||||
std::memcpy(&m_buffer[wpos], bData, end * sizeof(s32));
|
||||
std::memcpy(&m_buffer[wpos * m_internal_channels], data, end * m_internal_channels * sizeof(SampleType));
|
||||
if (start > 0)
|
||||
std::memcpy(&m_buffer[0], bData + end, start * sizeof(s32));
|
||||
std::memcpy(&m_buffer[0], data + end * m_internal_channels, start * m_internal_channels * sizeof(SampleType));
|
||||
|
||||
wpos = start;
|
||||
}
|
||||
else
|
||||
{
|
||||
// no split
|
||||
std::memcpy(&m_buffer[wpos], bData, nSamples * sizeof(s32));
|
||||
wpos += nSamples;
|
||||
std::memcpy(&m_buffer[wpos * m_internal_channels], data, num_frames * m_internal_channels * sizeof(SampleType));
|
||||
wpos += num_frames;
|
||||
}
|
||||
|
||||
m_wpos.store(wpos, std::memory_order_release);
|
||||
}
|
||||
|
||||
void AudioStream::BaseInitialize()
|
||||
void AudioStream::BaseInitialize(SampleReader sample_reader)
|
||||
{
|
||||
m_sample_reader = sample_reader;
|
||||
|
||||
AllocateBuffer();
|
||||
ExpandAllocate();
|
||||
StretchAllocate();
|
||||
}
|
||||
|
||||
void AudioStream::AllocateBuffer()
|
||||
{
|
||||
// use a larger buffer when time stretching, since we need more input
|
||||
const u32 multplier =
|
||||
(m_stretch_mode == AudioStretchMode::TimeStretch) ? 16 : ((m_stretch_mode == AudioStretchMode::Off) ? 1 : 2);
|
||||
m_buffer_size = GetAlignedBufferSize(((m_buffer_ms * multplier) * m_sample_rate) / 1000);
|
||||
m_target_buffer_size = GetAlignedBufferSize((m_sample_rate * m_buffer_ms) / 1000u);
|
||||
m_buffer = std::unique_ptr<s32[]>(new s32[m_buffer_size]);
|
||||
Log_DevPrintf("Allocated buffer of %u frames for buffer of %u ms [stretch %s, target size %u].", m_buffer_size,
|
||||
m_buffer_ms, GetStretchModeName(m_stretch_mode), m_target_buffer_size);
|
||||
// TODO: do we really? it's more the output...
|
||||
const u32 multiplier = (m_parameters.stretch_mode == AudioStretchMode::TimeStretch) ?
|
||||
16 :
|
||||
((m_parameters.stretch_mode == AudioStretchMode::Off) ? 1 : 2);
|
||||
m_buffer_size = GetAlignedBufferSize(((m_parameters.buffer_ms * multiplier) * m_sample_rate) / 1000);
|
||||
m_target_buffer_size = GetAlignedBufferSize((m_sample_rate * m_parameters.buffer_ms) / 1000u);
|
||||
|
||||
m_buffer = std::make_unique<s16[]>(m_buffer_size * m_internal_channels);
|
||||
m_staging_buffer = std::make_unique<s16[]>(CHUNK_SIZE * m_internal_channels);
|
||||
m_float_buffer = std::make_unique<float[]>(CHUNK_SIZE * m_internal_channels);
|
||||
|
||||
if (IsExpansionEnabled())
|
||||
m_expand_buffer = std::make_unique<float[]>(m_parameters.expand_block_size * NUM_INPUT_CHANNELS);
|
||||
|
||||
Log_DevFmt(
|
||||
"Allocated buffer of {} frames for buffer of {} ms [expansion {} (block size {}), stretch {}, target size {}].",
|
||||
m_buffer_size, m_parameters.buffer_ms, GetExpansionModeName(m_parameters.expansion_mode),
|
||||
m_parameters.expand_block_size, GetStretchModeName(m_parameters.stretch_mode), m_target_buffer_size);
|
||||
}
|
||||
|
||||
void AudioStream::DestroyBuffer()
|
||||
{
|
||||
m_expand_buffer.reset();
|
||||
m_staging_buffer.reset();
|
||||
m_float_buffer.reset();
|
||||
m_buffer.reset();
|
||||
m_buffer_size = 0;
|
||||
m_wpos.store(0, std::memory_order_release);
|
||||
@ -270,10 +436,19 @@ void AudioStream::DestroyBuffer()
|
||||
|
||||
void AudioStream::EmptyBuffer()
|
||||
{
|
||||
if (m_stretch_mode != AudioStretchMode::Off)
|
||||
#ifndef __ANDROID__
|
||||
if (IsExpansionEnabled())
|
||||
{
|
||||
m_expander->Flush();
|
||||
m_expand_output_buffer = nullptr;
|
||||
m_expand_buffer_pos = 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (IsStretchEnabled())
|
||||
{
|
||||
m_soundtouch->clear();
|
||||
if (m_stretch_mode == AudioStretchMode::TimeStretch)
|
||||
if (m_parameters.stretch_mode == AudioStretchMode::TimeStretch)
|
||||
m_soundtouch->setTempo(m_nominal_rate);
|
||||
}
|
||||
|
||||
@ -283,13 +458,13 @@ void AudioStream::EmptyBuffer()
|
||||
void AudioStream::SetNominalRate(float tempo)
|
||||
{
|
||||
m_nominal_rate = tempo;
|
||||
if (m_stretch_mode == AudioStretchMode::Resample)
|
||||
if (m_parameters.stretch_mode == AudioStretchMode::Resample)
|
||||
m_soundtouch->setRate(tempo);
|
||||
}
|
||||
|
||||
void AudioStream::UpdateTargetTempo(float tempo)
|
||||
{
|
||||
if (m_stretch_mode != AudioStretchMode::TimeStretch)
|
||||
if (m_parameters.stretch_mode != AudioStretchMode::TimeStretch)
|
||||
return;
|
||||
|
||||
// undo sqrt()
|
||||
@ -308,7 +483,7 @@ void AudioStream::UpdateTargetTempo(float tempo)
|
||||
|
||||
void AudioStream::SetStretchMode(AudioStretchMode mode)
|
||||
{
|
||||
if (m_stretch_mode == mode)
|
||||
if (m_parameters.stretch_mode == mode)
|
||||
return;
|
||||
|
||||
// can't resize the buffers while paused
|
||||
@ -318,10 +493,10 @@ void AudioStream::SetStretchMode(AudioStretchMode mode)
|
||||
|
||||
DestroyBuffer();
|
||||
StretchDestroy();
|
||||
m_stretch_mode = mode;
|
||||
m_parameters.stretch_mode = mode;
|
||||
|
||||
AllocateBuffer();
|
||||
if (m_stretch_mode != AudioStretchMode::Off)
|
||||
if (m_parameters.stretch_mode != AudioStretchMode::Off)
|
||||
StretchAllocate();
|
||||
|
||||
if (!paused)
|
||||
@ -341,8 +516,8 @@ void AudioStream::SetOutputVolume(u32 volume)
|
||||
void AudioStream::BeginWrite(SampleType** buffer_ptr, u32* num_frames)
|
||||
{
|
||||
// TODO: Write directly to buffer when not using stretching.
|
||||
*buffer_ptr = reinterpret_cast<s16*>(&m_staging_buffer[m_staging_buffer_pos]);
|
||||
*num_frames = CHUNK_SIZE - m_staging_buffer_pos;
|
||||
*buffer_ptr = &m_staging_buffer[m_staging_buffer_pos];
|
||||
*num_frames = CHUNK_SIZE - (m_staging_buffer_pos / NUM_INPUT_CHANNELS);
|
||||
}
|
||||
|
||||
void AudioStream::WriteFrames(const SampleType* frames, u32 num_frames)
|
||||
@ -350,41 +525,20 @@ void AudioStream::WriteFrames(const SampleType* frames, u32 num_frames)
|
||||
Panic("not implemented");
|
||||
}
|
||||
|
||||
void AudioStream::EndWrite(u32 num_frames)
|
||||
{
|
||||
// don't bother committing anything when muted
|
||||
if (m_volume == 0)
|
||||
return;
|
||||
|
||||
m_staging_buffer_pos += num_frames;
|
||||
DebugAssert(m_staging_buffer_pos <= CHUNK_SIZE);
|
||||
if (m_staging_buffer_pos < CHUNK_SIZE)
|
||||
return;
|
||||
|
||||
m_staging_buffer_pos = 0;
|
||||
|
||||
if (m_stretch_mode != AudioStretchMode::Off)
|
||||
StretchWrite();
|
||||
else
|
||||
InternalWriteFrames(m_staging_buffer.data(), CHUNK_SIZE);
|
||||
}
|
||||
|
||||
static constexpr float S16_TO_FLOAT = 1.0f / 32767.0f;
|
||||
static constexpr float FLOAT_TO_S16 = 32767.0f;
|
||||
|
||||
#if defined(CPU_ARCH_NEON)
|
||||
|
||||
static void S16ChunkToFloat(const s32* src, float* dst)
|
||||
static void S16ChunkToFloat(const s16* src, float* dst, u32 num_samples)
|
||||
{
|
||||
static_assert((AudioStream::CHUNK_SIZE % 4) == 0);
|
||||
constexpr u32 iterations = AudioStream::CHUNK_SIZE / 4;
|
||||
|
||||
const float32x4_t S16_TO_FLOAT_V = vdupq_n_f32(S16_TO_FLOAT);
|
||||
|
||||
const u32 iterations = (num_samples + 7) / 8;
|
||||
for (u32 i = 0; i < iterations; i++)
|
||||
{
|
||||
const int16x8_t sv = vreinterpretq_s16_s32(vld1q_s32(src));
|
||||
src += 4;
|
||||
const int16x8_t sv = vreinterpretq_s16_s32(vld1q_s16(src));
|
||||
src += 8;
|
||||
|
||||
int32x4_t iv1 = vreinterpretq_s32_s16(vzip1q_s16(sv, sv)); // [0, 0, 1, 1, 2, 2, 3, 3]
|
||||
int32x4_t iv2 = vreinterpretq_s32_s16(vzip2q_s16(sv, sv)); // [4, 4, 5, 5, 6, 6, 7, 7]
|
||||
@ -401,13 +555,11 @@ static void S16ChunkToFloat(const s32* src, float* dst)
|
||||
}
|
||||
}
|
||||
|
||||
static void FloatChunkToS16(s32* dst, const float* src, uint size)
|
||||
static void FloatChunkToS16(s16* dst, const float* src, u32 num_samples)
|
||||
{
|
||||
static_assert((AudioStream::CHUNK_SIZE % 4) == 0);
|
||||
constexpr u32 iterations = AudioStream::CHUNK_SIZE / 4;
|
||||
|
||||
const float32x4_t FLOAT_TO_S16_V = vdupq_n_f32(FLOAT_TO_S16);
|
||||
|
||||
const u32 iterations = (num_samples + 7) / 8;
|
||||
for (u32 i = 0; i < iterations; i++)
|
||||
{
|
||||
float32x4_t fv1 = vld1q_f32(src + 0);
|
||||
@ -420,24 +572,22 @@ static void FloatChunkToS16(s32* dst, const float* src, uint size)
|
||||
int32x4_t iv2 = vcvtq_s32_f32(fv2);
|
||||
|
||||
int16x8_t iv = vcombine_s16(vqmovn_s32(iv1), vqmovn_s32(iv2));
|
||||
vst1q_s32(dst, vreinterpretq_s32_s16(iv));
|
||||
dst += 4;
|
||||
vst1q_s16(dst, iv);
|
||||
dst += 8;
|
||||
}
|
||||
}
|
||||
|
||||
#elif defined(CPU_ARCH_SSE)
|
||||
|
||||
static void S16ChunkToFloat(const s32* src, float* dst)
|
||||
static void S16ChunkToFloat(const s16* src, float* dst, u32 num_samples)
|
||||
{
|
||||
static_assert((AudioStream::CHUNK_SIZE % 4) == 0);
|
||||
constexpr u32 iterations = AudioStream::CHUNK_SIZE / 4;
|
||||
|
||||
const __m128 S16_TO_FLOAT_V = _mm_set1_ps(S16_TO_FLOAT);
|
||||
|
||||
const u32 iterations = (num_samples + 7) / 8;
|
||||
for (u32 i = 0; i < iterations; i++)
|
||||
{
|
||||
const __m128i sv = _mm_load_si128(reinterpret_cast<const __m128i*>(src));
|
||||
src += 4;
|
||||
src += 8;
|
||||
|
||||
__m128i iv1 = _mm_unpacklo_epi16(sv, sv); // [0, 0, 1, 1, 2, 2, 3, 3]
|
||||
__m128i iv2 = _mm_unpackhi_epi16(sv, sv); // [4, 4, 5, 5, 6, 6, 7, 7]
|
||||
@ -454,13 +604,11 @@ static void S16ChunkToFloat(const s32* src, float* dst)
|
||||
}
|
||||
}
|
||||
|
||||
static void FloatChunkToS16(s32* dst, const float* src, uint size)
|
||||
static void FloatChunkToS16(s16* dst, const float* src, u32 num_samples)
|
||||
{
|
||||
static_assert((AudioStream::CHUNK_SIZE % 4) == 0);
|
||||
constexpr u32 iterations = AudioStream::CHUNK_SIZE / 4;
|
||||
|
||||
const __m128 FLOAT_TO_S16_V = _mm_set1_ps(FLOAT_TO_S16);
|
||||
|
||||
const u32 iterations = (num_samples + 7) / 8;
|
||||
for (u32 i = 0; i < iterations; i++)
|
||||
{
|
||||
__m128 fv1 = _mm_load_ps(src + 0);
|
||||
@ -474,33 +622,107 @@ static void FloatChunkToS16(s32* dst, const float* src, uint size)
|
||||
|
||||
__m128i iv = _mm_packs_epi32(iv1, iv2);
|
||||
_mm_store_si128(reinterpret_cast<__m128i*>(dst), iv);
|
||||
dst += 4;
|
||||
dst += 8;
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
static void S16ChunkToFloat(const s32* src, float* dst)
|
||||
static void S16ChunkToFloat(const s16* src, float* dst, u32 num_samples)
|
||||
{
|
||||
for (uint i = 0; i < AudioStream::CHUNK_SIZE; ++i)
|
||||
{
|
||||
*(dst++) = static_cast<float>(static_cast<s16>((u32)*src)) / 32767.0f;
|
||||
*(dst++) = static_cast<float>(static_cast<s16>(((u32)*src) >> 16)) / 32767.0f;
|
||||
src++;
|
||||
}
|
||||
for (u32 i = 0; i < num_samples; ++i)
|
||||
*(dst++) = static_cast<float>(*(src++)) / 32767.0f;
|
||||
}
|
||||
|
||||
static void FloatChunkToS16(s32* dst, const float* src, uint size)
|
||||
static void FloatChunkToS16(s16* dst, const float* src, u32 num_samples)
|
||||
{
|
||||
for (uint i = 0; i < size; ++i)
|
||||
{
|
||||
const s16 left = static_cast<s16>((*(src++) * 32767.0f));
|
||||
const s16 right = static_cast<s16>((*(src++) * 32767.0f));
|
||||
*(dst++) = (static_cast<u32>(left) & 0xFFFFu) | (static_cast<u32>(right) << 16);
|
||||
}
|
||||
for (u32 i = 0; i < num_samples; ++i)
|
||||
*(dst++) = static_cast<s16>((*(src++) * 32767.0f));
|
||||
}
|
||||
#endif
|
||||
|
||||
void AudioStream::ExpandAllocate()
|
||||
{
|
||||
DebugAssert(!m_expander);
|
||||
if (m_parameters.expansion_mode == AudioExpansionMode::Disabled)
|
||||
return;
|
||||
|
||||
#ifndef __ANDROID__
|
||||
static constexpr std::array<std::pair<FreeSurroundDecoder::ChannelSetup, bool>,
|
||||
static_cast<size_t>(AudioExpansionMode::Count)>
|
||||
channel_setup_mapping = {{
|
||||
{FreeSurroundDecoder::ChannelSetup::Stereo, false}, // Disabled
|
||||
{FreeSurroundDecoder::ChannelSetup::Stereo, true}, // StereoLFE
|
||||
{FreeSurroundDecoder::ChannelSetup::Surround41, false}, // Quadraphonic
|
||||
{FreeSurroundDecoder::ChannelSetup::Surround41, true}, // QuadraphonicLFE
|
||||
{FreeSurroundDecoder::ChannelSetup::Surround51, true}, // Surround51
|
||||
{FreeSurroundDecoder::ChannelSetup::Surround71, true}, // Surround71
|
||||
}};
|
||||
|
||||
const auto [fs_setup, fs_lfe] = channel_setup_mapping[static_cast<size_t>(m_parameters.expansion_mode)];
|
||||
|
||||
m_expander = std::make_unique<FreeSurroundDecoder>(fs_setup, m_parameters.expand_block_size);
|
||||
m_expander->SetBassRedirection(fs_lfe);
|
||||
m_expander->SetCircularWrap(m_parameters.expand_circular_wrap);
|
||||
m_expander->SetShift(m_parameters.expand_shift);
|
||||
m_expander->SetDepth(m_parameters.expand_depth);
|
||||
m_expander->SetFocus(m_parameters.expand_focus);
|
||||
m_expander->SetCenterImage(m_parameters.expand_center_image);
|
||||
m_expander->SetFrontSeparation(m_parameters.expand_front_separation);
|
||||
m_expander->SetRearSeparation(m_parameters.expand_rear_separation);
|
||||
m_expander->SetLowCutoff(static_cast<float>(m_parameters.expand_low_cutoff) / m_sample_rate * 2);
|
||||
m_expander->SetHighCutoff(static_cast<float>(m_parameters.expand_high_cutoff) / m_sample_rate * 2);
|
||||
#else
|
||||
Panic("Attempting to use expansion on Android.");
|
||||
#endif
|
||||
}
|
||||
|
||||
void AudioStream::EndWrite(u32 num_frames)
|
||||
{
|
||||
// don't bother committing anything when muted
|
||||
if (m_volume == 0)
|
||||
return;
|
||||
|
||||
m_staging_buffer_pos += num_frames * NUM_INPUT_CHANNELS;
|
||||
DebugAssert(m_staging_buffer_pos <= (CHUNK_SIZE * NUM_INPUT_CHANNELS));
|
||||
if ((m_staging_buffer_pos / NUM_INPUT_CHANNELS) < CHUNK_SIZE)
|
||||
return;
|
||||
|
||||
m_staging_buffer_pos = 0;
|
||||
|
||||
if (!IsExpansionEnabled() && !IsStretchEnabled())
|
||||
{
|
||||
InternalWriteFrames(m_staging_buffer.get(), CHUNK_SIZE);
|
||||
return;
|
||||
}
|
||||
|
||||
#ifndef __ANDROID__
|
||||
if (IsExpansionEnabled())
|
||||
{
|
||||
// StretchWriteBlock() overwrites the staging buffer on output, so we need to copy into the expand buffer first.
|
||||
S16ChunkToFloat(m_staging_buffer.get(), m_expand_buffer.get() + m_expand_buffer_pos * NUM_INPUT_CHANNELS,
|
||||
CHUNK_SIZE * NUM_INPUT_CHANNELS);
|
||||
|
||||
// Output the corresponding block.
|
||||
if (m_expand_output_buffer)
|
||||
StretchWriteBlock(m_expand_output_buffer + m_expand_buffer_pos * m_internal_channels);
|
||||
|
||||
// Decode the next block if we buffered enough.
|
||||
m_expand_buffer_pos += CHUNK_SIZE;
|
||||
if (m_expand_buffer_pos == m_parameters.expand_block_size)
|
||||
{
|
||||
m_expand_buffer_pos = 0;
|
||||
m_expand_output_buffer = m_expander->Decode(m_expand_buffer.get());
|
||||
}
|
||||
}
|
||||
else
|
||||
#endif
|
||||
{
|
||||
S16ChunkToFloat(m_staging_buffer.get(), m_float_buffer.get(), CHUNK_SIZE * NUM_INPUT_CHANNELS);
|
||||
StretchWriteBlock(m_float_buffer.get());
|
||||
}
|
||||
}
|
||||
|
||||
// Time stretching algorithm based on PCSX2 implementation.
|
||||
|
||||
template<class T>
|
||||
@ -511,21 +733,21 @@ ALWAYS_INLINE static bool IsInRange(const T& val, const T& min, const T& max)
|
||||
|
||||
void AudioStream::StretchAllocate()
|
||||
{
|
||||
if (m_stretch_mode == AudioStretchMode::Off)
|
||||
if (m_parameters.stretch_mode == AudioStretchMode::Off)
|
||||
return;
|
||||
|
||||
m_soundtouch = std::make_unique<soundtouch::SoundTouch>();
|
||||
m_soundtouch->setSampleRate(m_sample_rate);
|
||||
m_soundtouch->setChannels(m_channels);
|
||||
m_soundtouch->setChannels(m_internal_channels);
|
||||
|
||||
m_soundtouch->setSetting(SETTING_USE_QUICKSEEK, 0);
|
||||
m_soundtouch->setSetting(SETTING_USE_AA_FILTER, 0);
|
||||
m_soundtouch->setSetting(SETTING_USE_QUICKSEEK, m_parameters.stretch_use_quickseek);
|
||||
m_soundtouch->setSetting(SETTING_USE_AA_FILTER, m_parameters.stretch_use_aa_filter);
|
||||
|
||||
m_soundtouch->setSetting(SETTING_SEQUENCE_MS, 30);
|
||||
m_soundtouch->setSetting(SETTING_SEEKWINDOW_MS, 20);
|
||||
m_soundtouch->setSetting(SETTING_OVERLAP_MS, 10);
|
||||
m_soundtouch->setSetting(SETTING_SEQUENCE_MS, m_parameters.stretch_sequence_length_ms);
|
||||
m_soundtouch->setSetting(SETTING_SEEKWINDOW_MS, m_parameters.stretch_seekwindow_ms);
|
||||
m_soundtouch->setSetting(SETTING_OVERLAP_MS, m_parameters.stretch_overlap_ms);
|
||||
|
||||
if (m_stretch_mode == AudioStretchMode::Resample)
|
||||
if (m_parameters.stretch_mode == AudioStretchMode::Resample)
|
||||
m_soundtouch->setRate(m_nominal_rate);
|
||||
else
|
||||
m_soundtouch->setTempo(m_nominal_rate);
|
||||
@ -545,21 +767,27 @@ void AudioStream::StretchDestroy()
|
||||
m_soundtouch.reset();
|
||||
}
|
||||
|
||||
void AudioStream::StretchWrite()
|
||||
void AudioStream::StretchWriteBlock(const float* block)
|
||||
{
|
||||
S16ChunkToFloat(m_staging_buffer.data(), m_float_buffer.data());
|
||||
|
||||
m_soundtouch->putSamples(m_float_buffer.data(), CHUNK_SIZE);
|
||||
|
||||
int tempProgress;
|
||||
while (tempProgress = m_soundtouch->receiveSamples((float*)m_float_buffer.data(), CHUNK_SIZE), tempProgress != 0)
|
||||
if (IsStretchEnabled())
|
||||
{
|
||||
FloatChunkToS16(m_staging_buffer.data(), m_float_buffer.data(), tempProgress);
|
||||
InternalWriteFrames(m_staging_buffer.data(), tempProgress);
|
||||
}
|
||||
m_soundtouch->putSamples(block, CHUNK_SIZE);
|
||||
|
||||
if (m_stretch_mode == AudioStretchMode::TimeStretch)
|
||||
UpdateStretchTempo();
|
||||
u32 tempProgress;
|
||||
while (tempProgress = m_soundtouch->receiveSamples(m_float_buffer.get(), CHUNK_SIZE), tempProgress != 0)
|
||||
{
|
||||
FloatChunkToS16(m_staging_buffer.get(), m_float_buffer.get(), tempProgress * m_internal_channels);
|
||||
InternalWriteFrames(m_staging_buffer.get(), tempProgress);
|
||||
}
|
||||
|
||||
if (m_parameters.stretch_mode == AudioStretchMode::TimeStretch)
|
||||
UpdateStretchTempo();
|
||||
}
|
||||
else
|
||||
{
|
||||
FloatChunkToS16(m_staging_buffer.get(), block, CHUNK_SIZE * m_internal_channels);
|
||||
InternalWriteFrames(m_staging_buffer.get(), CHUNK_SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
float AudioStream::AddAndGetAverageTempo(float val)
|
||||
@ -691,3 +919,113 @@ void AudioStream::StretchOverrun()
|
||||
const u32 discard = CHUNK_SIZE * 2;
|
||||
m_rpos.store((m_rpos.load(std::memory_order_acquire) + discard) % m_buffer_size, std::memory_order_release);
|
||||
}
|
||||
|
||||
void AudioStreamParameters::Load(SettingsInterface& si, const char* section)
|
||||
{
|
||||
stretch_mode =
|
||||
AudioStream::ParseStretchMode(
|
||||
si.GetStringValue(section, "StretchMode", AudioStream::GetStretchModeName(DEFAULT_STRETCH_MODE)).c_str())
|
||||
.value_or(DEFAULT_STRETCH_MODE);
|
||||
#ifndef __ANDROID__
|
||||
expansion_mode =
|
||||
AudioStream::ParseExpansionMode(
|
||||
si.GetStringValue(section, "ExpansionMode", AudioStream::GetExpansionModeName(DEFAULT_EXPANSION_MODE)).c_str())
|
||||
.value_or(DEFAULT_EXPANSION_MODE);
|
||||
#else
|
||||
expansion_mode = AudioExpansionMode::Disabled;
|
||||
#endif
|
||||
output_latency_ms = static_cast<u16>(std::min<u32>(
|
||||
si.GetUIntValue(section, "OutputLatencyMS", DEFAULT_OUTPUT_LATENCY_MS), std::numeric_limits<u16>::max()));
|
||||
buffer_ms = static_cast<u16>(
|
||||
std::min<u32>(si.GetUIntValue(section, "BufferMS", DEFAULT_BUFFER_MS), std::numeric_limits<u16>::max()));
|
||||
|
||||
stretch_sequence_length_ms =
|
||||
static_cast<u16>(std::min<u32>(si.GetUIntValue(section, "StretchSequenceLengthMS", DEFAULT_STRETCH_SEQUENCE_LENGTH),
|
||||
std::numeric_limits<u16>::max()));
|
||||
stretch_seekwindow_ms = static_cast<u16>(std::min<u32>(
|
||||
si.GetUIntValue(section, "StretchSeekWindowMS", DEFAULT_STRETCH_SEEKWINDOW), std::numeric_limits<u16>::max()));
|
||||
stretch_overlap_ms = static_cast<u16>(std::min<u32>(
|
||||
si.GetUIntValue(section, "StretchOverlapMS", DEFAULT_STRETCH_OVERLAP), std::numeric_limits<u16>::max()));
|
||||
stretch_use_quickseek = si.GetBoolValue(section, "StretchUseQuickSeek", DEFAULT_STRETCH_USE_QUICKSEEK);
|
||||
stretch_use_aa_filter = si.GetBoolValue(section, "StretchUseAAFilter", DEFAULT_STRETCH_USE_AA_FILTER);
|
||||
|
||||
expand_block_size = static_cast<u16>(std::min<u32>(
|
||||
si.GetUIntValue(section, "ExpandBlockSize", DEFAULT_EXPAND_BLOCK_SIZE), std::numeric_limits<u16>::max()));
|
||||
expand_block_size = std::clamp<u16>(
|
||||
Common::IsPow2(expand_block_size) ? expand_block_size : Common::NextPow2(expand_block_size), 128, 8192);
|
||||
expand_circular_wrap =
|
||||
std::clamp(si.GetFloatValue(section, "ExpandCircularWrap", DEFAULT_EXPAND_CIRCULAR_WRAP), 0.0f, 360.0f);
|
||||
expand_shift = std::clamp(si.GetFloatValue(section, "ExpandShift", DEFAULT_EXPAND_SHIFT), -1.0f, 1.0f);
|
||||
expand_depth = std::clamp(si.GetFloatValue(section, "ExpandDepth", DEFAULT_EXPAND_DEPTH), 0.0f, 5.0f);
|
||||
expand_focus = std::clamp(si.GetFloatValue(section, "ExpandFocus", DEFAULT_EXPAND_FOCUS), -1.0f, 1.0f);
|
||||
expand_center_image =
|
||||
std::clamp(si.GetFloatValue(section, "ExpandCenterImage", DEFAULT_EXPAND_CENTER_IMAGE), 0.0f, 1.0f);
|
||||
expand_front_separation =
|
||||
std::clamp(si.GetFloatValue(section, "ExpandFrontSeparation", DEFAULT_EXPAND_FRONT_SEPARATION), 0.0f, 10.0f);
|
||||
expand_rear_separation =
|
||||
std::clamp(si.GetFloatValue(section, "ExpandRearSeparation", DEFAULT_EXPAND_REAR_SEPARATION), 0.0f, 10.0f);
|
||||
expand_low_cutoff =
|
||||
static_cast<u8>(std::min<u32>(si.GetUIntValue(section, "ExpandLowCutoff", DEFAULT_EXPAND_LOW_CUTOFF), 100));
|
||||
expand_high_cutoff =
|
||||
static_cast<u8>(std::min<u32>(si.GetUIntValue(section, "ExpandHighCutoff", DEFAULT_EXPAND_HIGH_CUTOFF), 100));
|
||||
}
|
||||
|
||||
void AudioStreamParameters::Save(SettingsInterface& si, const char* section) const
|
||||
{
|
||||
si.SetStringValue(section, "StretchMode", AudioStream::GetStretchModeName(stretch_mode));
|
||||
si.SetStringValue(section, "ExpansionMode", AudioStream::GetExpansionModeName(expansion_mode));
|
||||
si.SetUIntValue(section, "BufferMS", buffer_ms);
|
||||
si.SetUIntValue(section, "OutputLatencyMS", output_latency_ms);
|
||||
|
||||
si.SetUIntValue(section, "StretchSequenceLengthMS", stretch_sequence_length_ms);
|
||||
si.SetUIntValue(section, "StretchSeekWindowMS", stretch_seekwindow_ms);
|
||||
si.SetUIntValue(section, "StretchOverlapMS", stretch_overlap_ms);
|
||||
si.SetBoolValue(section, "StretchUseQuickSeek", stretch_use_quickseek);
|
||||
si.SetBoolValue(section, "StretchUseAAFilter", stretch_use_aa_filter);
|
||||
|
||||
si.SetUIntValue(section, "ExpandBlockSize", expand_block_size);
|
||||
si.SetFloatValue(section, "ExpandCircularWrap", expand_circular_wrap);
|
||||
si.SetFloatValue(section, "ExpandShift", expand_shift);
|
||||
si.SetFloatValue(section, "ExpandDepth", expand_depth);
|
||||
si.SetFloatValue(section, "ExpandFocus", expand_focus);
|
||||
si.SetFloatValue(section, "ExpandCenterImage", expand_center_image);
|
||||
si.SetFloatValue(section, "ExpandFrontSeparation", expand_front_separation);
|
||||
si.SetFloatValue(section, "ExpandRearSeparation", expand_rear_separation);
|
||||
si.SetUIntValue(section, "ExpandLowCutoff", expand_low_cutoff);
|
||||
si.SetUIntValue(section, "ExpandHighCutoff", expand_high_cutoff);
|
||||
}
|
||||
|
||||
void AudioStreamParameters::Clear(SettingsInterface& si, const char* section)
|
||||
{
|
||||
si.DeleteValue(section, "StretchMode");
|
||||
si.DeleteValue(section, "ExpansionMode");
|
||||
si.DeleteValue(section, "BufferMS");
|
||||
si.DeleteValue(section, "OutputLatencyMS");
|
||||
|
||||
si.DeleteValue(section, "StretchSequenceLengthMS");
|
||||
si.DeleteValue(section, "StretchSeekWindowMS");
|
||||
si.DeleteValue(section, "StretchOverlapMS");
|
||||
si.DeleteValue(section, "StretchUseQuickSeek");
|
||||
si.DeleteValue(section, "StretchUseAAFilter");
|
||||
|
||||
si.DeleteValue(section, "ExpandBlockSize");
|
||||
si.DeleteValue(section, "ExpandCircularWrap");
|
||||
si.DeleteValue(section, "ExpandShift");
|
||||
si.DeleteValue(section, "ExpandDepth");
|
||||
si.DeleteValue(section, "ExpandFocus");
|
||||
si.DeleteValue(section, "ExpandCenterImage");
|
||||
si.DeleteValue(section, "ExpandFrontSeparation");
|
||||
si.DeleteValue(section, "ExpandRearSeparation");
|
||||
si.DeleteValue(section, "ExpandLowCutoff");
|
||||
si.DeleteValue(section, "ExpandHighCutoff");
|
||||
}
|
||||
|
||||
bool AudioStreamParameters::operator!=(const AudioStreamParameters& rhs) const
|
||||
{
|
||||
return (std::memcmp(this, &rhs, sizeof(*this)) != 0);
|
||||
}
|
||||
|
||||
bool AudioStreamParameters::operator==(const AudioStreamParameters& rhs) const
|
||||
{
|
||||
return (std::memcmp(this, &rhs, sizeof(*this)) == 0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user