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.
This commit is contained in:
Rui Pinheiro 2019-01-09 22:21:55 +00:00 committed by kd-11
parent 1e4513e2e3
commit 49fbf9bf0f
2 changed files with 70 additions and 40 deletions

View file

@ -60,7 +60,7 @@ audio_ringbuffer::audio_ringbuffer(cell_audio_config& _cfg)
, emu_paused(Emu.IsPaused()) , emu_paused(Emu.IsPaused())
{ {
// Initialize buffers // 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"); 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) 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); ASSERT(new_ratio == 1.0f);
frequency_ratio = 1.0f; frequency_ratio = 1.0f;
@ -114,7 +114,7 @@ f32 audio_ringbuffer::set_frequency_ratio(f32 new_ratio)
else else
{ {
frequency_ratio = backend->SetFrequencyRatio(new_ratio); 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; return frequency_ratio;
} }
@ -174,7 +174,7 @@ void audio_ringbuffer::play()
return; return;
} }
if (has_capability(AudioBackend::IS_PLAYING) && playing) if (playing && has_capability(AudioBackend::IS_PLAYING))
{ {
return; return;
} }
@ -221,7 +221,7 @@ u64 audio_ringbuffer::update()
if (Emu.IsPaused()) if (Emu.IsPaused())
{ {
// Emulator paused // Emulator paused
if (has_capability(AudioBackend::PLAY_PAUSE_FLUSH) && playing) if (playing && has_capability(AudioBackend::PLAY_PAUSE_FLUSH))
{ {
backend->Pause(); backend->Pause();
} }
@ -230,13 +230,14 @@ u64 audio_ringbuffer::update()
else if (emu_paused) else if (emu_paused)
{ {
// Emulator unpaused // Emulator unpaused
if (has_capability(AudioBackend::PLAY_PAUSE_FLUSH) && enqueued_samples > 0) if (enqueued_samples > 0 && has_capability(AudioBackend::PLAY_PAUSE_FLUSH))
{ {
play(); play();
} }
emu_paused = false; emu_paused = false;
} }
// Prepare timestamp and playing status
const u64 timestamp = get_timestamp(); const u64 timestamp = get_timestamp();
const bool new_playing = !emu_paused && get_backend_playing(); 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); 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<u64>(cfg.audio_sampling_rate * frequency_ratio) + last_remainder; const u64 delta_samples_tmp = play_delta * static_cast<u64>(cfg.audio_sampling_rate * frequency_ratio) + last_remainder;
last_remainder = delta_samples_tmp % 1'000'000; last_remainder = delta_samples_tmp % 1'000'000;
const u64 delta_samples = 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) 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(); flush();
playing = false; playing = false;
@ -345,7 +345,6 @@ std::tuple<u32, u32, u32, u32> cell_audio_thread::count_port_buffer_tags()
bool retouched = false; 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++) 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]; const f32 val = port_buf[tag_pos];
f32& last_val = port.last_tag_value[tag_nr]; f32& last_val = port.last_tag_value[tag_nr];
@ -353,7 +352,7 @@ std::tuple<u32, u32, u32, u32> cell_audio_thread::count_port_buffer_tags()
{ {
last_val = val; 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; last_touched_tag_nr = tag_nr;
} }
} }
@ -371,13 +370,22 @@ std::tuple<u32, u32, u32, u32> cell_audio_thread::count_port_buffer_tags()
// we retouched, so wait at least once more to make sure no more tags get touched // we retouched, so wait at least once more to make sure no more tags get touched
in_progress++; in_progress++;
} }
// buffer has been completely filled // buffer has been completely filled
port.prev_touched_tag_nr = last_touched_tag_nr; port.prev_touched_tag_nr = last_touched_tag_nr;
} }
else if (last_touched_tag_nr == port.prev_touched_tag_nr) else if (last_touched_tag_nr == port.prev_touched_tag_nr)
{ {
// hasn't been touched since the last call if (retouched)
incomplete++; {
// 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 else
{ {
@ -392,7 +400,7 @@ std::tuple<u32, u32, u32, u32> cell_audio_thread::count_port_buffer_tags()
void cell_audio_thread::reset_ports(s32 offset) 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) for (auto& port : ports)
{ {
if (port.state != audio_port_state::started) continue; 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 // 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; 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); f32 desired_duration_adjusted = cfg.desired_buffer_duration + (cfg.audio_block_period / 2.0f);
if (average_playtime_ratio < 1.0f) if (average_playtime_ratio < 1.0f)
{ {
@ -547,12 +555,21 @@ void cell_audio_thread::operator()()
// update frequency ratio if necessary // update frequency ratio if necessary
f32 new_ratio = frequency_ratio; f32 new_ratio = frequency_ratio;
if ( if (desired_duration_rate < cfg.time_stretching_threshold)
(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
)
{ {
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) if (new_ratio != frequency_ratio)
@ -579,7 +596,7 @@ void cell_audio_thread::operator()()
else else
{ {
// not as full as desired // 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<u64>((cfg.audio_block_period - cfg.minimum_block_period) * multiplier); m_dynamic_period = cfg.minimum_block_period + static_cast<u64>((cfg.audio_block_period - cfg.minimum_block_period) * multiplier);
} }
} }
@ -595,7 +612,7 @@ void cell_audio_thread::operator()()
if (active_ports == 0) if (active_ports == 0)
{ {
// no need to mix, just enqueue silence and advance time // 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) if (playing)
{ {
ringbuffer->enqueue_silence(1); ringbuffer->enqueue_silence(1);
@ -619,23 +636,37 @@ void cell_audio_thread::operator()()
continue; continue;
} }
// Games may sometimes "skip" audio periods entirely if they're falling behind (a sort of "frameskip" for audio)
// We allow untouched buffers to go through after a timeout // As such, if the game doesn't touch buffers for too long we advance time hoping the game recovers
// While we should always wait for in-progress buffers, games may sometimes "skip" audio periods entirely if they're falling behind if (
if(time_since_last_period < cfg.untouched_timeout) (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); // There's no audio in the buffers, simply advance time and hope the game recovers
thread_ctrl::wait_for(1000);
continue;
}
else if (untouched == active_ports)
{
// There's no audio in the buffers, simply advance time
cellAudio.trace("advancing time: untouched=%u/%u (expected=%u), enqueued_buffers=%llu", untouched, active_ports, untouched_expected, enqueued_buffers); cellAudio.trace("advancing time: untouched=%u/%u (expected=%u), enqueued_buffers=%llu", untouched, active_ports, untouched_expected, enqueued_buffers);
untouched_expected = untouched; untouched_expected = untouched;
advance(timestamp); advance(timestamp);
continue; 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 // Wait for buffer(s) to be completely filled
@ -646,15 +677,12 @@ void cell_audio_thread::operator()()
continue; 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); //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 // Store number of untouched buffers for future reference
untouched_expected = untouched; untouched_expected = untouched;
// Warn if we enqueued untouched/incomplete buffers // Log if we enqueued untouched/incomplete buffers
if (untouched > 0 || incomplete > 0) 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); 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) if (!playing)
{ {
// We are not playing (likely buffer underrun) // 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; const s64 audio_period_alignment_delta = (timestamp - m_start_time) % cfg.audio_block_period;
if (audio_period_alignment_delta > cfg.period_comparison_margin) if (audio_period_alignment_delta > cfg.period_comparison_margin)
{ {

View file

@ -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 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 * Time Stretching
@ -212,7 +213,8 @@ public:
const bool time_stretching_enabled = raw_time_stretching_enabled && backend->has_capability(AudioBackend::SET_FREQUENCY_RATIO); 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_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 * Constructor
@ -232,7 +234,7 @@ private:
std::unique_ptr<AudioDumper> m_dump; std::unique_ptr<AudioDumper> m_dump;
std::unique_ptr<float[]> buffer[MAX_AUDIO_BUFFERS]; std::unique_ptr<float[]> 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 backend_open = false;
bool playing = false; bool playing = false;