Decouple audio processing and run at variable rate
Currently, processing of audio samples is called from AudioRenderer's Update method, using a fixed 4 buffers to process the given samples. Games call Update at variable rates, depending on framerate and/or sample count, which causes inconsistency in audio processing. From what I've seen, 60 FPS games update every ~0.004s, but 30 FPS/160 sample games update somewhere between 0.02 and 0.04, 5-10x slower. Not enough samples get fed to the backend, leading to a lot of audio skipping. This PR seeks to address this by de-coupling the audio consumption and the audio update. Update remains the same without calling for buffer queuing, and the consume now schedules itself to run based on the sample rate and count.
This commit is contained in:
parent
432fab7c4f
commit
0857d6a3db
3 changed files with 124 additions and 88 deletions
|
@ -12,6 +12,7 @@
|
||||||
#include "audio_core/voice_context.h"
|
#include "audio_core/voice_context.h"
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
#include "common/settings.h"
|
#include "common/settings.h"
|
||||||
|
#include "core/core_timing.h"
|
||||||
#include "core/memory.h"
|
#include "core/memory.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
@ -68,7 +69,9 @@ namespace {
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
namespace AudioCore {
|
namespace AudioCore {
|
||||||
AudioRenderer::AudioRenderer(Core::Timing::CoreTiming& core_timing, Core::Memory::Memory& memory_,
|
constexpr s32 NUM_BUFFERS = 2;
|
||||||
|
|
||||||
|
AudioRenderer::AudioRenderer(Core::Timing::CoreTiming& core_timing_, Core::Memory::Memory& memory_,
|
||||||
AudioCommon::AudioRendererParameter params,
|
AudioCommon::AudioRendererParameter params,
|
||||||
Stream::ReleaseCallback&& release_callback,
|
Stream::ReleaseCallback&& release_callback,
|
||||||
std::size_t instance_number)
|
std::size_t instance_number)
|
||||||
|
@ -77,7 +80,8 @@ AudioRenderer::AudioRenderer(Core::Timing::CoreTiming& core_timing, Core::Memory
|
||||||
sink_context(params.sink_count), splitter_context(),
|
sink_context(params.sink_count), splitter_context(),
|
||||||
voices(params.voice_count), memory{memory_},
|
voices(params.voice_count), memory{memory_},
|
||||||
command_generator(worker_params, voice_context, mix_context, splitter_context, effect_context,
|
command_generator(worker_params, voice_context, mix_context, splitter_context, effect_context,
|
||||||
memory) {
|
memory),
|
||||||
|
core_timing{core_timing_} {
|
||||||
behavior_info.SetUserRevision(params.revision);
|
behavior_info.SetUserRevision(params.revision);
|
||||||
splitter_context.Initialize(behavior_info, params.splitter_count,
|
splitter_context.Initialize(behavior_info, params.splitter_count,
|
||||||
params.num_splitter_send_channels);
|
params.num_splitter_send_channels);
|
||||||
|
@ -86,16 +90,27 @@ AudioRenderer::AudioRenderer(Core::Timing::CoreTiming& core_timing, Core::Memory
|
||||||
stream = audio_out->OpenStream(
|
stream = audio_out->OpenStream(
|
||||||
core_timing, params.sample_rate, AudioCommon::STREAM_NUM_CHANNELS,
|
core_timing, params.sample_rate, AudioCommon::STREAM_NUM_CHANNELS,
|
||||||
fmt::format("AudioRenderer-Instance{}", instance_number), std::move(release_callback));
|
fmt::format("AudioRenderer-Instance{}", instance_number), std::move(release_callback));
|
||||||
audio_out->StartStream(stream);
|
process_event = Core::Timing::CreateEvent(
|
||||||
|
fmt::format("AudioRenderer-Instance{}-Process", instance_number),
|
||||||
QueueMixedBuffer(0);
|
[this](std::uintptr_t, std::chrono::nanoseconds) { ReleaseAndQueueBuffers(); });
|
||||||
QueueMixedBuffer(1);
|
for (s32 i = 0; i < NUM_BUFFERS; ++i) {
|
||||||
QueueMixedBuffer(2);
|
QueueMixedBuffer(i);
|
||||||
QueueMixedBuffer(3);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AudioRenderer::~AudioRenderer() = default;
|
AudioRenderer::~AudioRenderer() = default;
|
||||||
|
|
||||||
|
ResultCode AudioRenderer::Start() {
|
||||||
|
audio_out->StartStream(stream);
|
||||||
|
ReleaseAndQueueBuffers();
|
||||||
|
return ResultSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResultCode AudioRenderer::Stop() {
|
||||||
|
audio_out->StopStream(stream);
|
||||||
|
return ResultSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
u32 AudioRenderer::GetSampleRate() const {
|
u32 AudioRenderer::GetSampleRate() const {
|
||||||
return worker_params.sample_rate;
|
return worker_params.sample_rate;
|
||||||
}
|
}
|
||||||
|
@ -114,7 +129,8 @@ Stream::State AudioRenderer::GetStreamState() const {
|
||||||
|
|
||||||
ResultCode AudioRenderer::UpdateAudioRenderer(const std::vector<u8>& input_params,
|
ResultCode AudioRenderer::UpdateAudioRenderer(const std::vector<u8>& input_params,
|
||||||
std::vector<u8>& output_params) {
|
std::vector<u8>& output_params) {
|
||||||
|
{
|
||||||
|
std::scoped_lock lock{mutex};
|
||||||
InfoUpdater info_updater{input_params, output_params, behavior_info};
|
InfoUpdater info_updater{input_params, output_params, behavior_info};
|
||||||
|
|
||||||
if (!info_updater.UpdateBehaviorInfo(behavior_info)) {
|
if (!info_updater.UpdateBehaviorInfo(behavior_info)) {
|
||||||
|
@ -150,8 +166,8 @@ ResultCode AudioRenderer::UpdateAudioRenderer(const std::vector<u8>& input_param
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto mix_result = info_updater.UpdateMixes(mix_context, worker_params.mix_buffer_count,
|
const auto mix_result = info_updater.UpdateMixes(
|
||||||
splitter_context, effect_context);
|
mix_context, worker_params.mix_buffer_count, splitter_context, effect_context);
|
||||||
|
|
||||||
if (mix_result.IsError()) {
|
if (mix_result.IsError()) {
|
||||||
LOG_ERROR(Audio, "Failed to update mix parameters");
|
LOG_ERROR(Audio, "Failed to update mix parameters");
|
||||||
|
@ -194,9 +210,7 @@ ResultCode AudioRenderer::UpdateAudioRenderer(const std::vector<u8>& input_param
|
||||||
LOG_ERROR(Audio, "Audio buffers were not consumed!");
|
LOG_ERROR(Audio, "Audio buffers were not consumed!");
|
||||||
return AudioCommon::Audren::ERR_INVALID_PARAMETERS;
|
return AudioCommon::Audren::ERR_INVALID_PARAMETERS;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
ReleaseAndQueueBuffers();
|
|
||||||
|
|
||||||
return ResultSuccess;
|
return ResultSuccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -315,10 +329,24 @@ void AudioRenderer::QueueMixedBuffer(Buffer::Tag tag) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioRenderer::ReleaseAndQueueBuffers() {
|
void AudioRenderer::ReleaseAndQueueBuffers() {
|
||||||
|
if (!stream->IsPlaying()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
std::scoped_lock lock{mutex};
|
||||||
const auto released_buffers{audio_out->GetTagsAndReleaseBuffers(stream)};
|
const auto released_buffers{audio_out->GetTagsAndReleaseBuffers(stream)};
|
||||||
for (const auto& tag : released_buffers) {
|
for (const auto& tag : released_buffers) {
|
||||||
QueueMixedBuffer(tag);
|
QueueMixedBuffer(tag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const f32 sample_rate = static_cast<f32>(GetSampleRate());
|
||||||
|
const f32 sample_count = static_cast<f32>(GetSampleCount());
|
||||||
|
const f32 consume_rate = sample_rate / (sample_count * (sample_count / 240));
|
||||||
|
const s32 ms = (1000 / static_cast<s32>(consume_rate)) - 1;
|
||||||
|
const std::chrono::milliseconds next_event_time(std::max(ms / NUM_BUFFERS, 1));
|
||||||
|
core_timing.ScheduleEvent(next_event_time, process_event, {});
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace AudioCore
|
} // namespace AudioCore
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "audio_core/behavior_info.h"
|
#include "audio_core/behavior_info.h"
|
||||||
|
@ -45,6 +46,8 @@ public:
|
||||||
|
|
||||||
[[nodiscard]] ResultCode UpdateAudioRenderer(const std::vector<u8>& input_params,
|
[[nodiscard]] ResultCode UpdateAudioRenderer(const std::vector<u8>& input_params,
|
||||||
std::vector<u8>& output_params);
|
std::vector<u8>& output_params);
|
||||||
|
[[nodiscard]] ResultCode Start();
|
||||||
|
[[nodiscard]] ResultCode Stop();
|
||||||
void QueueMixedBuffer(Buffer::Tag tag);
|
void QueueMixedBuffer(Buffer::Tag tag);
|
||||||
void ReleaseAndQueueBuffers();
|
void ReleaseAndQueueBuffers();
|
||||||
[[nodiscard]] u32 GetSampleRate() const;
|
[[nodiscard]] u32 GetSampleRate() const;
|
||||||
|
@ -68,6 +71,9 @@ private:
|
||||||
Core::Memory::Memory& memory;
|
Core::Memory::Memory& memory;
|
||||||
CommandGenerator command_generator;
|
CommandGenerator command_generator;
|
||||||
std::size_t elapsed_frame_count{};
|
std::size_t elapsed_frame_count{};
|
||||||
|
Core::Timing::CoreTiming& core_timing;
|
||||||
|
std::shared_ptr<Core::Timing::EventType> process_event;
|
||||||
|
std::mutex mutex;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace AudioCore
|
} // namespace AudioCore
|
||||||
|
|
|
@ -110,17 +110,19 @@ private:
|
||||||
void Start(Kernel::HLERequestContext& ctx) {
|
void Start(Kernel::HLERequestContext& ctx) {
|
||||||
LOG_WARNING(Service_Audio, "(STUBBED) called");
|
LOG_WARNING(Service_Audio, "(STUBBED) called");
|
||||||
|
|
||||||
IPC::ResponseBuilder rb{ctx, 2};
|
const auto result = renderer->Start();
|
||||||
|
|
||||||
rb.Push(ResultSuccess);
|
IPC::ResponseBuilder rb{ctx, 2};
|
||||||
|
rb.Push(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Stop(Kernel::HLERequestContext& ctx) {
|
void Stop(Kernel::HLERequestContext& ctx) {
|
||||||
LOG_WARNING(Service_Audio, "(STUBBED) called");
|
LOG_WARNING(Service_Audio, "(STUBBED) called");
|
||||||
|
|
||||||
IPC::ResponseBuilder rb{ctx, 2};
|
const auto result = renderer->Stop();
|
||||||
|
|
||||||
rb.Push(ResultSuccess);
|
IPC::ResponseBuilder rb{ctx, 2};
|
||||||
|
rb.Push(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
void QuerySystemEvent(Kernel::HLERequestContext& ctx) {
|
void QuerySystemEvent(Kernel::HLERequestContext& ctx) {
|
||||||
|
|
Loading…
Reference in a new issue