Modifying sound while it plays

Hello everyone !

newbie question here, I'm more used to developing in c++ in comfortable framework (JUCE).

I'm trying to modify the synth-tone exemple from cpal to make it change frequency when I press a key.

For what I understand I need a way to asynchronously get the input from the keyboard, and a way to lock the oscillator object to change some of its member value (namely : frequency) without disturbing the audio stream which uses a tight clock.

For the async input I found termion::async_stdin which seems to be a nice candidate, but for the second problem I don't even really know what to google for.

Am I even thinking in the right direction ?

Have a nice day !

My approach to this would be using a sync_channel to send the event to the audio thread.

I'm on Windows at the moment, so I'll use crossterm, but you can use termion for input handling instead.

fn main() -> anyhow::Result<()> {
    let (tx, rx) = sync_channel(1);
    let stream = stream_setup_for(rx)?;;

    loop {
        match event::read() {
            Ok(Event::Key(k)) if k.code == KeyCode::Char('s') => tx.send(Waveform::Sine)?,
            Ok(Event::Key(k)) if k.code == KeyCode::Char('r') => tx.send(Waveform::Square)?,
            Ok(Event::Key(k)) if k.code == KeyCode::Char('w') => tx.send(Waveform::Saw)?,
            Ok(Event::Key(k)) if k.code == KeyCode::Char('t') => tx.send(Waveform::Triangle)?,
            Ok(Event::Key(k)) if k.code == KeyCode::Char('q') => break,

            Ok(_) => (),
            Err(err) => panic!("{err:?}"),


This creates a bounded channel with room to store one message, and moves the Receiver to the audio thread. stream_setup_for just passes the receiver straight to make_stream:

pub fn stream_setup_for(rx: Receiver<Waveform>) -> Result<cpal::Stream, anyhow::Error> {
    let (_host, device, config) = host_device_setup()?;

    match config.sample_format() {
        cpal::SampleFormat::I8 => make_stream::<i8>(&device, &config.into(), rx),
        cpal::SampleFormat::I16 => make_stream::<i16>(&device, &config.into(), rx),
        // ... etc, Same for all other sample formats

pub fn make_stream<T>(
    device: &cpal::Device,
    config: &cpal::StreamConfig,
    rx: Receiver<Waveform>, // Added the channel receiver here
) -> Result<cpal::Stream, anyhow::Error>
    T: SizedSample + FromSample<f32>,
    // The function body is all the same.
    // But I replaced the `move` closure as shown below:

    let stream = device.build_output_stream(
        move |output: &mut [T], _: &cpal::OutputCallbackInfo| {
            // This if-let block is the only difference, here
            if let Ok(wf) = rx.try_recv() {

            process_frame(output, &mut oscillator, num_channels)


And that completes the whole thing. Keys s, r, w, and t change the waveform (which sounds pretty bad, FWIW) and q closes the annoying example.


Thanks !

I'll try this,

So here tx is owned by the main function thread, and rx ownership is move to concurrent thread inited by the stream, am I right ?

I was wondering though, If I start to build more complex stuff, say a synth with 20 parameters (which is not that much as far as synth goes). Would you recommend building a more complex channel ? Or would I be better using some kind of parameter struct shared between an UI that writes and a dsp that read from it, I've seen Arc<Mutex<_>> being mentioned in this thread

(I slowly start to get a grasp on all of this)

You would just extend your message type to handle the complexity. A new enum where one variant changes the waveform, another might change the frequency, a third changes the envelope, and so on.

I actually would design the messages to be higher level, IMHO. Like MIDI operations, for example. Send notes as your event message.

You don’t need a mutex if you are just passing messages.

1 Like

Thanks a lot !