use hexodsp::*; use std::sync::Arc; use std::sync::Mutex; 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 (2021-05-18) and is not implemented // in this jack interface). 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)); } }); } struct Notifications { node_exec: Arc>, } impl jack::NotificationHandler for Notifications { fn thread_init(&self, _: &jack::Client) { println!("JACK: thread init"); } fn shutdown(&mut self, status: jack::ClientStatus, reason: &str) { println!("JACK: shutdown with status {:?} because \"{}\"", status, reason); } fn freewheel(&mut self, _: &jack::Client, is_enabled: bool) { println!("JACK: freewheel mode is {}", if is_enabled { "on" } else { "off" }); } fn sample_rate(&mut self, _: &jack::Client, srate: jack::Frames) -> jack::Control { println!("JACK: sample rate changed to {}", srate); let mut ne = self.node_exec.lock().unwrap(); ne.set_sample_rate(srate as f32); jack::Control::Continue } fn client_registration(&mut self, _: &jack::Client, name: &str, is_reg: bool) { println!( "JACK: {} client with name \"{}\"", if is_reg { "registered" } else { "unregistered" }, name ); } fn port_registration(&mut self, client: &jack::Client, port_id: jack::PortId, is_reg: bool) { if let Some(p) = client.port_by_id(port_id) { if let Ok(name) = p.name() { println!("JACK: port registered: {}", name); } } println!( "JACK: {} port with id {}", if is_reg { "registered" } else { "unregistered" }, port_id ); } fn port_rename( &mut self, _: &jack::Client, port_id: jack::PortId, old_name: &str, new_name: &str, ) -> jack::Control { println!("JACK: port with id {} renamed from {} to {}", port_id, old_name, new_name); jack::Control::Continue } fn ports_connected( &mut self, _: &jack::Client, port_id_a: jack::PortId, port_id_b: jack::PortId, are_connected: bool, ) { println!( "JACK: ports with id {} and {} are {}", port_id_a, port_id_b, if are_connected { "connected" } else { "disconnected" } ); } fn graph_reorder(&mut self, _: &jack::Client) -> jack::Control { println!("JACK: graph reordered"); jack::Control::Continue } fn xrun(&mut self, _: &jack::Client) -> jack::Control { println!("JACK: xrun occurred"); jack::Control::Continue } } // This function starts the Jack backend and // runs the audio loop with the NodeExecutor. fn start_backend(node_exec: NodeExecutor, mut frontend_loop: F) { let (client, _status) = jack::Client::new("HexoDSPJackDemo", jack::ClientOptions::NO_START_SERVER).unwrap(); let in_a = client.register_port("hexodsp_in1", jack::AudioIn::default()).unwrap(); let in_b = client.register_port("hexodsp_in2", jack::AudioIn::default()).unwrap(); let mut out_a = client.register_port("hexodsp_out1", jack::AudioOut::default()).unwrap(); let mut out_b = client.register_port("hexodsp_out2", jack::AudioOut::default()).unwrap(); let ne = Arc::new(Mutex::new(node_exec)); let ne2 = ne.clone(); let mut first = true; let process_callback = move |client: &jack::Client, ps: &jack::ProcessScope| -> jack::Control { let out_a_p = out_a.as_mut_slice(ps); let out_b_p = out_b.as_mut_slice(ps); let in_a_p = in_a.as_slice(ps); let in_b_p = in_b.as_slice(ps); if first { client .connect_ports_by_name("HexoDSPJackDemo:hexodsp_out1", "system:playback_1") .expect("jack connect ports works"); client .connect_ports_by_name("HexoDSPJackDemo:hexodsp_out2", "system:playback_2") .expect("jack connect ports works"); first = false; } let nframes = out_a_p.len(); // Please note, locking the NodeExecutor is wrong and broken // and should not be done on a real time thread. // // We do it for educational purposes here. // In reality the jack::NotificationHandler should send a message through // a ring buffer to the audio thread and set the new sample rate or // other parameters there! let mut node_exec = ne.lock().unwrap(); // First task in the audio callback is processing any graph or parameter // updates that were sent by the frontend thread: node_exec.process_graph_updates(); let mut frames_left = nframes; let mut offs = 0; while frames_left > 0 { let cur_nframes = if frames_left >= hexodsp::dsp::MAX_BLOCK_SIZE { hexodsp::dsp::MAX_BLOCK_SIZE } else { frames_left }; frames_left -= cur_nframes; let output = &mut [ &mut out_a_p[offs..(offs + cur_nframes)], &mut out_b_p[offs..(offs + cur_nframes)], ]; let input = &[&in_a_p[offs..(offs + cur_nframes)], &in_b_p[offs..(offs + cur_nframes)]]; let mut context = Context { nframes: cur_nframes, output, input }; for i in 0..context.nframes { context.output[0][i] = 0.0; context.output[1][i] = 0.0; } node_exec.process(&mut context); offs += cur_nframes; } jack::Control::Continue }; let process = jack::ClosureProcessHandler::new(process_callback); // Activate the client, which starts the processing. let active_client = client.activate_async(Notifications { node_exec: ne2 }, process).unwrap(); frontend_loop(); active_client.deactivate().unwrap(); }