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 CPAL 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"); }