HexoDSP/examples/jack_demo_node_api.rs
2022-08-05 19:44:40 +02:00

303 lines
10 KiB
Rust

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<Mutex<NodeExecutor>>,
}
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<F: FnMut()>(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();
}