Start Linux audio preview after the first mixed samples, on a short device buffer.
Pulse and PipeWire were opening a large default period and playing silence until the feeder caught up, so the picture ran while the sound started late. The stream now waits for queued samples and requests about 10 ms of buffering.
This commit is contained in:
parent
813c7c4650
commit
139ff9a4e3
|
|
@ -10,11 +10,32 @@ use std::sync::Arc;
|
|||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::mpsc;
|
||||
use std::thread::JoinHandle;
|
||||
use std::time::Duration;
|
||||
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use cpal::{FromSample, SizedSample};
|
||||
use effectcraft_ui_egui::audio::{AudioDevice, AudioFeed, AudioOutput};
|
||||
|
||||
/// Device callback period: about 10 ms, clamped so Pulse/PipeWire/ALSA does not open a
|
||||
/// multi-hundred-millisecond default buffer that plays as silence at the start of preview (#554).
|
||||
fn preferred_buffer_frames(rate: u32) -> u32 {
|
||||
(rate / 100).clamp(256, 2048)
|
||||
}
|
||||
|
||||
/// Wait until the feeder has mixed a block, or until preview is stopped. Starting the stream
|
||||
/// before that fills the device with silence, so the first real samples are heard late (#554).
|
||||
fn wait_to_play(feed: &AudioFeed, stop: &mpsc::Receiver<()>) -> bool {
|
||||
loop {
|
||||
if feed.queued_frames() > 0 {
|
||||
return true;
|
||||
}
|
||||
match stop.recv_timeout(Duration::from_millis(2)) {
|
||||
Ok(()) | Err(mpsc::RecvTimeoutError::Disconnected) => return false,
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CpalOut {
|
||||
out: AudioOutput,
|
||||
rate: u32,
|
||||
|
|
@ -131,21 +152,36 @@ impl AudioDevice for CpalOut {
|
|||
};
|
||||
let mut cfg: cpal::StreamConfig = sup.config();
|
||||
cfg.sample_rate = cpal::SampleRate(rate);
|
||||
let stream = match sup.sample_format() {
|
||||
cpal::SampleFormat::F32 => run::<f32>(&dev, &cfg, feed, latency, map),
|
||||
cpal::SampleFormat::I16 => run::<i16>(&dev, &cfg, feed, latency, map),
|
||||
cpal::SampleFormat::U16 => run::<u16>(&dev, &cfg, feed, latency, map),
|
||||
cpal::SampleFormat::I32 => run::<i32>(&dev, &cfg, feed, latency, map),
|
||||
cfg.buffer_size = cpal::BufferSize::Fixed(preferred_buffer_frames(rate));
|
||||
let build = |cfg: &cpal::StreamConfig| match sup.sample_format() {
|
||||
cpal::SampleFormat::F32 => run::<f32>(&dev, cfg, feed.clone(), latency.clone(), map),
|
||||
cpal::SampleFormat::I16 => run::<i16>(&dev, cfg, feed.clone(), latency.clone(), map),
|
||||
cpal::SampleFormat::U16 => run::<u16>(&dev, cfg, feed.clone(), latency.clone(), map),
|
||||
cpal::SampleFormat::I32 => run::<i32>(&dev, cfg, feed.clone(), latency.clone(), map),
|
||||
f => Err(format!("unsupported sample format {f:?}")),
|
||||
};
|
||||
let stream = match stream.and_then(|s| s.play().map(|_| s).map_err(|e| e.to_string())) {
|
||||
let stream = match build(&cfg) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
let _ = ready_tx.send(Err(e));
|
||||
return;
|
||||
Err(_) => {
|
||||
// Some hosts reject a fixed period; a large default still plays, just later.
|
||||
cfg.buffer_size = cpal::BufferSize::Default;
|
||||
match build(&cfg) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
let _ = ready_tx.send(Err(e));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let _ = ready_tx.send(Ok(()));
|
||||
if !wait_to_play(&feed, &stop_rx) {
|
||||
return;
|
||||
}
|
||||
if let Err(e) = stream.play() {
|
||||
eprintln!("effectcraft: audio output: {e}");
|
||||
return;
|
||||
}
|
||||
let _ = stop_rx.recv();
|
||||
drop(stream);
|
||||
})
|
||||
|
|
@ -174,3 +210,37 @@ impl Drop for CpalOut {
|
|||
self.stop();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn preferred_buffer_is_about_ten_milliseconds() {
|
||||
assert_eq!(preferred_buffer_frames(48_000), 480);
|
||||
assert_eq!(preferred_buffer_frames(8_000), 256);
|
||||
assert_eq!(preferred_buffer_frames(192_000), 1920);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wait_to_play_returns_once_the_feeder_has_samples() {
|
||||
let feed = Arc::new(AudioFeed::default());
|
||||
let (_tx, rx) = mpsc::channel();
|
||||
let f = feed.clone();
|
||||
let h = std::thread::spawn(move || wait_to_play(&f, &rx));
|
||||
assert_eq!(feed.queued_frames(), 0);
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
feed.push(&[0.1, 0.2]);
|
||||
assert!(h.join().unwrap());
|
||||
assert_eq!(feed.queued_frames(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wait_to_play_stops_without_samples() {
|
||||
let feed = AudioFeed::default();
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let h = std::thread::spawn(move || wait_to_play(&feed, &rx));
|
||||
tx.send(()).unwrap();
|
||||
assert!(!h.join().unwrap());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue