Select Git revision
main.rs 16.98 KiB
//! # 16x16 Hexmatrix OSC-Jack-Mixer
use std::io;
use jack;
use jack::{Port, AudioIn, AudioOut};
use rosc;
use rosc::{OscPacket, OscMessage};
use std::net::{SocketAddrV4, UdpSocket};
use std::str::FromStr;
use std::sync::atomic::{AtomicUsize, Ordering};
use arr_macro::arr;
use clap::{Arg, App};
/*
TODO:
- [ ] Document dependecies
- [ ] Performance improvements:
- [ ] implement performance monitoring
- [ ] f32.mul_add() or f32.to_bits() / f32.from_bits()
- [ ] only calculate connected ports https://docs.rs/jack/0.7.1/jack/struct.Port.html#method.connected_count
- [ ] Flag volume modulations as dirty
- [x] Support other Buffers than 1024
- [ ] Features:
- [ ] Flags to control printing (--quiet etc)
- [ ] Option to set initial values
- [ ] Option to limit certain inputs/outputs to certain volumes
- [ ] Option to ignore certain modulations
- [ ] GUI/TLI to see matrix and current volumes
- [ ] OSC: Per Channel Solo/Mute/Invert
- [ ] OSC: Output Volumes
- [ ] OSC: Output Levels
*/
// Number of inputs and outputs.
// Careful: this needs to be changed in many places
const N_INPUTS: usize = 16;
const N_OUTPUTS: usize = 16;
// Atomic Variables for Shared Mutability of Modulation values
// One per Input, with n values where n is the number of outputs
// Incoming float values get scaled to a unsigned usize (e.g. 64 bit)
// Static arrays are used here for maximum performance
// (No: it is not possible to nest arr! macro calls. yet.)
static VOLS1: [AtomicUsize; N_INPUTS] = arr![AtomicUsize::new(0); 16];
static VOLS2: [AtomicUsize; N_INPUTS] = arr![AtomicUsize::new(0); 16];
static VOLS3: [AtomicUsize; N_INPUTS] = arr![AtomicUsize::new(0); 16];
static VOLS4: [AtomicUsize; N_INPUTS] = arr![AtomicUsize::new(0); 16];
static VOLS5: [AtomicUsize; N_INPUTS] = arr![AtomicUsize::new(0); 16];
static VOLS6: [AtomicUsize; N_INPUTS] = arr![AtomicUsize::new(0); 16];
static VOLS7: [AtomicUsize; N_INPUTS] = arr![AtomicUsize::new(0); 16];
static VOLS8: [AtomicUsize; N_INPUTS] = arr![AtomicUsize::new(0); 16];
static VOLS9: [AtomicUsize; N_INPUTS] = arr![AtomicUsize::new(0); 16];
static VOLS10: [AtomicUsize; N_INPUTS] = arr![AtomicUsize::new(0); 16];
static VOLS11: [AtomicUsize; N_INPUTS] = arr![AtomicUsize::new(0); 16];
static VOLS12: [AtomicUsize; N_INPUTS] = arr![AtomicUsize::new(0); 16];
static VOLS13: [AtomicUsize; N_INPUTS] = arr![AtomicUsize::new(0); 16];
static VOLS14: [AtomicUsize; N_INPUTS] = arr![AtomicUsize::new(0); 16];
static VOLS15: [AtomicUsize; N_INPUTS] = arr![AtomicUsize::new(0); 16];
static VOLS16: [AtomicUsize; N_INPUTS] = arr![AtomicUsize::new(0); 16];
fn main() {
let matches = App::new(option_env!("CARGO_PKG_NAME").unwrap())
.version(option_env!("CARGO_PKG_VERSION").unwrap())
.about(option_env!("CARGO_PKG_DESCRIPTION").unwrap())
.help_template("{bin} ({version})\n{about}\n\nUsage:\n {usage}\n\nTo send a certain amount of one input to a output send a OSC message that looks like this: /ch/1/send/16 <volume>\nThis would set the send volume of input 1 to output 16 to the value of <volume>. A value of 0.0 would be off, a value of 1.0 would result in a unattenuated signal. Values above 1.0 result in amplification.\n\nOSC-Addresses:\n /ch/<in>/send/<out> <volume> Set the send volume (in → out)\n\nOptions:\n{options}")
.arg(Arg::new("--host")
.long("--host")
.value_name("HOST")
.required(true)
.about(format!("Set IP-address of {}", option_env!("CARGO_PKG_NAME").unwrap()).as_str())
.takes_value(true))
.arg(Arg::new("--receiving-port")
.long("--receiving-port")
.value_name("INPORT")
.required(true)
.about("Set receiving port (OSC)")
.takes_value(true))
.arg(Arg::new("--sending-port")
.long("--sending-port")
.value_name("OUTPORT")
.about("Set sending port (OSC)")
.takes_value(true))
.arg(Arg::new("--name")
.long("--name")
.value_name("NAME")
.about("Set the name of the jack node")
.takes_value(true))
.get_matches();
let ip = matches.value_of("--host").unwrap();
let inport = matches.value_of("--receiving-port").unwrap();
// Receive OSC-Packets at this address
let addr = SocketAddrV4::from_str(format!("{}:{}", ip, inport).as_str()).expect("Please use a valid IP:Port addr");
let sock = UdpSocket::bind(addr).unwrap();
let mut buf = [0u8; rosc::decoder::MTU];
let nodename = matches.value_of("--name").unwrap_or("Hexmatrix");
// Create new jack client (visible e.g. in Catia)
let (client, _status) =
jack::Client::new(nodename, jack::ClientOptions::NO_START_SERVER).expect("Couldn't connect to jack server. Not running?");
// Open n Jack Input Ports via arr! macro.
let mut i = 0u16;
let inputs: [Port<AudioIn>; 16] = arr![
client.register_port(
format!("in_{}", {i += 1; i}).as_str(),
jack::AudioIn::default())
.unwrap();
16];
// Open n Jack Output Ports via arr! macro.
i = 0;
let mut outputs: [Port<AudioOut>; 16] = arr![
client.register_port(
format!("out_{}", {i += 1; i}).as_str(),
jack::AudioOut::default())
.unwrap();
16];
let buffersize = client.buffer_size() as usize;
// Is called asynchronously. Moves/mixes the actual audio samples from A to be
let process_callback = move |_: &jack::Client, ps: &jack::ProcessScope| -> jack::Control {
// For every output get a sum of corresponding inputs
let mut output_buffers: [[f32; 1024]; N_OUTPUTS] = [[0.0; 1024]; N_OUTPUTS];
// Iterate over the inputs and add values to the corresponding outputs
// (in this case for testing purposes: all to all)
inputs.iter()
.enumerate()
.for_each(|(input_index, inport)| {
// Get a slice [f32; 1024] of samples for each port
let input_samples = inport.as_slice(ps);
// Sum each input to output buffer
output_buffers.iter_mut()
.take(buffersize)
.enumerate()
.for_each(|(output_index, b)| {
b.iter_mut()
.take(buffersize)
.enumerate()
.for_each(|(sample_index, sample)| {
// For each input get the corrsponding atomic value for the volume control
let volume = match input_index {
0 => VOLS1[output_index].load(Ordering::Relaxed) as f32 / usize::MAX as f32,
1 => VOLS2[output_index].load(Ordering::Relaxed) as f32 / usize::MAX as f32,
2 => VOLS3[output_index].load(Ordering::Relaxed) as f32 / usize::MAX as f32,
3 => VOLS4[output_index].load(Ordering::Relaxed) as f32 / usize::MAX as f32,
4 => VOLS5[output_index].load(Ordering::Relaxed) as f32 / usize::MAX as f32,
5 => VOLS6[output_index].load(Ordering::Relaxed) as f32 / usize::MAX as f32,
6 => VOLS7[output_index].load(Ordering::Relaxed) as f32 / usize::MAX as f32,
7 => VOLS8[output_index].load(Ordering::Relaxed) as f32 / usize::MAX as f32,
8 => VOLS9[output_index].load(Ordering::Relaxed) as f32 / usize::MAX as f32,
9 => VOLS10[output_index].load(Ordering::Relaxed) as f32 / usize::MAX as f32,
10 => VOLS11[output_index].load(Ordering::Relaxed) as f32 / usize::MAX as f32,
11 => VOLS12[output_index].load(Ordering::Relaxed) as f32 / usize::MAX as f32,
12 => VOLS13[output_index].load(Ordering::Relaxed) as f32 / usize::MAX as f32,
13 => VOLS14[output_index].load(Ordering::Relaxed) as f32 / usize::MAX as f32,
14 => VOLS15[output_index].load(Ordering::Relaxed) as f32 / usize::MAX as f32,
15 => VOLS16[output_index].load(Ordering::Relaxed) as f32 / usize::MAX as f32,
_ => 0.0
};
if volume != 0.0 {
// Multiply each input sample at position `sample_index` with the
// volume and sum it with the corresponding (existing) output sample
// at the same position
*sample += input_samples[sample_index] * volume;
}
});
});
});
// Iterate over the outputs and clone the values of the corresponding
// output buffer to the output
outputs.iter_mut()
.enumerate()
.for_each(|(output_index, outport)| {
outport.as_mut_slice(ps).clone_from_slice(&output_buffers[output_index]);
});
jack::Control::Continue
};
// Register the above closure with jack
let process = jack::ClosureProcessHandler::new(process_callback);
// Activate the client, which starts the processing.
let active_client = client.activate_async(Notifications, process).unwrap();
// Loop and receive OSC packages
loop {
match sock.recv_from(&mut buf) {
Ok((size, addr)) => {
// println!("Received packet with size {} from: {}", size, addr);
let packet = rosc::decoder::decode(&buf[..size]).unwrap();
handle_packet(packet);
}
Err(e) => {
println!("Error receiving from socket: {}", e);
break;
}
}
}
// Wait for user input to quit
println!("Press enter/return to quit...");
let mut user_input = String::new();
io::stdin().read_line(&mut user_input).ok();
active_client.deactivate().unwrap();
}
/// Handles received OSC packets
fn handle_packet(packet: OscPacket) {
match packet {
OscPacket::Message(msg) => {
process_message(&msg);
}
OscPacket::Bundle(bundle) => {
match &bundle.content[0] {
OscPacket::Message(msg) => process_message(msg),
_ => {}
}
}
}
}
/// Decides what to do with incoming OSC messages (e.g. set the atomic variables)
///
/// Implemented addresses:
/// - `/ch/{input}/send/{output} volume`: Set the volume at which a input is sent to an output (default is 0.0, no attenuation is 1.0, 7.0 equals +18 dB)
fn process_message(msg: &OscMessage) {
// Split address at "/" and skip initial empty split
let addr = msg.addr.trim().split("/").skip(1).collect::<Vec<&str>>();
// Match the addresses
match addr[..] {
["ch", _, "send", _] => {
// Set the send volume for a given input/output pair
// Match the input channel
let maybe_input = match addr[..][1].parse::<usize>() {
Ok(n @ 1..=N_INPUTS) => Some(n),
_ => None
};
// Match the output channel
let maybe_output = match addr[..][3].parse::<usize>() {
Ok(n @ 1..=N_OUTPUTS) => Some(n),
_ => None
};
// Try to extract the Volume from the first arg
let maybe_volume = msg.args[0].clone().float();
// When both is set, do things
match (maybe_input, maybe_output, maybe_volume) {
(Some(input), Some(output), Some(volume)) => {
// println!("ᐅ OSC: Set Volume of Input {} at Output {} to {:.8} ({:.8} dB)", input, output, volume.max(0.0), (20.0* volume.max(0.0).log(10.0)));
match input {
1 => VOLS1[output-1].store((volume * usize::MAX as f32) as usize, Ordering::Relaxed),
2 => VOLS2[output-1].store((volume * usize::MAX as f32) as usize, Ordering::Relaxed),
3 => VOLS3[output-1].store((volume * usize::MAX as f32) as usize, Ordering::Relaxed),
4 => VOLS4[output-1].store((volume * usize::MAX as f32) as usize, Ordering::Relaxed),
5 => VOLS5[output-1].store((volume * usize::MAX as f32) as usize, Ordering::Relaxed),
6 => VOLS6[output-1].store((volume * usize::MAX as f32) as usize, Ordering::Relaxed),
7 => VOLS7[output-1].store((volume * usize::MAX as f32) as usize, Ordering::Relaxed),
8 => VOLS8[output-1].store((volume * usize::MAX as f32) as usize, Ordering::Relaxed),
9 => VOLS9[output-1].store((volume * usize::MAX as f32) as usize, Ordering::Relaxed),
10 => VOLS10[output-1].store((volume * usize::MAX as f32) as usize, Ordering::Relaxed),
11 => VOLS11[output-1].store((volume * usize::MAX as f32) as usize, Ordering::Relaxed),
12 => VOLS12[output-1].store((volume * usize::MAX as f32) as usize, Ordering::Relaxed),
13 => VOLS13[output-1].store((volume * usize::MAX as f32) as usize, Ordering::Relaxed),
14 => VOLS14[output-1].store((volume * usize::MAX as f32) as usize, Ordering::Relaxed),
15 => VOLS15[output-1].store((volume * usize::MAX as f32) as usize, Ordering::Relaxed),
16 => VOLS16[output-1].store((volume * usize::MAX as f32) as usize, Ordering::Relaxed),
_ => ()
}
},
_ => println!("ᐅ OSC: Ignored message with non-existing channels: {}, {:?} {:?}", msg.addr, maybe_input, maybe_output)
}
},
_ => {
println!("ᐅ OSC: Ignored message with unknown address: {}", msg.addr);
}
}
}
/// Notifications for Jack
struct Notifications;
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);
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, _: &jack::Client, _port_id: jack::PortId, _is_reg: bool) {
// 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
}
fn latency(&mut self, _: &jack::Client, mode: jack::LatencyType) {
println!(
"JACK: {} latency has changed",
match mode {
jack::LatencyType::Capture => "capture",
jack::LatencyType::Playback => "playback",
}
);
}
}