diff --git a/rpcs3/Emu/Audio/AudioBackend.h b/rpcs3/Emu/Audio/AudioBackend.h index bc07a1e46..037b36fbb 100644 --- a/rpcs3/Emu/Audio/AudioBackend.h +++ b/rpcs3/Emu/Audio/AudioBackend.h @@ -380,6 +380,23 @@ public: } } + /** + * Callbacks the backend could not fill completely, and had to pad. + * + * A padded callback is a hole in the output -- the device asked for N frames of audio and the + * emulator did not have them, so the gap is filled by repeating the last sample. That is what + * crackling IS, and until now nothing counted it: the buffer-level report samples every ten + * seconds and a starved callback lasts milliseconds, so every transient underrun passed + * between samples unseen. Counted here rather than per backend so Cubeb and Oboe report the + * same number the same way. + * + * Monotonic; readers take deltas. + */ + atomic_t m_underruns{0}; + +public: + u64 get_underruns() const { return m_underruns; } + protected: void setup_channel_layout(u32 input_channel_count, u32 output_channel_count, audio_channel_layout layout, logs::channel& log); diff --git a/rpcs3/Emu/Audio/Cubeb/CubebBackend.cpp b/rpcs3/Emu/Audio/Cubeb/CubebBackend.cpp index 192cd0718..579013114 100644 --- a/rpcs3/Emu/Audio/Cubeb/CubebBackend.cpp +++ b/rpcs3/Emu/Audio/Cubeb/CubebBackend.cpp @@ -433,6 +433,14 @@ long CubebBackend::data_cb(cubeb_stream* stream, void* user_ptr, void const* /* memcpy(cubeb->m_last_sample.data(), static_cast(output_buffer) + written - sample_size, sample_size); } + // One count per starved callback -- see AudioBackend::m_underruns. This is the hole the + // player hears as a crackle, and it was previously invisible: the buffer-level report + // samples every ten seconds and this lasts milliseconds. + if (written < bytes_req) + { + cubeb->m_underruns++; + } + for (u32 i = written; i < bytes_req; i += sample_size) { memcpy(static_cast(output_buffer) + i, cubeb->m_last_sample.data(), sample_size); diff --git a/rpcs3/Emu/Audio/Oboe/OboeBackend.cpp b/rpcs3/Emu/Audio/Oboe/OboeBackend.cpp index 78333df0e..83fffb279 100644 --- a/rpcs3/Emu/Audio/Oboe/OboeBackend.cpp +++ b/rpcs3/Emu/Audio/Oboe/OboeBackend.cpp @@ -274,6 +274,13 @@ oboe::DataCallbackResult OboeBackend::onAudioReady(oboe::AudioStream* /*stream*/ } } + // One count per starved callback, not per padded frame: the audible event is the gap, and + // its length is already implied by how much of the callback had to be invented. + if (written < bytes_req) + { + m_underruns++; + } + // Pad the remainder by repeating the last sample. Holding a value is much less audible // than a hole, and the emulator legitimately runs dry whenever it stalls. for (u32 i = written; i < bytes_req; i += sample_size) diff --git a/rpcs3/Emu/Cell/Modules/cellAudio.cpp b/rpcs3/Emu/Cell/Modules/cellAudio.cpp index 2983f4895..14d38648a 100644 --- a/rpcs3/Emu/Cell/Modules/cellAudio.cpp +++ b/rpcs3/Emu/Cell/Modules/cellAudio.cpp @@ -953,8 +953,15 @@ void cell_audio_thread::operator()() { m_last_buffer_report = timestamp; + // Underruns SINCE THE LAST LINE, not since boot: what matters is whether the + // output is breaking up now, and a running total from a rough patch minutes ago + // hides that. Non-zero here is crackling, measured rather than inferred. + const u64 underruns_total = cfg.backend ? cfg.backend->get_underruns() : 0; + const u64 underruns = underruns_total - m_last_underruns; + m_last_underruns = underruns_total; + cellAudio.notice("Audio buffer: queued=%.1fms target=%.1fms (%.0f%%) period=%.0f%% " - "blocks=%u ports=%u untouched=%u avg=%.2f ratio=%.2f", + "blocks=%u ports=%u untouched=%u avg=%.2f ratio=%.2f underruns=%u", enqueued_playtime / 1000.0, desired_duration_adjusted / 1000.0, desired_duration_rate * 100.0f, @@ -963,7 +970,8 @@ void cell_audio_thread::operator()() active_ports, untouched, average_playtime_ratio, - frequency_ratio); + frequency_ratio, + underruns); } const s64 time_left = m_dynamic_period - time_since_last_period; diff --git a/rpcs3/Emu/Cell/Modules/cellAudio.h b/rpcs3/Emu/Cell/Modules/cellAudio.h index 1e2e00d98..aeb3e50a2 100644 --- a/rpcs3/Emu/Cell/Modules/cellAudio.h +++ b/rpcs3/Emu/Cell/Modules/cellAudio.h @@ -412,6 +412,9 @@ public: // Timestamp of the last "Audio buffer:" line, so it stays at one per 10s. u64 m_last_buffer_report = 0; + + // Backend underrun total at that line, so the next one can report the delta. + u64 m_last_underruns = 0; f32 m_average_playtime = 0.0f; bool m_backend_failed = false; bool m_audio_should_restart = false;