mirror of
https://github.com/WinampDesktop/winamp.git
synced 2025-06-18 19:55:46 -04:00
SPU: Add time stretched audio output
This commit is contained in:
@ -27,8 +27,6 @@ add_library(util
|
||||
iso_reader.h
|
||||
jit_code_buffer.cpp
|
||||
jit_code_buffer.h
|
||||
null_audio_stream.cpp
|
||||
null_audio_stream.h
|
||||
memory_arena.cpp
|
||||
memory_arena.h
|
||||
page_fault_handler.cpp
|
||||
@ -44,4 +42,4 @@ add_library(util
|
||||
target_include_directories(util PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/..")
|
||||
target_include_directories(util PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/..")
|
||||
target_link_libraries(util PUBLIC common simpleini)
|
||||
target_link_libraries(util PRIVATE libchdr samplerate zlib)
|
||||
target_link_libraries(util PRIVATE libchdr samplerate zlib soundtouch)
|
||||
|
@ -1,387 +1,615 @@
|
||||
#include "audio_stream.h"
|
||||
#include "assert.h"
|
||||
#include "SoundTouch.h"
|
||||
#include "common/align.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/log.h"
|
||||
#include "samplerate.h"
|
||||
#include "common/make_array.h"
|
||||
#include "common/timer.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
Log_SetChannel(AudioStream);
|
||||
|
||||
AudioStream::AudioStream() = default;
|
||||
#if defined(_M_ARM64)
|
||||
#include <arm64_neon.h>
|
||||
#elif defined(__aarch64__)
|
||||
#include <arm_neon.h>
|
||||
#elif defined(_M_IX86) || defined(_M_AMD64)
|
||||
#include <emmintrin.h>
|
||||
#endif
|
||||
|
||||
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)
|
||||
{
|
||||
}
|
||||
|
||||
AudioStream::~AudioStream()
|
||||
{
|
||||
DestroyResampler();
|
||||
DestroyBuffer();
|
||||
}
|
||||
|
||||
bool AudioStream::Reconfigure(u32 input_sample_rate /* = DefaultInputSampleRate */,
|
||||
u32 output_sample_rate /* = DefaultOutputSampleRate */, u32 channels /* = 1 */,
|
||||
u32 buffer_size /* = DefaultBufferSize */)
|
||||
std::unique_ptr<AudioStream> AudioStream::CreateNullStream(u32 sample_rate, u32 channels, u32 buffer_ms)
|
||||
{
|
||||
std::unique_lock<std::mutex> buffer_lock(m_buffer_mutex);
|
||||
std::unique_lock<std::mutex> resampler_Lock(m_resampler_mutex);
|
||||
return std::unique_ptr<AudioStream>(new AudioStream(sample_rate, channels, buffer_ms, AudioStretchMode::Off));
|
||||
}
|
||||
|
||||
DestroyResampler();
|
||||
if (IsDeviceOpen())
|
||||
CloseDevice();
|
||||
u32 AudioStream::GetAlignedBufferSize(u32 size)
|
||||
{
|
||||
static_assert(Common::IsPow2(CHUNK_SIZE));
|
||||
return Common::AlignUpPow2(size, CHUNK_SIZE);
|
||||
}
|
||||
|
||||
m_output_sample_rate = output_sample_rate;
|
||||
m_channels = channels;
|
||||
m_buffer_size = buffer_size;
|
||||
m_buffer_filling.store(m_wait_for_buffer_fill);
|
||||
m_output_paused = true;
|
||||
u32 AudioStream::GetBufferSizeForMS(u32 sample_rate, u32 ms)
|
||||
{
|
||||
return GetAlignedBufferSize((ms * sample_rate) / 1000u);
|
||||
}
|
||||
|
||||
if (!SetBufferSize(buffer_size))
|
||||
return false;
|
||||
u32 AudioStream::GetMSForBufferSize(u32 sample_rate, u32 buffer_size)
|
||||
{
|
||||
buffer_size = GetAlignedBufferSize(buffer_size);
|
||||
return (buffer_size * 1000u) / sample_rate;
|
||||
}
|
||||
|
||||
if (!OpenDevice())
|
||||
static constexpr const auto s_stretch_mode_names = make_array("None", "Resample", "TimeStretch");
|
||||
|
||||
const char* AudioStream::GetStretchModeName(AudioStretchMode mode)
|
||||
{
|
||||
return (static_cast<u32>(mode) < s_stretch_mode_names.size()) ? s_stretch_mode_names[static_cast<u32>(mode)] : "";
|
||||
}
|
||||
|
||||
std::optional<AudioStretchMode> AudioStream::ParseStretchMode(const char* name)
|
||||
{
|
||||
for (u8 i = 0; i < static_cast<u8>(AudioStretchMode::Count); i++)
|
||||
{
|
||||
LockedEmptyBuffers();
|
||||
m_buffer_size = 0;
|
||||
m_output_sample_rate = 0;
|
||||
m_channels = 0;
|
||||
return false;
|
||||
if (std::strcmp(name, s_stretch_mode_names[i]) == 0)
|
||||
return static_cast<AudioStretchMode>(i);
|
||||
}
|
||||
|
||||
CreateResampler();
|
||||
InternalSetInputSampleRate(input_sample_rate);
|
||||
|
||||
return true;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void AudioStream::SetInputSampleRate(u32 sample_rate)
|
||||
u32 AudioStream::GetBufferedFramesRelaxed() const
|
||||
{
|
||||
std::unique_lock<std::mutex> buffer_lock(m_buffer_mutex);
|
||||
std::unique_lock<std::mutex> resampler_lock(m_resampler_mutex);
|
||||
|
||||
InternalSetInputSampleRate(sample_rate);
|
||||
const u32 rpos = m_rpos.load(std::memory_order_relaxed);
|
||||
const u32 wpos = m_wpos.load(std::memory_order_relaxed);
|
||||
return (wpos + m_buffer_size - rpos) % m_buffer_size;
|
||||
}
|
||||
|
||||
void AudioStream::SetWaitForBufferFill(bool enabled)
|
||||
void AudioStream::ReadFrames(s16* bData, u32 nFrames)
|
||||
{
|
||||
std::unique_lock<std::mutex> buffer_lock(m_buffer_mutex);
|
||||
m_wait_for_buffer_fill = enabled;
|
||||
if (enabled && m_buffer.IsEmpty())
|
||||
m_buffer_filling.store(true);
|
||||
const u32 available_frames = GetBufferedFramesRelaxed();
|
||||
u32 frames_to_read = nFrames;
|
||||
u32 silence_frames = 0;
|
||||
|
||||
if (m_filling)
|
||||
{
|
||||
u32 toFill = m_buffer_size / ((m_stretch_mode != AudioStretchMode::TimeStretch) ? 32 : 400);
|
||||
toFill = GetAlignedBufferSize(toFill);
|
||||
|
||||
if (available_frames < toFill)
|
||||
{
|
||||
silence_frames = nFrames;
|
||||
frames_to_read = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_filling = false;
|
||||
Log_VerbosePrintf("Underrun compensation done (%d frames buffered)", toFill);
|
||||
}
|
||||
}
|
||||
else if (available_frames < nFrames)
|
||||
{
|
||||
silence_frames = nFrames - available_frames;
|
||||
frames_to_read = available_frames;
|
||||
m_filling = true;
|
||||
|
||||
if (m_stretch_mode == AudioStretchMode::TimeStretch)
|
||||
StretchUnderrun();
|
||||
}
|
||||
|
||||
if (frames_to_read > 0)
|
||||
{
|
||||
u32 rpos = m_rpos.load(std::memory_order_acquire);
|
||||
|
||||
u32 end = m_buffer_size - rpos;
|
||||
if (end > frames_to_read)
|
||||
end = frames_to_read;
|
||||
|
||||
// towards the end of the buffer
|
||||
if (end > 0)
|
||||
{
|
||||
std::memcpy(bData, &m_buffer[rpos], sizeof(s32) * end);
|
||||
rpos += end;
|
||||
rpos = (rpos == m_buffer_size) ? 0 : rpos;
|
||||
}
|
||||
|
||||
// after wrapping around
|
||||
const u32 start = frames_to_read - end;
|
||||
if (start > 0)
|
||||
{
|
||||
std::memcpy(&bData[end * 2], &m_buffer[0], sizeof(s32) * start);
|
||||
rpos = start;
|
||||
}
|
||||
|
||||
m_rpos.store(rpos, std::memory_order_release);
|
||||
}
|
||||
|
||||
// TODO: Bring back the crappy resampler?
|
||||
if (silence_frames > 0)
|
||||
std::memset(bData + frames_to_read, 0, sizeof(s32) * silence_frames);
|
||||
}
|
||||
|
||||
void AudioStream::InternalSetInputSampleRate(u32 sample_rate)
|
||||
void AudioStream::InternalWriteFrames(s32* bData, u32 nSamples)
|
||||
{
|
||||
if (m_input_sample_rate == sample_rate)
|
||||
const u32 free = m_buffer_size - GetBufferedFramesRelaxed();
|
||||
if (free <= nSamples)
|
||||
{
|
||||
if (m_stretch_mode == AudioStretchMode::TimeStretch)
|
||||
{
|
||||
StretchOverrun();
|
||||
}
|
||||
else
|
||||
{
|
||||
Log_DebugPrintf("Buffer overrun, chunk dropped");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
u32 wpos = m_wpos.load(std::memory_order_acquire);
|
||||
|
||||
// wrapping around the end of the buffer?
|
||||
if ((m_buffer_size - wpos) <= nSamples)
|
||||
{
|
||||
// needs to be written in two parts
|
||||
const u32 end = m_buffer_size - wpos;
|
||||
const u32 start = nSamples - end;
|
||||
|
||||
// start is zero when this chunk reaches exactly the end
|
||||
std::memcpy(&m_buffer[wpos], bData, end * sizeof(s32));
|
||||
if (start > 0)
|
||||
std::memcpy(&m_buffer[0], bData + end, start * sizeof(s32));
|
||||
|
||||
wpos = start;
|
||||
}
|
||||
else
|
||||
{
|
||||
// no split
|
||||
std::memcpy(&m_buffer[wpos], bData, nSamples * sizeof(s32));
|
||||
wpos += nSamples;
|
||||
}
|
||||
|
||||
m_wpos.store(wpos, std::memory_order_release);
|
||||
}
|
||||
|
||||
void AudioStream::BaseInitialize()
|
||||
{
|
||||
AllocateBuffer();
|
||||
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);
|
||||
}
|
||||
|
||||
void AudioStream::DestroyBuffer()
|
||||
{
|
||||
m_buffer.reset();
|
||||
m_buffer_size = 0;
|
||||
m_wpos.store(0, std::memory_order_release);
|
||||
m_rpos.store(0, std::memory_order_release);
|
||||
}
|
||||
|
||||
void AudioStream::EmptyBuffer()
|
||||
{
|
||||
if (m_stretch_mode != AudioStretchMode::Off)
|
||||
{
|
||||
m_soundtouch->clear();
|
||||
if (m_stretch_mode == AudioStretchMode::TimeStretch)
|
||||
m_soundtouch->setTempo(m_nominal_rate);
|
||||
}
|
||||
|
||||
m_wpos.store(m_rpos.load(std::memory_order_acquire), std::memory_order_release);
|
||||
}
|
||||
|
||||
void AudioStream::SetNominalRate(float tempo)
|
||||
{
|
||||
m_nominal_rate = tempo;
|
||||
if (m_stretch_mode == AudioStretchMode::Resample)
|
||||
m_soundtouch->setRate(tempo);
|
||||
}
|
||||
|
||||
void AudioStream::SetStretchMode(AudioStretchMode mode)
|
||||
{
|
||||
if (m_stretch_mode == mode)
|
||||
return;
|
||||
|
||||
m_input_sample_rate = sample_rate;
|
||||
m_resampler_ratio = static_cast<double>(m_output_sample_rate) / static_cast<double>(sample_rate);
|
||||
src_set_ratio(static_cast<SRC_STATE*>(m_resampler_state), m_resampler_ratio);
|
||||
ResetResampler();
|
||||
// can't resize the buffers while paused
|
||||
bool paused = m_paused;
|
||||
if (!paused)
|
||||
SetPaused(true);
|
||||
|
||||
DestroyBuffer();
|
||||
StretchDestroy();
|
||||
m_stretch_mode = mode;
|
||||
|
||||
AllocateBuffer();
|
||||
if (m_stretch_mode != AudioStretchMode::Off)
|
||||
StretchAllocate();
|
||||
|
||||
if (!paused)
|
||||
SetPaused(false);
|
||||
}
|
||||
|
||||
void AudioStream::SetPaused(bool paused)
|
||||
{
|
||||
m_paused = paused;
|
||||
}
|
||||
|
||||
void AudioStream::SetOutputVolume(u32 volume)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_buffer_mutex);
|
||||
m_output_volume = volume;
|
||||
}
|
||||
|
||||
void AudioStream::PauseOutput(bool paused)
|
||||
{
|
||||
if (m_output_paused == paused)
|
||||
return;
|
||||
|
||||
PauseDevice(paused);
|
||||
m_output_paused = paused;
|
||||
|
||||
// Empty buffers on pause.
|
||||
if (paused)
|
||||
EmptyBuffers();
|
||||
}
|
||||
|
||||
void AudioStream::Shutdown()
|
||||
{
|
||||
if (!IsDeviceOpen())
|
||||
return;
|
||||
|
||||
CloseDevice();
|
||||
EmptyBuffers();
|
||||
m_buffer_size = 0;
|
||||
m_output_sample_rate = 0;
|
||||
m_channels = 0;
|
||||
m_output_paused = true;
|
||||
m_volume = volume;
|
||||
}
|
||||
|
||||
void AudioStream::BeginWrite(SampleType** buffer_ptr, u32* num_frames)
|
||||
{
|
||||
m_buffer_mutex.lock();
|
||||
|
||||
const u32 requested_frames = std::min(*num_frames, m_buffer_size);
|
||||
EnsureBuffer(requested_frames * m_channels);
|
||||
|
||||
*buffer_ptr = m_buffer.GetWritePointer();
|
||||
*num_frames = std::min(m_buffer_size, m_buffer.GetContiguousSpace() / m_channels);
|
||||
// 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;
|
||||
}
|
||||
|
||||
void AudioStream::WriteFrames(const SampleType* frames, u32 num_frames)
|
||||
{
|
||||
Assert(num_frames <= m_buffer_size);
|
||||
const u32 num_samples = num_frames * m_channels;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_buffer_mutex);
|
||||
EnsureBuffer(num_samples);
|
||||
m_buffer.PushRange(frames, num_samples);
|
||||
}
|
||||
|
||||
FramesAvailable();
|
||||
Panic("not implemented");
|
||||
}
|
||||
|
||||
void AudioStream::EndWrite(u32 num_frames)
|
||||
{
|
||||
m_buffer.AdvanceTail(num_frames * m_channels);
|
||||
if (m_buffer_filling.load())
|
||||
{
|
||||
if ((m_buffer.GetSize() / m_channels) >= m_buffer_size)
|
||||
m_buffer_filling.store(false);
|
||||
}
|
||||
m_buffer_mutex.unlock();
|
||||
FramesAvailable();
|
||||
}
|
||||
|
||||
float AudioStream::GetMaxLatency(u32 sample_rate, u32 buffer_size)
|
||||
{
|
||||
return (static_cast<float>(buffer_size) / static_cast<float>(sample_rate));
|
||||
}
|
||||
|
||||
bool AudioStream::SetBufferSize(u32 buffer_size)
|
||||
{
|
||||
const u32 buffer_size_in_samples = buffer_size * m_channels;
|
||||
const u32 max_samples = buffer_size_in_samples * 2u;
|
||||
if (max_samples > m_buffer.GetCapacity())
|
||||
return false;
|
||||
|
||||
m_buffer_size = buffer_size;
|
||||
m_max_samples = max_samples;
|
||||
return true;
|
||||
}
|
||||
|
||||
u32 AudioStream::GetSamplesAvailable() const
|
||||
{
|
||||
// TODO: Use atomic loads
|
||||
u32 available_samples;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_buffer_mutex);
|
||||
available_samples = m_buffer.GetSize();
|
||||
}
|
||||
|
||||
return available_samples / m_channels;
|
||||
}
|
||||
|
||||
u32 AudioStream::GetSamplesAvailableLocked() const
|
||||
{
|
||||
return m_buffer.GetSize() / m_channels;
|
||||
}
|
||||
|
||||
void AudioStream::ReadFrames(SampleType* samples, u32 num_frames, bool apply_volume)
|
||||
{
|
||||
const u32 total_samples = num_frames * m_channels;
|
||||
u32 samples_copied = 0;
|
||||
std::unique_lock<std::mutex> buffer_lock(m_buffer_mutex);
|
||||
if (!m_buffer_filling.load())
|
||||
{
|
||||
if (m_input_sample_rate == m_output_sample_rate)
|
||||
{
|
||||
samples_copied = std::min(m_buffer.GetSize(), total_samples);
|
||||
if (samples_copied > 0)
|
||||
m_buffer.PopRange(samples, samples_copied);
|
||||
|
||||
ReleaseBufferLock(std::move(buffer_lock));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (m_resampled_buffer.GetSize() < total_samples)
|
||||
ResampleInput(std::move(buffer_lock));
|
||||
else
|
||||
ReleaseBufferLock(std::move(buffer_lock));
|
||||
|
||||
samples_copied = std::min(m_resampled_buffer.GetSize(), total_samples);
|
||||
if (samples_copied > 0)
|
||||
m_resampled_buffer.PopRange(samples, samples_copied);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ReleaseBufferLock(std::move(buffer_lock));
|
||||
}
|
||||
|
||||
if (samples_copied < total_samples)
|
||||
{
|
||||
if (samples_copied > 0)
|
||||
{
|
||||
m_resample_buffer.resize(samples_copied);
|
||||
std::memcpy(m_resample_buffer.data(), samples, sizeof(SampleType) * samples_copied);
|
||||
|
||||
// super basic resampler - spread the input samples evenly across the output samples. will sound like ass and have
|
||||
// aliasing, but better than popping by inserting silence.
|
||||
const u32 increment =
|
||||
static_cast<u32>(65536.0f * (static_cast<float>(samples_copied / m_channels) / static_cast<float>(num_frames)));
|
||||
|
||||
SampleType* out_ptr = samples;
|
||||
const SampleType* resample_ptr = m_resample_buffer.data();
|
||||
const u32 copy_stride = sizeof(SampleType) * m_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;
|
||||
|
||||
resample_subpos += increment;
|
||||
resample_ptr += (resample_subpos >> 16) * m_channels;
|
||||
resample_subpos %= 65536u;
|
||||
}
|
||||
|
||||
Log_VerbosePrintf("Audio buffer underflow, resampled %u frames to %u", samples_copied / m_channels, num_frames);
|
||||
m_underflow_flag.store(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
// read nothing, so zero-fill
|
||||
std::memset(samples, 0, sizeof(SampleType) * total_samples);
|
||||
Log_VerbosePrintf("Audio buffer underflow with no samples, added %u frames silence", num_frames);
|
||||
m_underflow_flag.store(true);
|
||||
}
|
||||
|
||||
m_buffer_filling.store(m_wait_for_buffer_fill);
|
||||
}
|
||||
|
||||
if (apply_volume && m_output_volume != FullVolume)
|
||||
{
|
||||
SampleType* current_ptr = samples;
|
||||
const SampleType* end_ptr = samples + (num_frames * m_channels);
|
||||
while (current_ptr != end_ptr)
|
||||
{
|
||||
*current_ptr = ApplyVolume(*current_ptr, m_output_volume);
|
||||
current_ptr++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AudioStream::EnsureBuffer(u32 size)
|
||||
{
|
||||
DebugAssert(size <= (m_buffer_size * m_channels));
|
||||
if (GetBufferSpace() >= size)
|
||||
// don't bother committing anything when muted
|
||||
if (m_volume == 0)
|
||||
return;
|
||||
|
||||
if (m_sync)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_buffer_mutex, std::adopt_lock);
|
||||
m_buffer_draining_cv.wait(lock, [this, size]() { return GetBufferSpace() >= size; });
|
||||
lock.release();
|
||||
}
|
||||
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(_M_ARM64) || defined(__aarch64__)
|
||||
|
||||
static void S16ChunkToFloat(const s32* src, float* dst)
|
||||
{
|
||||
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);
|
||||
|
||||
for (u32 i = 0; i < iterations; i++)
|
||||
{
|
||||
m_buffer.Remove(size);
|
||||
const int16x8_t sv = vreinterpretq_s16_s32(vld1q_s32(src));
|
||||
src += 4;
|
||||
|
||||
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]
|
||||
iv1 = vshrq_n_s32(iv1, 16); // [0, 1, 2, 3]
|
||||
iv2 = vshrq_n_s32(iv2, 16); // [4, 5, 6, 7]
|
||||
float32x4_t fv1 = vcvtq_f32_s32(iv1); // [f0, f1, f2, f3]
|
||||
float32x4_t fv2 = vcvtq_f32_s32(iv2); // [f4, f5, f6, f7]
|
||||
fv1 = vmulq_f32(fv1, S16_TO_FLOAT_V);
|
||||
fv2 = vmulq_f32(fv2, S16_TO_FLOAT_V);
|
||||
|
||||
vst1q_f32(dst + 0, fv1);
|
||||
vst1q_f32(dst + 4, fv2);
|
||||
dst += 8;
|
||||
}
|
||||
}
|
||||
|
||||
void AudioStream::DropFrames(u32 count)
|
||||
static void FloatChunkToS16(s32* dst, const float* src, uint size)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_buffer_mutex);
|
||||
m_buffer.Remove(count);
|
||||
}
|
||||
static_assert((AudioStream::CHUNK_SIZE % 4) == 0);
|
||||
constexpr u32 iterations = AudioStream::CHUNK_SIZE / 4;
|
||||
|
||||
void AudioStream::EmptyBuffers()
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_buffer_mutex);
|
||||
std::unique_lock<std::mutex> resampler_lock(m_resampler_mutex);
|
||||
LockedEmptyBuffers();
|
||||
}
|
||||
const float32x4_t FLOAT_TO_S16_V = vdupq_n_f32(FLOAT_TO_S16);
|
||||
|
||||
void AudioStream::LockedEmptyBuffers()
|
||||
{
|
||||
m_buffer.Clear();
|
||||
m_underflow_flag.store(false);
|
||||
m_buffer_filling.store(m_wait_for_buffer_fill);
|
||||
ResetResampler();
|
||||
}
|
||||
|
||||
void AudioStream::CreateResampler()
|
||||
{
|
||||
m_resampler_state = src_new(SRC_SINC_MEDIUM_QUALITY, static_cast<int>(m_channels), nullptr);
|
||||
if (!m_resampler_state)
|
||||
Panic("Failed to allocate resampler");
|
||||
}
|
||||
|
||||
void AudioStream::DestroyResampler()
|
||||
{
|
||||
if (m_resampler_state)
|
||||
for (u32 i = 0; i < iterations; i++)
|
||||
{
|
||||
src_delete(static_cast<SRC_STATE*>(m_resampler_state));
|
||||
m_resampler_state = nullptr;
|
||||
float32x4_t fv1 = vld1q_s32(src + 0);
|
||||
float32x4_t fv2 = vld1q_s32(src + 4);
|
||||
src += 8;
|
||||
|
||||
fv1 = vmulq_f32(fv1, FLOAT_TO_S16_V);
|
||||
fv2 = vmulq_f32(fv2, FLOAT_TO_S16_V);
|
||||
int32x4_t iv1 = vcvtq_s32_f32(fv1);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
void AudioStream::ResetResampler()
|
||||
#elif defined(_M_IX86) || defined(_M_AMD64)
|
||||
|
||||
static void S16ChunkToFloat(const s32* src, float* dst)
|
||||
{
|
||||
m_resampled_buffer.Clear();
|
||||
m_resample_in_buffer.clear();
|
||||
m_resample_out_buffer.clear();
|
||||
src_reset(static_cast<SRC_STATE*>(m_resampler_state));
|
||||
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);
|
||||
|
||||
for (u32 i = 0; i < iterations; i++)
|
||||
{
|
||||
const __m128i sv = _mm_load_si128(reinterpret_cast<const __m128i*>(src));
|
||||
src += 4;
|
||||
|
||||
__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]
|
||||
iv1 = _mm_srai_epi32(iv1, 16); // [0, 1, 2, 3]
|
||||
iv2 = _mm_srai_epi32(iv2, 16); // [4, 5, 6, 7]
|
||||
__m128 fv1 = _mm_cvtepi32_ps(iv1); // [f0, f1, f2, f3]
|
||||
__m128 fv2 = _mm_cvtepi32_ps(iv2); // [f4, f5, f6, f7]
|
||||
fv1 = _mm_mul_ps(fv1, S16_TO_FLOAT_V);
|
||||
fv2 = _mm_mul_ps(fv2, S16_TO_FLOAT_V);
|
||||
|
||||
_mm_store_ps(dst + 0, fv1);
|
||||
_mm_store_ps(dst + 4, fv2);
|
||||
dst += 8;
|
||||
}
|
||||
}
|
||||
|
||||
void AudioStream::ResampleInput(std::unique_lock<std::mutex> buffer_lock)
|
||||
static void FloatChunkToS16(s32* dst, const float* src, uint size)
|
||||
{
|
||||
std::unique_lock<std::mutex> resampler_lock(m_resampler_mutex);
|
||||
static_assert((AudioStream::CHUNK_SIZE % 4) == 0);
|
||||
constexpr u32 iterations = AudioStream::CHUNK_SIZE / 4;
|
||||
|
||||
const u32 input_space_from_output = (m_resampled_buffer.GetSpace() * m_output_sample_rate) / m_input_sample_rate;
|
||||
u32 remaining = std::min(m_buffer.GetSize(), input_space_from_output);
|
||||
if (m_resample_in_buffer.size() < remaining)
|
||||
const __m128 FLOAT_TO_S16_V = _mm_set1_ps(FLOAT_TO_S16);
|
||||
|
||||
for (u32 i = 0; i < iterations; i++)
|
||||
{
|
||||
remaining -= static_cast<u32>(m_resample_in_buffer.size());
|
||||
m_resample_in_buffer.reserve(m_resample_in_buffer.size() + remaining);
|
||||
while (remaining > 0)
|
||||
__m128 fv1 = _mm_load_ps(src + 0);
|
||||
__m128 fv2 = _mm_load_ps(src + 4);
|
||||
src += 8;
|
||||
|
||||
fv1 = _mm_mul_ps(fv1, FLOAT_TO_S16_V);
|
||||
fv2 = _mm_mul_ps(fv2, FLOAT_TO_S16_V);
|
||||
__m128i iv1 = _mm_cvtps_epi32(fv1);
|
||||
__m128i iv2 = _mm_cvtps_epi32(fv2);
|
||||
|
||||
__m128i iv = _mm_packs_epi32(iv1, iv2);
|
||||
_mm_store_si128(reinterpret_cast<__m128i*>(dst), iv);
|
||||
dst += 4;
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
static void S16ChunkToFloat(const s32* src, float* dst)
|
||||
{
|
||||
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++;
|
||||
}
|
||||
}
|
||||
|
||||
static void FloatChunkToS16(s32* dst, const float* src, uint size)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Time stretching algorithm based on PCSX2 implementation.
|
||||
|
||||
template<class T>
|
||||
ALWAYS_INLINE static bool IsInRange(const T& val, const T& min, const T& max)
|
||||
{
|
||||
return (min <= val && val <= max);
|
||||
}
|
||||
|
||||
void AudioStream::StretchAllocate()
|
||||
{
|
||||
if (m_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->setSetting(SETTING_USE_QUICKSEEK, 0);
|
||||
m_soundtouch->setSetting(SETTING_USE_AA_FILTER, 0);
|
||||
|
||||
m_soundtouch->setSetting(SETTING_SEQUENCE_MS, 30);
|
||||
m_soundtouch->setSetting(SETTING_SEEKWINDOW_MS, 20);
|
||||
m_soundtouch->setSetting(SETTING_OVERLAP_MS, 10);
|
||||
|
||||
if (m_stretch_mode == AudioStretchMode::Resample)
|
||||
m_soundtouch->setRate(m_nominal_rate);
|
||||
else
|
||||
m_soundtouch->setTempo(m_nominal_rate);
|
||||
|
||||
m_stretch_reset = STRETCH_RESET_THRESHOLD;
|
||||
m_stretch_inactive = false;
|
||||
m_stretch_ok_count = 0;
|
||||
m_dynamic_target_usage = 0.0f;
|
||||
m_average_position = 0;
|
||||
m_average_available = 0;
|
||||
|
||||
m_staging_buffer_pos = 0;
|
||||
}
|
||||
|
||||
void AudioStream::StretchDestroy()
|
||||
{
|
||||
m_soundtouch.reset();
|
||||
}
|
||||
|
||||
void AudioStream::StretchWrite()
|
||||
{
|
||||
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)
|
||||
{
|
||||
FloatChunkToS16(m_staging_buffer.data(), m_float_buffer.data(), tempProgress);
|
||||
InternalWriteFrames(m_staging_buffer.data(), tempProgress);
|
||||
}
|
||||
|
||||
if (m_stretch_mode == AudioStretchMode::TimeStretch)
|
||||
UpdateStretchTempo();
|
||||
}
|
||||
|
||||
float AudioStream::AddAndGetAverageTempo(float val)
|
||||
{
|
||||
if (m_stretch_reset >= STRETCH_RESET_THRESHOLD)
|
||||
m_average_available = 0;
|
||||
if (m_average_available < AVERAGING_BUFFER_SIZE)
|
||||
m_average_available++;
|
||||
|
||||
m_average_fullness[m_average_position] = val;
|
||||
m_average_position = (m_average_position + 1U) % AVERAGING_BUFFER_SIZE;
|
||||
|
||||
const u32 actual_window = std::min<u32>(m_average_available, AVERAGING_WINDOW);
|
||||
const u32 first_index = (m_average_position - actual_window + AVERAGING_BUFFER_SIZE) % AVERAGING_BUFFER_SIZE;
|
||||
|
||||
float sum = 0;
|
||||
for (u32 i = first_index; i < first_index + actual_window; i++)
|
||||
sum += m_average_fullness[i % AVERAGING_BUFFER_SIZE];
|
||||
sum = sum / actual_window;
|
||||
|
||||
return (sum != 0.0f) ? sum : 1.0f;
|
||||
}
|
||||
|
||||
void AudioStream::UpdateStretchTempo()
|
||||
{
|
||||
static constexpr float MIN_TEMPO = 0.05f;
|
||||
static constexpr float MAX_TEMPO = 50.0f;
|
||||
|
||||
// Which range we will run in 1:1 mode for.
|
||||
static constexpr float INACTIVE_GOOD_FACTOR = 1.04f;
|
||||
static constexpr float INACTIVE_BAD_FACTOR = 1.2f;
|
||||
static constexpr u32 INACTIVE_MIN_OK_COUNT = 50;
|
||||
static constexpr u32 COMPENSATION_DIVIDER = 100;
|
||||
|
||||
float base_target_usage = static_cast<float>(m_target_buffer_size) * m_nominal_rate;
|
||||
|
||||
// state vars
|
||||
if (m_stretch_reset >= STRETCH_RESET_THRESHOLD)
|
||||
{
|
||||
Log_VerbosePrintf("___ Stretcher is being reset.");
|
||||
m_stretch_inactive = false;
|
||||
m_stretch_ok_count = 0;
|
||||
m_dynamic_target_usage = base_target_usage;
|
||||
}
|
||||
|
||||
const u32 ibuffer_usage = GetBufferedFramesRelaxed();
|
||||
float buffer_usage = static_cast<float>(ibuffer_usage);
|
||||
float tempo = buffer_usage / m_dynamic_target_usage;
|
||||
tempo = AddAndGetAverageTempo(tempo);
|
||||
|
||||
// Dampening when we get close to target.
|
||||
if (tempo < 2.0f)
|
||||
tempo = std::sqrt(tempo);
|
||||
|
||||
tempo = std::clamp(tempo, MIN_TEMPO, MAX_TEMPO);
|
||||
|
||||
if (tempo < 1.0f)
|
||||
base_target_usage /= std::sqrt(tempo);
|
||||
|
||||
m_dynamic_target_usage +=
|
||||
static_cast<float>(base_target_usage / tempo - m_dynamic_target_usage) / static_cast<float>(COMPENSATION_DIVIDER);
|
||||
if (IsInRange(tempo, 0.9f, 1.1f) &&
|
||||
IsInRange(m_dynamic_target_usage, base_target_usage * 0.9f, base_target_usage * 1.1f))
|
||||
{
|
||||
m_dynamic_target_usage = base_target_usage;
|
||||
}
|
||||
|
||||
if (!m_stretch_inactive)
|
||||
{
|
||||
if (IsInRange(tempo, 1.0f / INACTIVE_GOOD_FACTOR, INACTIVE_GOOD_FACTOR))
|
||||
m_stretch_ok_count++;
|
||||
else
|
||||
m_stretch_ok_count = 0;
|
||||
|
||||
if (m_stretch_ok_count >= INACTIVE_MIN_OK_COUNT)
|
||||
{
|
||||
const u32 read_len = std::min(m_buffer.GetContiguousSize(), remaining);
|
||||
const size_t old_pos = m_resample_in_buffer.size();
|
||||
m_resample_in_buffer.resize(m_resample_in_buffer.size() + read_len);
|
||||
src_short_to_float_array(m_buffer.GetReadPointer(), m_resample_in_buffer.data() + old_pos,
|
||||
static_cast<int>(read_len));
|
||||
m_buffer.Remove(read_len);
|
||||
remaining -= read_len;
|
||||
Log_VerbosePrintf("=== Stretcher is now inactive.");
|
||||
m_stretch_inactive = true;
|
||||
}
|
||||
}
|
||||
|
||||
ReleaseBufferLock(std::move(buffer_lock));
|
||||
|
||||
const u32 potential_output_size =
|
||||
(static_cast<u32>(m_resample_in_buffer.size()) * m_input_sample_rate) / m_output_sample_rate;
|
||||
const u32 output_size = std::min(potential_output_size, m_resampled_buffer.GetSpace());
|
||||
m_resample_out_buffer.resize(output_size);
|
||||
|
||||
SRC_DATA sd = {};
|
||||
sd.data_in = m_resample_in_buffer.data();
|
||||
sd.data_out = m_resample_out_buffer.data();
|
||||
sd.input_frames = static_cast<u32>(m_resample_in_buffer.size()) / m_channels;
|
||||
sd.output_frames = output_size / m_channels;
|
||||
sd.src_ratio = m_resampler_ratio;
|
||||
|
||||
const int error = src_process(static_cast<SRC_STATE*>(m_resampler_state), &sd);
|
||||
if (error)
|
||||
else if (!IsInRange(tempo, 1.0f / INACTIVE_BAD_FACTOR, INACTIVE_BAD_FACTOR))
|
||||
{
|
||||
Log_ErrorPrintf("Resampler error %d", error);
|
||||
m_resample_in_buffer.clear();
|
||||
m_resample_out_buffer.clear();
|
||||
return;
|
||||
Log_VerbosePrintf("~~~ Stretcher is now active @ tempo %f.", tempo);
|
||||
m_stretch_inactive = false;
|
||||
m_stretch_ok_count = 0;
|
||||
}
|
||||
|
||||
m_resample_in_buffer.erase(m_resample_in_buffer.begin(),
|
||||
m_resample_in_buffer.begin() + (static_cast<u32>(sd.input_frames_used) * m_channels));
|
||||
if (m_stretch_inactive)
|
||||
tempo = m_nominal_rate;
|
||||
|
||||
const float* write_ptr = m_resample_out_buffer.data();
|
||||
remaining = static_cast<u32>(sd.output_frames_gen) * m_channels;
|
||||
while (remaining > 0)
|
||||
if constexpr (LOG_TIMESTRETCH_STATS)
|
||||
{
|
||||
const u32 samples_to_write = std::min(m_resampled_buffer.GetContiguousSpace(), remaining);
|
||||
src_float_to_short_array(write_ptr, m_resampled_buffer.GetWritePointer(), static_cast<int>(samples_to_write));
|
||||
m_resampled_buffer.AdvanceTail(samples_to_write);
|
||||
write_ptr += samples_to_write;
|
||||
remaining -= samples_to_write;
|
||||
static int iterations = 0;
|
||||
static u64 last_log_time = 0;
|
||||
|
||||
const u64 now = Common::Timer::GetCurrentValue();
|
||||
|
||||
if (Common::Timer::ConvertValueToSeconds(now - last_log_time) > 1.0f)
|
||||
{
|
||||
Log_VerbosePrintf("buffers: %4u ms (%3.0f%%), tempo: %f, comp: %2.3f, iters: %d, reset:%d",
|
||||
(ibuffer_usage * 1000u) / m_sample_rate, 100.0f * buffer_usage / base_target_usage, tempo,
|
||||
m_dynamic_target_usage / base_target_usage, iterations, m_stretch_reset);
|
||||
|
||||
last_log_time = now;
|
||||
iterations = 0;
|
||||
}
|
||||
|
||||
iterations++;
|
||||
}
|
||||
m_resample_out_buffer.erase(m_resample_out_buffer.begin(),
|
||||
m_resample_out_buffer.begin() + (static_cast<u32>(sd.output_frames_gen) * m_channels));
|
||||
}
|
||||
|
||||
m_soundtouch->setTempo(tempo);
|
||||
|
||||
if (m_stretch_reset >= STRETCH_RESET_THRESHOLD)
|
||||
m_stretch_reset = 0;
|
||||
}
|
||||
|
||||
void AudioStream::StretchUnderrun()
|
||||
{
|
||||
// Didn't produce enough frames in time.
|
||||
m_stretch_reset++;
|
||||
}
|
||||
|
||||
void AudioStream::StretchOverrun()
|
||||
{
|
||||
// Produced more frames than can fit in the buffer.
|
||||
m_stretch_reset++;
|
||||
|
||||
// Drop two packets to give the time stretcher a bit more time to slow things down.
|
||||
const u32 discard = CHUNK_SIZE * 2;
|
||||
m_rpos.store((m_rpos.load(std::memory_order_acquire) + discard) % m_buffer_size, std::memory_order_release);
|
||||
}
|
||||
|
@ -1,13 +1,26 @@
|
||||
#pragma once
|
||||
#include "common/fifo_queue.h"
|
||||
#include "common/types.h"
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
|
||||
// Uses signed 16-bits samples.
|
||||
#ifdef _MSC_VER
|
||||
#pragma warning(push)
|
||||
#pragma warning(disable : 4324) // warning C4324: structure was padded due to alignment specifier
|
||||
#endif
|
||||
|
||||
namespace soundtouch {
|
||||
class SoundTouch;
|
||||
}
|
||||
|
||||
enum class AudioStretchMode : u8
|
||||
{
|
||||
Off,
|
||||
Resample,
|
||||
TimeStretch,
|
||||
Count
|
||||
};
|
||||
|
||||
class AudioStream
|
||||
{
|
||||
@ -16,111 +29,116 @@ public:
|
||||
|
||||
enum : u32
|
||||
{
|
||||
DefaultInputSampleRate = 44100,
|
||||
DefaultOutputSampleRate = 44100,
|
||||
DefaultBufferSize = 2048,
|
||||
MaxSamples = 32768,
|
||||
FullVolume = 100
|
||||
CHUNK_SIZE = 64,
|
||||
MAX_CHANNELS = 2
|
||||
};
|
||||
|
||||
AudioStream();
|
||||
public:
|
||||
virtual ~AudioStream();
|
||||
|
||||
u32 GetOutputSampleRate() const { return m_output_sample_rate; }
|
||||
u32 GetChannels() const { return m_channels; }
|
||||
u32 GetBufferSize() const { return m_buffer_size; }
|
||||
s32 GetOutputVolume() const { return m_output_volume; }
|
||||
bool IsSyncing() const { return m_sync; }
|
||||
static u32 GetAlignedBufferSize(u32 size);
|
||||
static u32 GetBufferSizeForMS(u32 sample_rate, u32 ms);
|
||||
static u32 GetMSForBufferSize(u32 sample_rate, u32 buffer_size);
|
||||
|
||||
bool Reconfigure(u32 input_sample_rate = DefaultInputSampleRate, u32 output_sample_rate = DefaultOutputSampleRate,
|
||||
u32 channels = 1, u32 buffer_size = DefaultBufferSize);
|
||||
void SetSync(bool enable) { m_sync = enable; }
|
||||
static const char* GetStretchModeName(AudioStretchMode mode);
|
||||
static std::optional<AudioStretchMode> ParseStretchMode(const char* name);
|
||||
|
||||
void SetInputSampleRate(u32 sample_rate);
|
||||
void SetWaitForBufferFill(bool enabled);
|
||||
ALWAYS_INLINE u32 GetSampleRate() const { return m_sample_rate; }
|
||||
ALWAYS_INLINE u32 GetChannels() const { return m_channels; }
|
||||
ALWAYS_INLINE u32 GetBufferSize() const { return m_buffer_size; }
|
||||
ALWAYS_INLINE u32 GetTargetBufferSize() const { return m_target_buffer_size; }
|
||||
ALWAYS_INLINE u32 GetOutputVolume() const { return m_volume; }
|
||||
ALWAYS_INLINE float GetNominalTempo() const { return m_nominal_rate; }
|
||||
ALWAYS_INLINE bool IsPaused() const { return m_paused; }
|
||||
|
||||
u32 GetBufferedFramesRelaxed() const;
|
||||
|
||||
/// Temporarily pauses the stream, preventing it from requesting data.
|
||||
virtual void SetPaused(bool paused);
|
||||
|
||||
virtual void SetOutputVolume(u32 volume);
|
||||
|
||||
void PauseOutput(bool paused);
|
||||
void EmptyBuffers();
|
||||
|
||||
void Shutdown();
|
||||
|
||||
void BeginWrite(SampleType** buffer_ptr, u32* num_frames);
|
||||
void WriteFrames(const SampleType* frames, u32 num_frames);
|
||||
void EndWrite(u32 num_frames);
|
||||
|
||||
bool DidUnderflow()
|
||||
{
|
||||
bool expected = true;
|
||||
return m_underflow_flag.compare_exchange_strong(expected, false);
|
||||
}
|
||||
void EmptyBuffer();
|
||||
|
||||
static std::unique_ptr<AudioStream> CreateNullAudioStream();
|
||||
/// Nominal rate is used for both resampling and timestretching, input samples are assumed to be this amount faster
|
||||
/// than the sample rate.
|
||||
void SetNominalRate(float tempo);
|
||||
|
||||
// Latency computation - returns values in seconds
|
||||
static float GetMaxLatency(u32 sample_rate, u32 buffer_size);
|
||||
void SetStretchMode(AudioStretchMode mode);
|
||||
|
||||
static std::unique_ptr<AudioStream> CreateNullStream(u32 sample_rate, u32 channels, u32 buffer_ms);
|
||||
|
||||
protected:
|
||||
virtual bool OpenDevice() = 0;
|
||||
virtual void PauseDevice(bool paused) = 0;
|
||||
virtual void CloseDevice() = 0;
|
||||
virtual void FramesAvailable() = 0;
|
||||
AudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, AudioStretchMode stretch);
|
||||
void BaseInitialize();
|
||||
|
||||
ALWAYS_INLINE static SampleType ApplyVolume(SampleType sample, u32 volume)
|
||||
{
|
||||
return s16((s32(sample) * s32(volume)) / 100);
|
||||
}
|
||||
void ReadFrames(s16* bData, u32 nSamples);
|
||||
|
||||
ALWAYS_INLINE u32 GetBufferSpace() const { return (m_max_samples - m_buffer.GetSize()); }
|
||||
ALWAYS_INLINE void ReleaseBufferLock(std::unique_lock<std::mutex> lock)
|
||||
{
|
||||
// lock is released implicitly by destruction
|
||||
m_buffer_draining_cv.notify_one();
|
||||
}
|
||||
|
||||
bool SetBufferSize(u32 buffer_size);
|
||||
bool IsDeviceOpen() const { return (m_output_sample_rate > 0); }
|
||||
|
||||
void EnsureBuffer(u32 size);
|
||||
void LockedEmptyBuffers();
|
||||
u32 GetSamplesAvailable() const;
|
||||
u32 GetSamplesAvailableLocked() const;
|
||||
void ReadFrames(SampleType* samples, u32 num_frames, bool apply_volume);
|
||||
void DropFrames(u32 count);
|
||||
|
||||
void CreateResampler();
|
||||
void DestroyResampler();
|
||||
void ResetResampler();
|
||||
void InternalSetInputSampleRate(u32 sample_rate);
|
||||
void ResampleInput(std::unique_lock<std::mutex> buffer_lock);
|
||||
|
||||
u32 m_input_sample_rate = 0;
|
||||
u32 m_output_sample_rate = 0;
|
||||
u32 m_sample_rate = 0;
|
||||
u32 m_channels = 0;
|
||||
u32 m_buffer_ms = 0;
|
||||
u32 m_volume = 0;
|
||||
|
||||
AudioStretchMode m_stretch_mode = AudioStretchMode::Off;
|
||||
bool m_stretch_inactive = false;
|
||||
bool m_filling = false;
|
||||
bool m_paused = false;
|
||||
|
||||
private:
|
||||
enum : u32
|
||||
{
|
||||
AVERAGING_BUFFER_SIZE = 256,
|
||||
AVERAGING_WINDOW = 50,
|
||||
STRETCH_RESET_THRESHOLD = 5,
|
||||
TARGET_IPS = 691,
|
||||
};
|
||||
|
||||
void AllocateBuffer();
|
||||
void DestroyBuffer();
|
||||
|
||||
void InternalWriteFrames(s32* bData, u32 nFrames);
|
||||
|
||||
void StretchAllocate();
|
||||
void StretchDestroy();
|
||||
void StretchWrite();
|
||||
void StretchUnderrun();
|
||||
void StretchOverrun();
|
||||
|
||||
float AddAndGetAverageTempo(float val);
|
||||
void UpdateStretchTempo();
|
||||
|
||||
u32 m_buffer_size = 0;
|
||||
std::unique_ptr<s32[]> m_buffer;
|
||||
|
||||
// volume, 0-100
|
||||
u32 m_output_volume = FullVolume;
|
||||
std::atomic<u32> m_rpos{0};
|
||||
std::atomic<u32> m_wpos{0};
|
||||
|
||||
HeapFIFOQueue<SampleType, MaxSamples> m_buffer;
|
||||
mutable std::mutex m_buffer_mutex;
|
||||
std::condition_variable m_buffer_draining_cv;
|
||||
std::vector<SampleType> m_resample_buffer;
|
||||
std::unique_ptr<soundtouch::SoundTouch> m_soundtouch;
|
||||
|
||||
std::atomic_bool m_underflow_flag{false};
|
||||
std::atomic_bool m_buffer_filling{false};
|
||||
u32 m_max_samples = 0;
|
||||
u32 m_target_buffer_size = 0;
|
||||
u32 m_stretch_reset = STRETCH_RESET_THRESHOLD;
|
||||
|
||||
bool m_output_paused = true;
|
||||
bool m_sync = true;
|
||||
bool m_wait_for_buffer_fill = false;
|
||||
u32 m_stretch_ok_count = 0;
|
||||
float m_nominal_rate = 1.0f;
|
||||
float m_dynamic_target_usage = 0.0f;
|
||||
|
||||
// Resampling
|
||||
double m_resampler_ratio = 1.0;
|
||||
void* m_resampler_state = nullptr;
|
||||
std::mutex m_resampler_mutex;
|
||||
HeapFIFOQueue<SampleType, MaxSamples> m_resampled_buffer;
|
||||
std::vector<float> m_resample_in_buffer;
|
||||
std::vector<float> m_resample_out_buffer;
|
||||
};
|
||||
u32 m_average_position = 0;
|
||||
u32 m_average_available = 0;
|
||||
u32 m_staging_buffer_pos = 0;
|
||||
|
||||
std::array<float, AVERAGING_BUFFER_SIZE> m_average_fullness = {};
|
||||
|
||||
// temporary staging buffer, used for timestretching
|
||||
alignas(16) std::array<s32, CHUNK_SIZE> m_staging_buffer;
|
||||
|
||||
// float buffer, soundtouch only accepts float samples as input
|
||||
alignas(16) std::array<float, CHUNK_SIZE * MAX_CHANNELS> m_float_buffer;
|
||||
};
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#pragma warning(pop)
|
||||
#endif
|
||||
|
@ -1,25 +0,0 @@
|
||||
#include "null_audio_stream.h"
|
||||
|
||||
NullAudioStream::NullAudioStream() = default;
|
||||
|
||||
NullAudioStream::~NullAudioStream() = default;
|
||||
|
||||
bool NullAudioStream::OpenDevice()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void NullAudioStream::PauseDevice(bool paused) {}
|
||||
|
||||
void NullAudioStream::CloseDevice() {}
|
||||
|
||||
void NullAudioStream::FramesAvailable()
|
||||
{
|
||||
// drop any buffer as soon as they're available
|
||||
DropFrames(GetSamplesAvailable());
|
||||
}
|
||||
|
||||
std::unique_ptr<AudioStream> AudioStream::CreateNullAudioStream()
|
||||
{
|
||||
return std::make_unique<NullAudioStream>();
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
#pragma once
|
||||
#include "audio_stream.h"
|
||||
|
||||
class NullAudioStream final : public AudioStream
|
||||
{
|
||||
public:
|
||||
NullAudioStream();
|
||||
~NullAudioStream();
|
||||
|
||||
protected:
|
||||
bool OpenDevice() override;
|
||||
void PauseDevice(bool paused) override;
|
||||
void CloseDevice() override;
|
||||
void FramesAvailable() override;
|
||||
};
|
@ -4,13 +4,15 @@
|
||||
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\libsamplerate\include;$(SolutionDir)dep\libchdr\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<PreprocessorDefinitions>%(PreprocessorDefinitions);SOUNDTOUCH_FLOAT_SAMPLES;SOUNDTOUCH_ALLOW_SSE</PreprocessorDefinitions>
|
||||
<PreprocessorDefinitions Condition="'$(Platform)'=='ARM64'">%(PreprocessorDefinitions);SOUNDTOUCH_USE_NEON</PreprocessorDefinitions>
|
||||
<AdditionalIncludeDirectories>$(SolutionDir)dep\soundtouch\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\libchdr\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
|
||||
<ItemDefinitionGroup>
|
||||
<Link>
|
||||
<AdditionalDependencies>$(RootBuildDir)simpleini\simpleini.lib;$(RootBuildDir)libchdr\libchdr.lib;$(RootBuildDir)libsamplerate\libsamplerate.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalDependencies>$(RootBuildDir)soundtouch\soundtouch.lib;$(RootBuildDir)simpleini\simpleini.lib;$(RootBuildDir)libchdr\libchdr.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
</Project>
|
||||
|
@ -9,7 +9,6 @@
|
||||
<ClInclude Include="ini_settings_interface.h" />
|
||||
<ClInclude Include="iso_reader.h" />
|
||||
<ClInclude Include="jit_code_buffer.h" />
|
||||
<ClInclude Include="null_audio_stream.h" />
|
||||
<ClInclude Include="pbp_types.h" />
|
||||
<ClInclude Include="memory_arena.h" />
|
||||
<ClInclude Include="page_fault_handler.h" />
|
||||
@ -38,7 +37,6 @@
|
||||
<ClCompile Include="iso_reader.cpp" />
|
||||
<ClCompile Include="jit_code_buffer.cpp" />
|
||||
<ClCompile Include="cd_subchannel_replacement.cpp" />
|
||||
<ClCompile Include="null_audio_stream.cpp" />
|
||||
<ClCompile Include="shiftjis.cpp" />
|
||||
<ClCompile Include="memory_arena.cpp" />
|
||||
<ClCompile Include="page_fault_handler.cpp" />
|
||||
|
@ -8,7 +8,6 @@
|
||||
<ClInclude Include="iso_reader.h" />
|
||||
<ClInclude Include="cd_image.h" />
|
||||
<ClInclude Include="cd_subchannel_replacement.h" />
|
||||
<ClInclude Include="null_audio_stream.h" />
|
||||
<ClInclude Include="wav_writer.h" />
|
||||
<ClInclude Include="cd_image_hasher.h" />
|
||||
<ClInclude Include="shiftjis.h" />
|
||||
@ -28,7 +27,6 @@
|
||||
<ClCompile Include="cd_image_bin.cpp" />
|
||||
<ClCompile Include="iso_reader.cpp" />
|
||||
<ClCompile Include="cd_subchannel_replacement.cpp" />
|
||||
<ClCompile Include="null_audio_stream.cpp" />
|
||||
<ClCompile Include="cd_image_chd.cpp" />
|
||||
<ClCompile Include="wav_writer.cpp" />
|
||||
<ClCompile Include="cd_image_hasher.cpp" />
|
||||
|
Reference in New Issue
Block a user