fix(audio): fix DMC loop byte skip, add DC blocker, lazy cpal stream
Some checks failed
CI / rust (push) Has been cancelled
Some checks failed
CI / rust (push) Has been cancelled
Three audio bugs fixed: 1. DMC loop mode skipped the last byte of each sample iteration. provide_dmc_dma_byte() was immediately setting dmc_dma_request on loop restart while the sample buffer was still full, causing the while-loop in clock_cpu_cycles to service a second DMA immediately and overwrite the valid buffer. Per NES hardware spec, the reader only fills an empty buffer — the request is now left to clock_dmc when the output unit actually empties the buffer into the shift register. Fixes intermittent clicking/crackling in games that use looped DMC samples (BGM, SFX). 2. Missing DC blocker (high-pass filter) in AudioMixer. The NES APU has a capacitor-coupled output stage that blocks DC bias. Without it, abrupt channel state changes (length counter expiry, sweep mute, triangle period < 2) produce DC steps that manifest as audible clicks. Added a one-pole IIR high-pass filter at ~5 Hz applied after the existing low-pass filter. 3. cpal stream was opened at application startup with BufferSize::Fixed(256), forcing PipeWire/PulseAudio to run the entire audio graph at a 5.3 ms quantum. This disrupted other audio applications (browsers, media players) even when no ROM was loaded. Fixed by: (a) creating the stream lazily on the first push_samples call so no device is touched until a ROM is running, and (b) switching to BufferSize::Default so the audio server chooses the quantum instead of the emulator imposing one. Ring buffer capacity increased from 1536 to 4096 samples to absorb larger server quanta.
This commit is contained in:
committed by
Se.Cherkasov
parent
82ac084b53
commit
1b4db3a506
@@ -20,7 +20,7 @@ const APP_ID: &str = "org.nesemu.desktop";
|
||||
const TITLE: &str = "NES Emulator";
|
||||
const SCALE: i32 = 3;
|
||||
const SAMPLE_RATE: u32 = 48_000;
|
||||
const AUDIO_RING_CAPACITY: usize = 1536;
|
||||
const AUDIO_RING_CAPACITY: usize = 4096;
|
||||
const AUDIO_CALLBACK_FRAMES: u32 = 256;
|
||||
|
||||
fn main() {
|
||||
@@ -482,16 +482,26 @@ struct CpalAudioSink {
|
||||
impl CpalAudioSink {
|
||||
fn new(volume: Arc<AtomicU32>) -> Self {
|
||||
let ring = Arc::new(RingBuffer::new(AUDIO_RING_CAPACITY));
|
||||
let ring_for_cb = Arc::clone(&ring);
|
||||
let vol_for_cb = Arc::clone(&volume);
|
||||
let stream = Self::try_build_stream(ring_for_cb, vol_for_cb);
|
||||
// Do NOT open the audio device here. Creating a cpal stream at startup
|
||||
// forces the system audio server (PipeWire/PulseAudio) to allocate
|
||||
// resources and may disrupt other running audio applications even when
|
||||
// the emulator is idle. The stream is opened lazily on the first
|
||||
// push_samples call, i.e. only when a ROM is actually playing.
|
||||
Self {
|
||||
_stream: stream,
|
||||
_stream: None,
|
||||
ring,
|
||||
_volume: volume,
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_stream(&mut self) {
|
||||
if self._stream.is_none() {
|
||||
let ring_for_cb = Arc::clone(&self.ring);
|
||||
let vol_for_cb = Arc::clone(&self._volume);
|
||||
self._stream = Self::try_build_stream(ring_for_cb, vol_for_cb);
|
||||
}
|
||||
}
|
||||
|
||||
fn try_build_stream(ring: Arc<RingBuffer>, volume: Arc<AtomicU32>) -> Option<cpal::Stream> {
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
|
||||
@@ -548,6 +558,7 @@ impl CpalAudioSink {
|
||||
|
||||
impl nesemu::AudioOutput for CpalAudioSink {
|
||||
fn push_samples(&mut self, samples: &[f32]) {
|
||||
self.ensure_stream();
|
||||
self.ring.push(samples);
|
||||
}
|
||||
}
|
||||
@@ -567,7 +578,10 @@ fn cpal_stream_config() -> cpal::StreamConfig {
|
||||
cpal::StreamConfig {
|
||||
channels: 1,
|
||||
sample_rate: cpal::SampleRate(SAMPLE_RATE),
|
||||
buffer_size: cpal::BufferSize::Fixed(AUDIO_CALLBACK_FRAMES),
|
||||
// Use the audio server's default buffer size to avoid forcing the entire
|
||||
// PipeWire/PulseAudio graph into low-latency mode, which would disturb
|
||||
// other audio applications (browsers, media players, etc.).
|
||||
buffer_size: cpal::BufferSize::Default,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -803,9 +817,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn desktop_audio_ring_budget_stays_below_25ms() {
|
||||
fn desktop_audio_ring_budget_stays_below_100ms() {
|
||||
let latency_ms = audio_ring_latency_ms(AUDIO_RING_CAPACITY, SAMPLE_RATE);
|
||||
let max_budget_ms = 40.0;
|
||||
let max_budget_ms = 100.0;
|
||||
assert!(
|
||||
latency_ms <= max_budget_ms,
|
||||
"desktop audio ring latency budget too high: {latency_ms:.2}ms"
|
||||
@@ -813,12 +827,11 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn desktop_audio_uses_fixed_low_latency_callback_size() {
|
||||
fn desktop_audio_uses_default_buffer_size() {
|
||||
let config = cpal_stream_config();
|
||||
assert_eq!(
|
||||
config.buffer_size,
|
||||
cpal::BufferSize::Fixed(AUDIO_CALLBACK_FRAMES)
|
||||
);
|
||||
// Default lets the audio server (PipeWire/PulseAudio) choose the
|
||||
// buffer size, preventing interference with other audio applications.
|
||||
assert_eq!(config.buffer_size, cpal::BufferSize::Default);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user