diff --git a/Cargo.toml b/Cargo.toml index 4a3fe30..9746475 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,8 @@ num-traits = "0.2.14" num-complex = "0.2" jack = "0.6.6" rustfft = "6.0.0" +cpal = "0.13.5" +anyhow = "1.0.58" [lib] path = "src/lib.rs" diff --git a/examples/cpal_demo_node_api.rs b/examples/cpal_demo_node_api.rs new file mode 100644 index 0000000..fb5155f --- /dev/null +++ b/examples/cpal_demo_node_api.rs @@ -0,0 +1,236 @@ +use hexodsp::*; + +use anyhow; +use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; + +fn main() { + let (mut node_conf, node_exec) = new_node_engine(); + + start_backend(node_exec, move || { + // To get an overview of the existing nodes you can + // take a look in the file src/dsp/mod.rs + // where the `macro_rules! node_list` definition + // is. + // + // This defines all supported nodes and their + // parameters/inputs ports and their outputs. + let sin = NodeId::Sin(0); + let amp = NodeId::Amp(0); + let out = NodeId::Out(0); + + let amp_gain_param = amp.inp_param("gain").unwrap(); + let sin_freq_param = sin.inp_param("freq").unwrap(); + + // Create the nodes in the frontend and in the + // audio backend. You only have to do this once + // and it's up to you to track which nodes you + // already created. + // + // Keep in mind, that the only way to deallocate + // notes is to call `node_conf.delete_nodes()`, + // which deletes all nodes. + // + // You can't delete only one specific node. + node_conf.create_node(sin); + node_conf.create_node(amp); + node_conf.create_node(out); + + // Silence the Amp for the start + node_conf.set_param(amp_gain_param, (0.0).into()); + + // Create a NodeProg from the currently created nodes: + let mut prog = node_conf.rebuild_node_ports(); + + // The order you add the nodes to the NodeProg determines + // the order they will be executed by the audio thread. + // You will have to take care that all nodes get their + // data in the right order here. + node_conf.add_prog_node(&mut prog, &NodeId::Sin(0)); + node_conf.add_prog_node(&mut prog, &NodeId::Amp(0)); + node_conf.add_prog_node(&mut prog, &NodeId::Out(0)); + + // Define the connections between the nodes in the NodeProg: + node_conf.set_prog_node_exec_connection( + &mut prog, + // first the input: + (amp, amp.inp("inp").unwrap()), + // then the output that is assigned to it: + (sin, sin.out("sig").unwrap())); + + node_conf.set_prog_node_exec_connection( + &mut prog, + (out, out.inp("ch1").unwrap()), + (amp, amp.out("sig").unwrap())); + + node_conf.set_prog_node_exec_connection( + &mut prog, + (out, out.inp("ch2").unwrap()), + (amp, amp.out("sig").unwrap())); + + // Finally upload the NodeProg to the audio thread. + node_conf.upload_prog(prog, true); + + // You can repeatedly create new NodeProgs with `rebuild_node_ports` + // and change the graph all the way you like at runtime. + + let mut amp_counter = 0; + let mut pitch_counter = 0; + loop { + // In this loop we simulate someone adjusting the paramter + // knobs of the amplifier gain and sine oscillator pitch. + // + // Please note, that for sample accurate modulation you should + // use the built in tracker or receive MIDI data from + // different application (MIDI processing has not been + // implemented yet though). + + let new_gain = + match amp_counter { + 0 => 0.2, + 1 => 0.3, + 2 => 0.35, + 3 => 0.3, + 4 => 0.1, + _ => { + amp_counter = 0; + // Pitch is defined in 0.1 per octave. + // 0.0 is A4, + // 0.1 is A5 + // -0.1 is A3 + let new_pitch = + match pitch_counter { + 0 => 0.0, // A4 + 1 => -0.1, // A3 + 2 => 0.1, // A5 + 3 => -0.1, // A3 + 4 => -0.2, // A2 + _ => { + pitch_counter = 0; + // -0.15 is 6 semitones above A3 => D#3 + -0.15 + }, + }; + pitch_counter += 1; + + println!("set pitch={:4.2}", new_pitch); + node_conf.set_param(sin_freq_param, new_pitch.into()); + + 0.1 + }, + }; + amp_counter += 1; + + println!("set gain={:4.2}", new_gain); + node_conf.set_param(amp_gain_param, new_gain.into()); + + std::thread::sleep(std::time::Duration::from_millis(300)); + } + }); +} + +pub fn run( + device: &cpal::Device, + config: &cpal::StreamConfig, + mut node_exec: NodeExecutor, + mut frontend_loop: F, +) -> Result<(), anyhow::Error> +where + T: cpal::Sample, +{ + let sample_rate = config.sample_rate.0 as f32; + let channels = config.channels as usize; + + node_exec.set_sample_rate(sample_rate); + + let input_bufs = [[0.0; hexodsp::dsp::MAX_BLOCK_SIZE]; 2]; + let mut outputbufs = [[0.0; hexodsp::dsp::MAX_BLOCK_SIZE]; 2]; + + let err_fn = |err| eprintln!("an error occurred on stream: {}", err); + let stream = device.build_output_stream( + config, + move |data: &mut [T], _: &cpal::OutputCallbackInfo| { + let mut frames_left = data.len() / channels; + + let mut out_iter = data.chunks_mut(channels); + + node_exec.process_graph_updates(); + + while frames_left > 0 { + let cur_nframes = + if frames_left >= hexodsp::dsp::MAX_BLOCK_SIZE { + hexodsp::dsp::MAX_BLOCK_SIZE + } else { + frames_left + }; + + let input = &[ + &input_bufs[0][0..cur_nframes], + &input_bufs[1][0..cur_nframes], + ]; + + let split = outputbufs.split_at_mut(1); + + let mut output = [ + &mut ((split.0[0])[0..cur_nframes]), + &mut ((split.1[0])[0..cur_nframes]), + ]; + + let mut context = + Context { + nframes: cur_nframes, + output: &mut output[..], + input, + }; + + context.output[0].fill(0.0); + context.output[1].fill(0.0); + + node_exec.process(&mut context); + + // This copy loop is a bit inefficient, it's likely you can + // pass the right array slices directly into node_exec.process() + // via the Context structure. But I was too lazy at this point + // to figure this out. Check also the Jack example for a more + // efficient solution. + for i in 0..cur_nframes { + if let Some(frame) = out_iter.next() { + let mut ctx_chan = 0; + for sample in frame.iter_mut() { + let value: T = + cpal::Sample::from::(&context.output[ctx_chan][i]); + *sample = value; + + ctx_chan += 1; + if ctx_chan > context.output.len() { + ctx_chan = context.output.len() - 1; + } + } + } + } + + frames_left -= cur_nframes; + } + }, + err_fn, + )?; + stream.play()?; + + frontend_loop(); + + Ok(()) +} + +// This function starts the Jack backend and +// runs the audio loop with the NodeExecutor. +fn start_backend(node_exec: NodeExecutor, frontend_loop: F) { + let host = cpal::default_host(); + let device = host.default_output_device().expect("Finding useable audio device"); + let config = device.default_output_config().expect("A workable output config"); + + match config.sample_format() { + cpal::SampleFormat::F32 => run::(&device, &config.into(), node_exec, frontend_loop), + cpal::SampleFormat::I16 => run::(&device, &config.into(), node_exec, frontend_loop), + cpal::SampleFormat::U16 => run::(&device, &config.into(), node_exec, frontend_loop), + }.expect("cpal works fine"); +} +