SamiPerttu / fundsp

Library for audio processing and synthesis
Apache License 2.0
798 stars 43 forks source link

Replacing a node in a network resets its sample rate #40

Closed ajoklar closed 7 months ago

ajoklar commented 7 months ago

I appended a full example. The network plays a sine wave at 440 Hz. After replacing signal with the same sine wave, the pitch is higher. I don't think the need to set the sample rate after a replace is intended behavior.

On a side note, I understand that declick fades in the signal. When I replace the signal node, a click happens even when the pitch is the same. Is it possible to do a fade in on every replace? Thanks a lot in advance.

//! Make real-time changes to a network while it is playing.
#![allow(clippy::precedence)]

use assert_no_alloc::*;
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use cpal::{FromSample, SizedSample};
use fundsp::hacker::*;
use std::time::Duration;

#[cfg(debug_assertions)] // required when disable_release is set (default)
#[global_allocator]
static A: AllocDisabler = AllocDisabler;

fn main() {
    let host = cpal::default_host();

    let device = host
        .default_output_device()
        .expect("Failed to find a default output device");
    let config = device.default_output_config().unwrap();

    match config.sample_format() {
        cpal::SampleFormat::F32 => run::<f32>(&device, &config.into()).unwrap(),
        cpal::SampleFormat::I16 => run::<i16>(&device, &config.into()).unwrap(),
        cpal::SampleFormat::U16 => run::<u16>(&device, &config.into()).unwrap(),
        _ => panic!("Unsupported format"),
    }
}

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

    // prints 48000
    println!("{sample_rate}");

    let a = Box::new(sine_hz(440.0));
    let mut net = Net64::new(0, 2);
    let signal = net.chain(a.clone());
    net.chain(Box::new(declick() >> pan(0.0)));
    net.set_sample_rate(sample_rate);

    let mut backend = net.backend();

    let mut next_value = move || assert_no_alloc(|| backend.get_stereo());

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

    let stream = device.build_output_stream(
        config,
        move |data: &mut [T], _: &cpal::OutputCallbackInfo| {
            write_data(data, channels, &mut next_value)
        },
        err_fn,
        None,
    )?;
    stream.play()?;

    std::thread::sleep(Duration::from_secs(3));
    println!("replace signal with the same function, but now it outputs a higher pitch");
    net.replace(signal, a.clone());
    // uncomment to set sample rate again and the pitch stays at 440 Hz
    // net.set_sample_rate(sample_rate);
    net.commit();
    std::thread::sleep(Duration::from_secs(3));

    Ok(())
}

fn write_data<T>(output: &mut [T], channels: usize, next_sample: &mut dyn FnMut() -> (f64, f64))
where
    T: SizedSample + FromSample<f64>,
{
    for frame in output.chunks_mut(channels) {
        let sample = next_sample();
        let left = T::from_sample(sample.0);
        let right: T = T::from_sample(sample.1);

        for (channel, sample) in frame.iter_mut().enumerate() {
            if channel & 1 == 0 {
                *sample = left;
            } else {
                *sample = right;
            }
        }
    }
}
SamiPerttu commented 7 months ago

Thanks for the report. I committed a fix.

I agree that some kind of fading would be a good idea! I'll bump it up in the TODO order.