cpal icon indicating copy to clipboard operation
cpal copied to clipboard

Pausing a stream doesn't clear out sample buffers

Open zkldi opened this issue 2 years ago • 0 comments

I'm not sure whether this is intentional behaviour or not, but it's extremely undesirable and hard to work around.

I'm on ALSA, Linux.

Rough outline

  • We play some audio
  • Pause device
  • change what the device is playing somehow (i.e. change audio, tell the device to emit silence instead, etc.)
  • unpause device
  • a bit of the music (whatever was leftover in the previous bit of samples you returned) will be played, before more samples are requested and the new requested audio is emitted.

Why does this suck?

I can't use stream.pause() to save battery life in my applications, as it results in "lingering" audio when seeking around music. This means my device always has to be on, requesting samples, doing nothing, burning cpu.

Example

Attached is a video of this behaviour: https://github.com/RustAudio/cpal/assets/20380519/f3aaa9e7-2063-43c1-9c79-6eb5bc774393

Code example

Place this in examples/beep_pause.rs and run it with cargo run --example beep_pause

use std::sync::{atomic::AtomicBool, Arc};

use anyhow;
use clap::Parser;
use cpal::{
    traits::{DeviceTrait, HostTrait, StreamTrait},
    FromSample, Sample, SizedSample,
};

#[derive(Parser, Debug)]
#[command(version, about = "CPAL beep_pause bug example", long_about = None)]
struct Opt {
    /// The audio device to use
    #[arg(short, long, default_value_t = String::from("default"))]
    device: String,

    /// Use the JACK host
    #[cfg(all(
        any(
            target_os = "linux",
            target_os = "dragonfly",
            target_os = "freebsd",
            target_os = "netbsd"
        ),
        feature = "jack"
    ))]
    #[arg(short, long)]
    #[allow(dead_code)]
    jack: bool,
}

fn main() -> anyhow::Result<()> {
    let opt = Opt::parse();

    // Conditionally compile with jack if the feature is specified.
    #[cfg(all(
        any(
            target_os = "linux",
            target_os = "dragonfly",
            target_os = "freebsd",
            target_os = "netbsd"
        ),
        feature = "jack"
    ))]
    // Manually check for flags. Can be passed through cargo with -- e.g.
    // cargo run --release --example beep --features jack -- --jack
    let host = if opt.jack {
        cpal::host_from_id(cpal::available_hosts()
            .into_iter()
            .find(|id| *id == cpal::HostId::Jack)
            .expect(
                "make sure --features jack is specified. only works on OSes where jack is available",
            )).expect("jack host unavailable")
    } else {
        cpal::default_host()
    };

    #[cfg(any(
        not(any(
            target_os = "linux",
            target_os = "dragonfly",
            target_os = "freebsd",
            target_os = "netbsd"
        )),
        not(feature = "jack")
    ))]
    let host = cpal::default_host();

    let device = if opt.device == "default" {
        host.default_output_device()
    } else {
        host.output_devices()?
            .find(|x| x.name().map(|y| y == opt.device).unwrap_or(false))
    }
    .expect("failed to find output device");
    println!("Output device: {}", device.name()?);

    let config = device.default_output_config().unwrap();

    println!("Default output config: {:?}", config);

    let emit_silence = AtomicBool::new(false);

    match config.sample_format() {
        cpal::SampleFormat::I8 => run::<i8>(&device, &config.into(), emit_silence),
        cpal::SampleFormat::I16 => run::<i16>(&device, &config.into(), emit_silence),
        // cpal::SampleFormat::I24 => run::<I24>(&device, &config.into(), emit_silence),
        cpal::SampleFormat::I32 => run::<i32>(&device, &config.into(), emit_silence),
        // cpal::SampleFormat::I48 => run::<I48>(&device, &config.into(), emit_silence),
        cpal::SampleFormat::I64 => run::<i64>(&device, &config.into(), emit_silence),
        cpal::SampleFormat::U8 => run::<u8>(&device, &config.into(), emit_silence),
        cpal::SampleFormat::U16 => run::<u16>(&device, &config.into(), emit_silence),
        // cpal::SampleFormat::U24 => run::<U24>(&device, &config.into(), emit_silence),
        cpal::SampleFormat::U32 => run::<u32>(&device, &config.into(), emit_silence),
        // cpal::SampleFormat::U48 => run::<U48>(&device, &config.into(), emit_silence),
        cpal::SampleFormat::U64 => run::<u64>(&device, &config.into(), emit_silence),
        cpal::SampleFormat::F32 => run::<f32>(&device, &config.into(), emit_silence),
        cpal::SampleFormat::F64 => run::<f64>(&device, &config.into(), emit_silence),
        sample_format => panic!("Unsupported sample format '{sample_format}'"),
    }
}

pub fn run<T>(
    device: &cpal::Device,
    config: &cpal::StreamConfig,
    emit_silence: AtomicBool,
) -> Result<(), anyhow::Error>
where
    T: SizedSample + FromSample<f32>,
{
    let sample_rate = config.sample_rate.0 as f32;
    let channels = config.channels as usize;

    // Produce a sinusoid of maximum amplitude.
    let mut sample_clock = 0f32;
    let mut next_value = move || {
        sample_clock = (sample_clock + 1.0) % sample_rate;
        (sample_clock * 440.0 * 2.0 * std::f32::consts::PI / sample_rate).sin()
    };

    let err_fn = |err| eprintln!("an error occurred on stream: {}", err);

    let emit_silence = Arc::new(emit_silence);
    let st_em_silence = emit_silence.clone();

    let stream = device.build_output_stream(
        config,
        move |data: &mut [T], _: &cpal::OutputCallbackInfo| {
            // if we're told to emit silence, emit silence.
            if st_em_silence.load(std::sync::atomic::Ordering::SeqCst) {
                for d in data {
                    *d = Sample::EQUILIBRIUM;
                }
            } else {
                write_data(data, channels, &mut next_value)
            }
        },
        err_fn,
        None,
    )?;
    stream.play()?;

    println!("playing sound");

    // play for 1 second.
    std::thread::sleep(std::time::Duration::from_millis(1000));

    println!("paused device");

    // pause stream
    stream.pause()?;
    // tell the sound card to emit silence
    emit_silence.store(true, std::sync::atomic::Ordering::SeqCst);

    println!("told cpal to no longer emit audio");

    // wait a second just so the end user can read what's going on
    std::thread::sleep(std::time::Duration::from_millis(1000));

    println!("un-paused device.");

    // play sound. you should hear something?
    stream.play()?;

    // wait 2 seconds
    std::thread::sleep(std::time::Duration::from_millis(2000));

    Ok(())
}

fn write_data<T>(output: &mut [T], channels: usize, next_sample: &mut dyn FnMut() -> f32)
where
    T: Sample + FromSample<f32>,
{
    for frame in output.chunks_mut(channels) {
        let value: T = T::from_sample(next_sample());
        for sample in frame.iter_mut() {
            *sample = value;
        }
    }
}

What should happen instead?

Pausing the device should ideally clear anything we've previously sent to the device, and unpausing should (therefore) result in an immediate request-for-new-samples.

zkldi avatar Oct 21 '23 13:10 zkldi