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())
{
// 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<u64>(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<u32, u32, u32, u32> 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<u32, u32, u32, u32> 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<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
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<u32, u32, u32, u32> 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<u64>((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)
{

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 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<AudioDumper> m_dump;
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 playing = false;