Added CPAL example

This commit is contained in:
Weird Constructor 2022-07-09 20:36:56 +02:00
parent 3c515766fc
commit 79d6ac66d3
2 changed files with 238 additions and 0 deletions

View file

@ -24,6 +24,8 @@ num-traits = "0.2.14"
num-complex = "0.2" num-complex = "0.2"
jack = "0.6.6" jack = "0.6.6"
rustfft = "6.0.0" rustfft = "6.0.0"
cpal = "0.13.5"
anyhow = "1.0.58"
[lib] [lib]
path = "src/lib.rs" path = "src/lib.rs"

View file

@ -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<T, F: FnMut()>(
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::<f32>(&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<F: FnMut()>(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::<f32, F>(&device, &config.into(), node_exec, frontend_loop),
cpal::SampleFormat::I16 => run::<i16, F>(&device, &config.into(), node_exec, frontend_loop),
cpal::SampleFormat::U16 => run::<u16, F>(&device, &config.into(), node_exec, frontend_loop),
}.expect("cpal works fine");
}