diff --git a/Justfile b/Justfile index 8ea06719..a6ff44fe 100644 --- a/Justfile +++ b/Justfile @@ -31,9 +31,31 @@ transport-release: arranger: reset cargo run --bin tek_arranger +arranger-ext: + reset + cargo run --bin tek_arranger -- -n tek \ + -i "1=Midi-Bridge:nanoKEY Studio 2:(capture_0) nanoKEY Studio nanoKEY Studio _" \ + -o "1=Midi-Bridge:Komplete Audio 6 1:(playback_0) Komplete Audio 6 MIDI 1" \ + -i "2=Midi-Bridge:nanoKEY Studio 2:(capture_0) nanoKEY Studio nanoKEY Studio _" \ + -o "2=Midi-Bridge:Komplete Audio 6 1:(playback_0) Komplete Audio 6 MIDI 1" \ + -i "3=Midi-Bridge:nanoKEY Studio 2:(capture_0) nanoKEY Studio nanoKEY Studio _" \ + -o "3=Midi-Bridge:Komplete Audio 6 1:(playback_0) Komplete Audio 6 MIDI 1" \ + -i "4=Midi-Bridge:nanoKEY Studio 2:(capture_0) nanoKEY Studio nanoKEY Studio _" \ + -o "4=Midi-Bridge:Komplete Audio 6 1:(playback_0) Komplete Audio 6 MIDI 1" arranger-release: reset cargo run --release --bin tek_arranger +arranger-release-ext: + reset + cargo run --release --bin tek_arranger -- -n tek \ + -i "1=Midi-Bridge:nanoKEY Studio 2:(capture_0) nanoKEY Studio nanoKEY Studio _" \ + -o "1=Midi-Bridge:Komplete Audio 6 1:(playback_0) Komplete Audio 6 MIDI 1" \ + -i "2=Midi-Bridge:nanoKEY Studio 2:(capture_0) nanoKEY Studio nanoKEY Studio _" \ + -o "2=Midi-Bridge:Komplete Audio 6 1:(playback_0) Komplete Audio 6 MIDI 1" \ + -i "3=Midi-Bridge:nanoKEY Studio 2:(capture_0) nanoKEY Studio nanoKEY Studio _" \ + -o "3=Midi-Bridge:Komplete Audio 6 1:(playback_0) Komplete Audio 6 MIDI 1" \ + -i "4=Midi-Bridge:nanoKEY Studio 2:(capture_0) nanoKEY Studio nanoKEY Studio _" \ + -o "4=Midi-Bridge:Komplete Audio 6 1:(playback_0) Komplete Audio 6 MIDI 1" groovebox: reset diff --git a/crates/tek/src/cli/cli_arranger.rs b/crates/tek/src/cli/cli_arranger.rs index 4470c606..a5dd7d86 100644 --- a/crates/tek/src/cli/cli_arranger.rs +++ b/crates/tek/src/cli/cli_arranger.rs @@ -4,22 +4,33 @@ pub fn main () -> Usually<()> { ArrangerCli::parse().run() } -/// Parses CLI arguments to the `tek_arranger` invocation. +/// Launches an interactive MIDI arranger. #[derive(Debug, Parser)] #[command(version, about, long_about = None)] pub struct ArrangerCli { /// Name of JACK client #[arg(short, long)] - name: Option, + name: Option, + /// Whether to include a transport toolbar (default: true) #[arg(short, long, default_value_t = true)] transport: bool, + /// Number of tracks #[arg(short = 'x', long, default_value_t = 4)] - tracks: usize, + tracks: usize, + /// Number of scenes #[arg(short, long, default_value_t = 8)] - scenes: usize, + scenes: usize, + + /// MIDI outs to connect each track to. + #[arg(short='i', long)] + midi_from: Vec, + + /// MIDI ins to connect each track to. + #[arg(short='o', long)] + midi_to: Vec, } impl ArrangerCli { @@ -31,8 +42,9 @@ impl ArrangerCli { } Tui::run(JackClient::new(client_name.as_str())?.activate_with(|jack|{ let mut app = ArrangerTui::try_from(jack)?; + let jack = jack.read().unwrap(); app.color = ItemPalette::random(); - add_tracks(&mut app, self.tracks)?; + add_tracks(&jack, &mut app, &self)?; add_scenes(&mut app, self.scenes)?; Ok(app) })?)?; @@ -40,7 +52,8 @@ impl ArrangerCli { } } -fn add_tracks (app: &mut ArrangerTui, n: usize) -> Usually<()> { +fn add_tracks (jack: &JackClient, app: &mut ArrangerTui, cli: &ArrangerCli) -> Usually<()> { + let n = cli.tracks; let track_color_1 = ItemColor::random(); let track_color_2 = ItemColor::random(); for i in 0..n { @@ -48,6 +61,60 @@ fn add_tracks (app: &mut ArrangerTui, n: usize) -> Usually<()> { track_color_1.mix(track_color_2, i as f32 / n as f32).into() ))?; track.width = 8; + let name = track.name.read().unwrap(); + track.player.midi_ins.push( + jack.register_port(&format!("{}I", &name), MidiIn::default())? + ); + track.player.midi_outs.push( + jack.register_port(&format!("{}O", &name), MidiOut::default())? + ); + } + for connection in cli.midi_from.iter() { + let mut split = connection.split("="); + let number = split.next().unwrap().trim(); + if let Ok(track) = number.parse::() { + if track < 1 { + panic!("Tracks are zero-indexed") + } + if track > n { + panic!("Tried to connect track {track} or {n}. Pass -t {track} to increase number of tracks.") + } + if let Some(port) = split.next() { + if let Some(port) = jack.port_by_name(&port) { + jack.client().connect_ports(&port, &app.tracks[track-1].player.midi_ins[0])?; + //jack.client().connect_ports(&port, &app.tracks[track].player.midi_ins[0])?; + } else { + panic!("Missing MIDI output: {port}. Use jack_lsp to list all port names."); + } + } else { + panic!("No port specified for track {track}. Format is TRACK_NUMBER=PORT_NAME") + } + } else { + panic!("Failed to parse track number: {number}") + } + } + for connection in cli.midi_to.iter() { + let mut split = connection.split("="); + let number = split.next().unwrap().trim(); + if let Ok(track) = number.parse::() { + if track < 1 { + panic!("Tracks are zero-indexed") + } + if track > n { + panic!("Tried to connect track {track} or {n}. Pass -t {track} to increase number of tracks.") + } + if let Some(port) = split.next() { + if let Some(port) = jack.port_by_name(&port) { + jack.client().connect_ports(&app.tracks[track-1].player.midi_outs[0], &port)?; + } else { + panic!("Missing MIDI input: {port}. Use jack_lsp to list all port names."); + } + } else { + panic!("No port specified for track {track}. Format is TRACK_NUMBER=PORT_NAME") + } + } else { + panic!("Failed to parse track number: {number}") + } } Ok(()) } diff --git a/crates/tek/src/cli/cli_sequencer.rs b/crates/tek/src/cli/cli_sequencer.rs index 52d935d7..f5cbb3b3 100644 --- a/crates/tek/src/cli/cli_sequencer.rs +++ b/crates/tek/src/cli/cli_sequencer.rs @@ -4,24 +4,29 @@ pub fn main () -> Usually<()> { SequencerCli::parse().run() } +/// Launches a single interactive MIDI sequencer. #[derive(Debug, Parser)] #[command(version, about, long_about = None)] pub struct SequencerCli { /// Name of JACK client #[arg(short, long)] name: Option, + + /// Whether to include a transport toolbar (default: true) + #[arg(short, long, default_value_t = true)] + transport: bool, + /// MIDI outs to connect to (multiple instances accepted) #[arg(short='i', long)] midi_from: Vec, + /// MIDI ins to connect to (multiple instances accepted) #[arg(short='o', long)] midi_to: Vec, + /// Default phrase duration (in pulses; default: 4 * PPQ = 1 bar) #[arg(short, long)] length: Option, - /// Whether to include a transport toolbar (default: true) - #[arg(short, long, default_value_t = true)] - transport: bool } impl SequencerCli { @@ -30,8 +35,8 @@ impl SequencerCli { Tui::run(JackClient::new(name)?.activate_with(|jack|{ let mut app = SequencerTui::try_from(jack)?; let jack = jack.read().unwrap(); - let midi_in = jack.register_port("in", MidiIn::default())?; - let midi_out = jack.register_port("out", MidiOut::default())?; + let midi_in = jack.register_port("i", MidiIn::default())?; + let midi_out = jack.register_port("o", MidiOut::default())?; connect_from(&jack, &midi_in, &self.midi_from)?; connect_to(&jack, &midi_out, &self.midi_to)?; app.player.midi_ins.push(midi_in); @@ -52,7 +57,7 @@ fn connect_from (jack: &JackClient, input: &Port, ports: &[String]) -> U if let Some(port) = jack.port_by_name(&port) { jack.client().connect_ports(&port, &input)?; } else { - panic!("Missing MIDI output: {port}"); + panic!("Missing MIDI output: {port}. Use jack_lsp to list all port names."); } } Ok(()) @@ -63,7 +68,7 @@ fn connect_to (jack: &JackClient, output: &Port, ports: &[String]) -> U if let Some(port) = jack.port_by_name(&port) { jack.client().connect_ports(&output, &port)?; } else { - panic!("Missing MIDI output: {port}"); + panic!("Missing MIDI input: {port}. Use jack_lsp to list all port names."); } } Ok(())