From 49fbf9bf0f54f49e35929aa1f65da19adff4ca27 Mon Sep 17 00:00:00 2001 From: Rui Pinheiro Date: Wed, 9 Jan 2019 22:21:55 +0000 Subject: [PATCH] Tweaks to buffering algorithm Increase untouched buffer timeout when some of the buffers have been touched. Might improve audio quality on games that suffered from miniscule popping even when buffering was enabled (such as DeS). In addition, made time stretching algorithm slightly more aggressive. Includes some other tiny tweaks as well. --- rpcs3/Emu/Cell/Modules/cellAudio.cpp | 102 +++++++++++++++++---------- rpcs3/Emu/Cell/Modules/cellAudio.h | 8 ++- 2 files changed, 70 insertions(+), 40 deletions(-) diff --git a/rpcs3/Emu/Cell/Modules/cellAudio.cpp b/rpcs3/Emu/Cell/Modules/cellAudio.cpp index 68ce4d4833..e8cb9d4e17 100644 --- a/rpcs3/Emu/Cell/Modules/cellAudio.cpp +++ b/rpcs3/Emu/Cell/Modules/cellAudio.cpp @@ -60,7 +60,7 @@ audio_ringbuffer::audio_ringbuffer(cell_audio_config& _cfg) , emu_paused(Emu.IsPaused()) { // Initialize buffers - if (cfg.num_allocated_buffers >= MAX_AUDIO_BUFFERS) + if (cfg.num_allocated_buffers > MAX_AUDIO_BUFFERS) { fmt::throw_exception("MAX_AUDIO_BUFFERS is too small"); } @@ -106,7 +106,7 @@ audio_ringbuffer::~audio_ringbuffer() f32 audio_ringbuffer::set_frequency_ratio(f32 new_ratio) { - if(!has_capability(AudioBackend::SET_FREQUENCY_RATIO)) + if (!has_capability(AudioBackend::SET_FREQUENCY_RATIO)) { ASSERT(new_ratio == 1.0f); frequency_ratio = 1.0f; @@ -114,7 +114,7 @@ f32 audio_ringbuffer::set_frequency_ratio(f32 new_ratio) else { frequency_ratio = backend->SetFrequencyRatio(new_ratio); - //cellAudio.trace("set_frequency_ratio(%1.2f) -> %1.2f", new_ratio, frequency_ratio); + cellAudio.error("set_frequency_ratio(%1.2f) -> %1.2f", new_ratio, frequency_ratio); } return frequency_ratio; } @@ -174,7 +174,7 @@ void audio_ringbuffer::play() return; } - if (has_capability(AudioBackend::IS_PLAYING) && playing) + if (playing && has_capability(AudioBackend::IS_PLAYING)) { return; } @@ -221,7 +221,7 @@ u64 audio_ringbuffer::update() if (Emu.IsPaused()) { // Emulator paused - if (has_capability(AudioBackend::PLAY_PAUSE_FLUSH) && playing) + if (playing && has_capability(AudioBackend::PLAY_PAUSE_FLUSH)) { backend->Pause(); } @@ -230,13 +230,14 @@ u64 audio_ringbuffer::update() else if (emu_paused) { // Emulator unpaused - if (has_capability(AudioBackend::PLAY_PAUSE_FLUSH) && enqueued_samples > 0) + if (enqueued_samples > 0 && has_capability(AudioBackend::PLAY_PAUSE_FLUSH)) { play(); } emu_paused = false; } + // Prepare timestamp and playing status const u64 timestamp = get_timestamp(); const bool new_playing = !emu_paused && get_backend_playing(); @@ -252,7 +253,6 @@ u64 audio_ringbuffer::update() { const u64 play_delta = timestamp - (play_timestamp > update_timestamp ? play_timestamp : update_timestamp); - // NOTE: Only works with a fixed sampling rate const u64 delta_samples_tmp = play_delta * static_cast(cfg.audio_sampling_rate * frequency_ratio) + last_remainder; last_remainder = delta_samples_tmp % 1'000'000; const u64 delta_samples = delta_samples_tmp / 1'000'000; @@ -283,7 +283,7 @@ u64 audio_ringbuffer::update() { if (!new_playing) { - cellAudio.warning("Audio backend stopped unexpectedly, likely due to a buffer underrun"); + cellAudio.error("Audio backend stopped unexpectedly, likely due to a buffer underrun"); flush(); playing = false; @@ -345,7 +345,6 @@ std::tuple cell_audio_thread::count_port_buffer_tags() bool retouched = false; for (u32 tag_pos = tag_first_pos, tag_nr = 0; tag_nr < PORT_BUFFER_TAG_COUNT; tag_pos += tag_delta, tag_nr++) { - // grab current value and re-tag atomically const f32 val = port_buf[tag_pos]; f32& last_val = port.last_tag_value[tag_nr]; @@ -353,7 +352,7 @@ std::tuple cell_audio_thread::count_port_buffer_tags() { last_val = val; - retouched |= tag_nr < port.prev_touched_tag_nr && port.prev_touched_tag_nr != UINT32_MAX; + retouched |= (tag_nr <= port.prev_touched_tag_nr) && port.prev_touched_tag_nr != UINT32_MAX; last_touched_tag_nr = tag_nr; } } @@ -371,13 +370,22 @@ std::tuple cell_audio_thread::count_port_buffer_tags() // we retouched, so wait at least once more to make sure no more tags get touched in_progress++; } + // buffer has been completely filled port.prev_touched_tag_nr = last_touched_tag_nr; } else if (last_touched_tag_nr == port.prev_touched_tag_nr) { - // hasn't been touched since the last call - incomplete++; + if (retouched) + { + // we retouched, so wait at least once more to make sure no more tags get touched + in_progress++; + } + else + { + // hasn't been touched since the last call + incomplete++; + } } else { @@ -392,7 +400,7 @@ std::tuple cell_audio_thread::count_port_buffer_tags() void cell_audio_thread::reset_ports(s32 offset) { - // Memset previous buffer to 0 and tag + // Memset buffer to 0 and tag for (auto& port : ports) { if (port.state != audio_port_state::started) continue; @@ -528,7 +536,7 @@ void cell_audio_thread::operator()() // Ratio between the rolling average of the audio period, and the desired audio period const f32 average_playtime_ratio = m_average_playtime / cfg.audio_buffer_length; - // Use the above adjusted ratio to decide how much buffer we should be aiming for + // Use the above average ratio to decide how much buffer we should be aiming for f32 desired_duration_adjusted = cfg.desired_buffer_duration + (cfg.audio_block_period / 2.0f); if (average_playtime_ratio < 1.0f) { @@ -547,12 +555,21 @@ void cell_audio_thread::operator()() // update frequency ratio if necessary f32 new_ratio = frequency_ratio; - if ( - (desired_duration_rate < cfg.time_stretching_threshold && desired_duration_rate < frequency_ratio - cfg.time_stretching_step) || // Reduce frequency ratio below threshold in 0.1f steps - (desired_duration_rate > frequency_ratio + cfg.time_stretching_step) // Increase frequency ratio in 0.1f steps - ) + if (desired_duration_rate < cfg.time_stretching_threshold) { - new_ratio = ringbuffer->set_frequency_ratio(std::min(desired_duration_rate, 1.0f)); + const f32 normalized_desired_duration_rate = desired_duration_rate / cfg.time_stretching_threshold; + const f32 request_ratio = normalized_desired_duration_rate * cfg.time_stretching_scale; + AUDIT(request_ratio <= 1.0f); + + // change frequency ratio in steps + if (std::abs(frequency_ratio - request_ratio) > cfg.time_stretching_step) + { + new_ratio = ringbuffer->set_frequency_ratio(request_ratio); + } + } + else if(frequency_ratio != 1.0f) + { + new_ratio = ringbuffer->set_frequency_ratio(1.0f); } if (new_ratio != frequency_ratio) @@ -579,7 +596,7 @@ void cell_audio_thread::operator()() else { // not as full as desired - const f32 multiplier = desired_duration_rate * desired_duration_rate; + const f32 multiplier = desired_duration_rate * desired_duration_rate; // quite aggressive, but helps more times than it hurts m_dynamic_period = cfg.minimum_block_period + static_cast((cfg.audio_block_period - cfg.minimum_block_period) * multiplier); } } @@ -595,7 +612,7 @@ void cell_audio_thread::operator()() if (active_ports == 0) { // no need to mix, just enqueue silence and advance time - cellAudio.trace("advancing time: no active ports, enqueued_buffers=%llu", enqueued_buffers); + cellAudio.trace("enqueuing silence: no active ports, enqueued_buffers=%llu", enqueued_buffers); if (playing) { ringbuffer->enqueue_silence(1); @@ -619,23 +636,37 @@ void cell_audio_thread::operator()() continue; } - - // We allow untouched buffers to go through after a timeout - // While we should always wait for in-progress buffers, games may sometimes "skip" audio periods entirely if they're falling behind - if(time_since_last_period < cfg.untouched_timeout) + // Games may sometimes "skip" audio periods entirely if they're falling behind (a sort of "frameskip" for audio) + // As such, if the game doesn't touch buffers for too long we advance time hoping the game recovers + if ( + (untouched == active_ports && time_since_last_period > cfg.fully_untouched_timeout) || + (time_since_last_period > cfg.partially_untouched_timeout) + ) { - cellAudio.trace("waiting: untouched=%u/%u (expected=%u), enqueued_buffers=%llu", untouched, active_ports, untouched_expected, enqueued_buffers); - thread_ctrl::wait_for(1000); - continue; - } - else if (untouched == active_ports) - { - // There's no audio in the buffers, simply advance time + // There's no audio in the buffers, simply advance time and hope the game recovers cellAudio.trace("advancing time: untouched=%u/%u (expected=%u), enqueued_buffers=%llu", untouched, active_ports, untouched_expected, enqueued_buffers); untouched_expected = untouched; advance(timestamp); continue; } + + cellAudio.trace("waiting: untouched=%u/%u (expected=%u), enqueued_buffers=%llu", untouched, active_ports, untouched_expected, enqueued_buffers); + thread_ctrl::wait_for(1000); + continue; + } + + // Fast-path for when there is no audio in the buffers + if (untouched == active_ports) + { + // There's no audio in the buffers, simply advance time + cellAudio.trace("enqueuing silence: untouched=%u/%u (expected=%u), enqueued_buffers=%llu", untouched, active_ports, untouched_expected, enqueued_buffers); + if (playing) + { + ringbuffer->enqueue_silence(1); + } + untouched_expected = untouched; + advance(timestamp); + continue; } // Wait for buffer(s) to be completely filled @@ -646,15 +677,12 @@ void cell_audio_thread::operator()() continue; } - /*cellAudio.error("time_since_last=%llu, dynamic_period=%llu => %3.2f%%, average_period=%4.2f => %3.2f%%", time_since_last_period, - m_dynamic_period, (((f32)m_dynamic_period) / cfg.audio_block_period) * 100.0f, - m_average_period, (m_average_period / cfg.audio_block_period) * 100.0f);*/ //cellAudio.error("active=%u, untouched=%u, in_progress=%d, incomplete=%d, enqueued_buffers=%u", active_ports, untouched, in_progress, incomplete, enqueued_buffers); // Store number of untouched buffers for future reference untouched_expected = untouched; - // Warn if we enqueued untouched/incomplete buffers + // Log if we enqueued untouched/incomplete buffers if (untouched > 0 || incomplete > 0) { cellAudio.trace("enqueueing: untouched=%u/%u (expected=%u), incomplete=%u/%u enqueued_buffers=%llu", untouched, active_ports, untouched_expected, incomplete, active_ports, enqueued_buffers); @@ -664,7 +692,7 @@ void cell_audio_thread::operator()() if (!playing) { // We are not playing (likely buffer underrun) - // align to 5.(3)ms on global clock + // align to 5.(3)ms on global clock - some games seem to prefer this const s64 audio_period_alignment_delta = (timestamp - m_start_time) % cfg.audio_block_period; if (audio_period_alignment_delta > cfg.period_comparison_margin) { diff --git a/rpcs3/Emu/Cell/Modules/cellAudio.h b/rpcs3/Emu/Cell/Modules/cellAudio.h index 4dc198b535..89aca5f49a 100644 --- a/rpcs3/Emu/Cell/Modules/cellAudio.h +++ b/rpcs3/Emu/Cell/Modules/cellAudio.h @@ -200,7 +200,8 @@ public: const s64 period_comparison_margin = 250; // when comparing the current period time with the desired period, if it is below this number of usecs we do not wait any longer - const u64 untouched_timeout = 2 * audio_block_period; + const u64 fully_untouched_timeout = 2 * audio_block_period; // timeout if the game has not touched any audio buffer yet + const u64 partially_untouched_timeout = 4 * audio_block_period; // timeout if the game has not touched all audio buffers yet /* * Time Stretching @@ -212,7 +213,8 @@ public: const bool time_stretching_enabled = raw_time_stretching_enabled && backend->has_capability(AudioBackend::SET_FREQUENCY_RATIO); const f32 time_stretching_threshold = g_cfg.audio.time_stretching_threshold / 100.0f; // we only apply time stretching below this buffer fill rate (adjusted for average period) - const f32 time_stretching_step = 0.1f; + const f32 time_stretching_step = 0.1f; // will only reduce/increase the frequency ratio in steps of at least this value + const f32 time_stretching_scale = 0.9f; /* * Constructor @@ -232,7 +234,7 @@ private: std::unique_ptr m_dump; std::unique_ptr buffer[MAX_AUDIO_BUFFERS]; - const float silence_buffer[8 * AUDIO_BUFFER_SAMPLES] = { 0 }; + const float silence_buffer[AUDIO_MAX_CHANNELS_COUNT * AUDIO_BUFFER_SAMPLES] = { 0 }; bool backend_open = false; bool playing = false;