From 7bb3f6224d806b2d80b824e06b88be2ded8a50d8 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 8 Jan 2025 15:55:18 +0100 Subject: [PATCH] unify cli --- Cargo.toml | 32 +++-- README.md | 31 +++-- bin/cli_arranger.rs | 129 ------------------ bin/cli_clock.rs | 8 -- bin/cli_groovebox.rs | 76 ----------- bin/cli_sampler.rs | 48 ------- bin/cli_sequencer.rs | 47 ------- bin/lib.rs | 57 -------- bin/tek.rs | 231 +++++++++++++++++++++++++++++++++ src/arranger/arranger_scene.rs | 10 ++ src/arranger/arranger_track.rs | 71 ++++++++++ src/sequencer.rs | 44 ++++--- 12 files changed, 374 insertions(+), 410 deletions(-) delete mode 100644 bin/cli_arranger.rs delete mode 100644 bin/cli_clock.rs delete mode 100644 bin/cli_groovebox.rs delete mode 100644 bin/cli_sampler.rs delete mode 100644 bin/cli_sequencer.rs delete mode 100644 bin/lib.rs create mode 100644 bin/tek.rs diff --git a/Cargo.toml b/Cargo.toml index ff199121..c1d320e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,24 +30,28 @@ wavers = "1.4.3" default = [] [[bin]] -name = "tek_arranger" -path = "bin/cli_arranger.rs" +name = "tek" +path = "bin/tek.rs" -[[bin]] -name = "tek_sequencer" -path = "bin/cli_sequencer.rs" +#[[bin]] +#name = "tek_arranger" +#path = "bin/cli_arranger.rs" -[[bin]] -name = "tek_groovebox" -path = "bin/cli_groovebox.rs" +#[[bin]] +#name = "tek_sequencer" +#path = "bin/cli_sequencer.rs" -[[bin]] -name = "tek_clock" -path = "bin/cli_clock.rs" +#[[bin]] +#name = "tek_groovebox" +#path = "bin/cli_groovebox.rs" -[[bin]] -name = "tek_sampler" -path = "bin/cli_sampler.rs" +#[[bin]] +#name = "tek_clock" +#path = "bin/cli_clock.rs" + +#[[bin]] +#name = "tek_sampler" +#path = "bin/cli_sampler.rs" #[[bin]] #name = "tek_mixer" diff --git a/README.md b/README.md index f5022158..08eeb3ed 100644 --- a/README.md +++ b/README.md @@ -15,18 +15,29 @@ or [**matrix** `@unspeaker:matrix.org`](https://matrix.to/#/@unspeaker:matrix.or ![Screenshot](https://codeberg.org/unspeaker/tek/releases/download/0.2.0-rc.7/Screenshot%20From%202025-01-02%2023-18-05.png) -this codebase produces the following binaries: +``` +Usage: tek [OPTIONS] -* **`tek_sequencer`** is a single-track, multi-pattern MIDI sequencer with properly tempo-synced pattern switch -* **`tek_groovebox`** connects the sequencer to a sampler, so that you can sample while you sequence -* **`tek_arranger`** is a multi-track sequencer based on a familiar clip launching UI -* **`tek_transport`** is a JACK transport controller -* **`tek_sampler`** is a MIDI-controlled sampler -* **`tek_plugin`** is an audio plugin host. -* **`tek_channel`** is a standalone channel strip -* **`tek_mixer`** is an audio mixer. +Commands: + clock A standalone transport view + sequencer A MIDI sequencer + sampler A MIDI-controlled audio sampler + groovebox Sequencer and sampler together + arranger Multi-track MIDI sequencer + mixer TODO: A MIDI-controlled audio mixer + track TODO: A customizable channel strip + plugin TODO: An audio plugin host + help Print this message or the help of the given subcommand(s) -some of these are currently work in progress. +Options: + -t, --name Name of JACK client + -S, --sync-lead Whether to attempt to become transport master + -s, --sync-follow Whether to sync to external transport master + -b, --bpm Initial tempo in beats per minute + -h, --help Print help + -V, --version Print version + +``` the project roadmap is at https://codeberg.org/unspeaker/tek/milestones diff --git a/bin/cli_arranger.rs b/bin/cli_arranger.rs deleted file mode 100644 index 54f22c35..00000000 --- a/bin/cli_arranger.rs +++ /dev/null @@ -1,129 +0,0 @@ -include!("./lib.rs"); -use tek::Arranger; -pub fn main () -> Usually<()> { ArrangerCli::parse().run() } - -/// 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, - /// 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 = 16)] - tracks: usize, - /// Width of tracks - #[arg(short = 'w', long, default_value_t = 6)] - track_width: usize, - /// Number of scenes - #[arg(short, long, default_value_t = 8)] - 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 { - /// Run the arranger TUI from CLI arguments. - fn run (&self) -> Usually<()> { - let name = self.name.as_deref().unwrap_or("tek_arranger"); - let engine = Tui::new()?; - let state = JackConnection::new(name)?.activate_with(|jack|{ - let mut app = Arranger::try_from(jack)?; - let jack = jack.read().unwrap(); - app.color = ItemPalette::random(); - add_tracks(&jack, &mut app, self)?; - add_scenes(&mut app, self.scenes)?; - Ok(app) - })?; - engine.run(&state) - } -} - -fn add_tracks (jack: &JackConnection, app: &mut Arranger, cli: &ArrangerCli) -> Usually<()> { - let n = cli.tracks; - let track_color_1 = ItemColor::random(); - let track_color_2 = ItemColor::random(); - for i in 0..n { - let track = app.track_add(None, Some( - track_color_1.mix(track_color_2, i as f32 / n as f32).into() - ))?; - track.width = cli.track_width; - let name = track.name.clone(); - 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).as_ref() { - jack.client().connect_ports(port, &app.tracks[track-1].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).as_ref() { - 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(()) -} - -fn add_scenes (app: &mut Arranger, n: usize) -> Usually<()> { - let scene_color_1 = ItemColor::random(); - let scene_color_2 = ItemColor::random(); - for i in 0..n { - let _scene = app.scene_add(None, Some( - scene_color_1.mix(scene_color_2, i as f32 / n as f32).into() - ))?; - } - Ok(()) -} - -#[test] fn verify_arranger_cli () { - use clap::CommandFactory; - ArrangerCli::command().debug_assert(); -} diff --git a/bin/cli_clock.rs b/bin/cli_clock.rs deleted file mode 100644 index 2bee935c..00000000 --- a/bin/cli_clock.rs +++ /dev/null @@ -1,8 +0,0 @@ -include!("./lib.rs"); - -/// Application entrypoint. -pub fn main () -> Usually<()> { - let name = "tek_transport"; - Tui::new()?.run(&JackConnection::new(name)? - .activate_with(|jack|TransportTui::new(jack))?) -} diff --git a/bin/cli_groovebox.rs b/bin/cli_groovebox.rs deleted file mode 100644 index 472916e3..00000000 --- a/bin/cli_groovebox.rs +++ /dev/null @@ -1,76 +0,0 @@ -include!("./lib.rs"); -pub fn main () -> Usually<()> { GrooveboxCli::parse().run() } -#[derive(Debug, Parser)] -#[command(version, about, long_about = None)] -pub struct GrooveboxCli { - /// 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, - /// Whether to attempt to become transport master - #[arg(short='S', long, default_value_t = false)] - sync_lead: bool, - /// Whether to attempt to become transport master - #[arg(short='s', long, default_value_t = true)] - sync_follow: bool, - /// Default BPM - #[arg(short='b', long, default_value = None)] - bpm: Option, - /// MIDI outs to connect to MIDI input - #[arg(short='i', long)] - midi_from: Vec, - /// MIDI ins to connect from MIDI output - #[arg(short='o', long)] - midi_to: Vec, - /// Audio outs to connect to left input - #[arg(short='l', long)] - l_from: Vec, - /// Audio outs to connect to right input - #[arg(short='r', long)] - r_from: Vec, - /// Audio ins to connect from left output - #[arg(short='L', long)] - l_to: Vec, - /// Audio ins to connect from right output - #[arg(short='R', long)] - r_to: Vec, -} -impl GrooveboxCli { - fn run (&self) -> Usually<()> { - let name = self.name.as_deref().unwrap_or("tek_groovebox"); - let engine = Tui::new()?; - let state = JackConnection::new(name)?.activate_with(|jack|{ - let app = tek::Groovebox::new( - jack, - &self.midi_from.as_slice(), - &self.midi_to.as_slice(), - &[&self.l_from.as_slice(), &self.r_from.as_slice()], - &[&self.l_to.as_slice(), &self.r_to.as_slice()], - )?; - if let Some(bpm) = self.bpm { - app.clock().timebase.bpm.set(bpm); - } - if self.sync_lead { - jack.read().unwrap().client().register_timebase_callback(false, |mut state|{ - app.clock().playhead.update_from_sample(state.position.frame() as f64); - state.position.bbt = Some(app.clock().bbt()); - state.position - })? - } else if self.sync_follow { - jack.read().unwrap().client().register_timebase_callback(false, |state|{ - app.clock().playhead.update_from_sample(state.position.frame() as f64); - state.position - })? - } - Ok(app) - })?; - engine.run(&state) - } -} - -#[test] fn verify_groovebox_cli () { - use clap::CommandFactory; - GrooveboxCli::command().debug_assert(); -} diff --git a/bin/cli_sampler.rs b/bin/cli_sampler.rs deleted file mode 100644 index d28bcf52..00000000 --- a/bin/cli_sampler.rs +++ /dev/null @@ -1,48 +0,0 @@ -include!("./lib.rs"); -pub fn main () -> Usually<()> { SamplerCli::parse().run() } -#[derive(Debug, Parser)] #[command(version, about, long_about = None)] pub struct SamplerCli { - /// Name of JACK client - #[arg(short, long)] name: Option, - /// Path to plugin - #[arg(short, long)] path: Option, - /// MIDI outs to connect to MIDI input - #[arg(short='i', long)] - midi_from: Vec, - /// Audio outs to connect to left input - #[arg(short='l', long)] - l_from: Vec, - /// Audio outs to connect to right input - #[arg(short='r', long)] - r_from: Vec, - /// Audio ins to connect from left output - #[arg(short='L', long)] - l_to: Vec, - /// Audio ins to connect from right output - #[arg(short='R', long)] - r_to: Vec, -} -impl SamplerCli { - fn run (&self) -> Usually<()> { - let name = self.name.as_deref().unwrap_or("tek_sampler"); - let engine = Tui::new()?; - let state = JackConnection::new(name)?.activate_with(|jack|{ - Ok(tek::SamplerTui { - cursor: (0, 0), - editing: None, - mode: None, - size: Measure::new(), - note_lo: 36.into(), - note_pt: 36.into(), - color: ItemPalette::from(Color::Rgb(64, 128, 32)), - state: Sampler::new( - jack, - &"sampler", - &self.midi_from.as_slice(), - &[&self.l_from.as_slice(), &self.r_from.as_slice()], - &[&self.l_to.as_slice(), &self.r_to.as_slice()], - )?, - }) - })?; - engine.run(&state) - } -} diff --git a/bin/cli_sequencer.rs b/bin/cli_sequencer.rs deleted file mode 100644 index 604998d7..00000000 --- a/bin/cli_sequencer.rs +++ /dev/null @@ -1,47 +0,0 @@ -include!("./lib.rs"); - -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, -} - -impl SequencerCli { - fn run (&self) -> Usually<()> { - let name = self.name.as_deref().unwrap_or("tek_sequencer"); - let engine = Tui::new()?; - let state = JackConnection::new(name)?.activate_with(|jack|{ - let mut app = Sequencer::try_from(jack)?; - let jack = jack.read().unwrap(); - 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); - app.player.midi_outs.push(midi_out); - Ok(app) - })?; - engine.run(&state) - } -} - -#[test] fn verify_sequencer_cli () { - use clap::CommandFactory; - SequencerCli::command().debug_assert(); -} diff --git a/bin/lib.rs b/bin/lib.rs deleted file mode 100644 index 9d4f22c7..00000000 --- a/bin/lib.rs +++ /dev/null @@ -1,57 +0,0 @@ -#[allow(unused_imports)] use std::sync::Arc; -#[allow(unused_imports)] use clap::{self, Parser}; -#[allow(unused_imports)] use tek::{ - *, - jack::*, - tek_input::*, - tek_output::*, - tek_tui::{*, ratatui::prelude::Color} -}; - -#[allow(unused)] -fn connect_from (jack: &JackConnection, input: &Port, ports: &[String]) -> Usually<()> { - for port in ports.iter() { - if let Some(port) = jack.port_by_name(port).as_ref() { - jack.client().connect_ports(port, input)?; - } else { - panic!("Missing MIDI output: {port}. Use jack_lsp to list all port names."); - } - } - Ok(()) -} - -#[allow(unused)] -fn connect_to (jack: &JackConnection, output: &Port, ports: &[String]) -> Usually<()> { - for port in ports.iter() { - if let Some(port) = jack.port_by_name(port).as_ref() { - jack.client().connect_ports(output, port)?; - } else { - panic!("Missing MIDI input: {port}. Use jack_lsp to list all port names."); - } - } - Ok(()) -} - -#[allow(unused)] -fn connect_audio_from (jack: &JackConnection, input: &Port, ports: &[String]) -> Usually<()> { - for port in ports.iter() { - if let Some(port) = jack.port_by_name(port).as_ref() { - jack.client().connect_ports(port, input)?; - } else { - panic!("Missing MIDI output: {port}. Use jack_lsp to list all port names."); - } - } - Ok(()) -} - -#[allow(unused)] -fn connect_audio_to (jack: &JackConnection, output: &Port, ports: &[String]) -> Usually<()> { - for port in ports.iter() { - if let Some(port) = jack.port_by_name(port).as_ref() { - jack.client().connect_ports(output, port)?; - } else { - panic!("Missing MIDI input: {port}. Use jack_lsp to list all port names."); - } - } - Ok(()) -} diff --git a/bin/tek.rs b/bin/tek.rs new file mode 100644 index 00000000..07328f16 --- /dev/null +++ b/bin/tek.rs @@ -0,0 +1,231 @@ +#[allow(unused_imports)] use std::sync::Arc; +#[allow(unused_imports)] use clap::{self, Parser, Subcommand, ValueEnum}; +#[allow(unused_imports)] use tek::{ + *, + jack::*, + tek_input::*, + tek_output::*, + tek_tui::{*, ratatui::prelude::Color} +}; + +#[derive(Debug, Parser)] +#[command(version, about, long_about = None)] +pub struct TekCli { + /// Which app to initialize + #[command(subcommand)] mode: TekMode, + /// Name of JACK client + #[arg(short='t', long)] name: Option, + /// Whether to attempt to become transport master + #[arg(short='S', long, default_value_t = false)] sync_lead: bool, + /// Whether to sync to external transport master + #[arg(short='s', long, default_value_t = true)] sync_follow: bool, + /// Initial tempo in beats per minute + #[arg(short='b', long, default_value = None)] bpm: Option, +} + +#[derive(Debug, Clone, Subcommand)] +pub enum TekMode { + /// A standalone transport view. + Clock, + /// A MIDI sequencer. + Sequencer { + /// Whether to include a transport toolbar (default: true) + #[arg(short='t', long, default_value_t = true)] show_clock: 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, + }, + /// A MIDI-controlled audio sampler. + Sampler { + /// MIDI outs to connect to (multiple instances accepted) + #[arg(short='i', long)] midi_from: Vec, + /// Audio outs to connect to left input + #[arg(short='l', long)] l_from: Vec, + /// Audio outs to connect to right input + #[arg(short='r', long)] r_from: Vec, + /// Audio ins to connect from left output + #[arg(short='L', long)] l_to: Vec, + /// Audio ins to connect from right output + #[arg(short='R', long)] r_to: Vec, + }, + /// Sequencer and sampler together. + Groovebox { + /// Whether to include a transport toolbar (default: true) + #[arg(short='t', long, default_value_t = true)] show_clock: 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, + /// Audio outs to connect to left input + #[arg(short='l', long)] l_from: Vec, + /// Audio outs to connect to right input + #[arg(short='r', long)] r_from: Vec, + /// Audio ins to connect from left output + #[arg(short='L', long)] l_to: Vec, + /// Audio ins to connect from right output + #[arg(short='R', long)] r_to: Vec, + }, + /// Multi-track MIDI sequencer. + Arranger { + /// Whether to include a transport toolbar (default: true) + #[arg(short='t', long, default_value_t = true)] show_clock: 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, + /// Audio outs to connect to left input + #[arg(short='l', long)] l_from: Vec, + /// Audio outs to connect to right input + #[arg(short='r', long)] r_from: Vec, + /// Audio ins to connect from left output + #[arg(short='L', long)] l_to: Vec, + /// Audio ins to connect from right output + #[arg(short='R', long)] r_to: Vec, + /// Number of tracks + #[arg(short = 'x', long, default_value_t = 16)] tracks: usize, + /// Width of tracks + #[arg(short = 'w', long, default_value_t = 6)] track_width: usize, + /// Number of scenes + #[arg(short = 'y', long, default_value_t = 8)] scenes: usize, + }, + /// TODO: A MIDI-controlled audio mixer + Mixer, + /// TODO: A customizable channel strip + Track, + /// TODO: An audio plugin host + Plugin, +} + +/// Application entrypoint. +pub fn main () -> Usually<()> { + use TekMode::*; + let cli = TekCli::parse(); + let name = cli.name.as_ref().map_or("tek", |x|x.as_str()); + let jack = JackConnection::new(name)?; + let engine = Tui::new()?; + Ok(match cli.mode { + Clock => + engine.run(&jack.activate_with(|jack|Ok( + crate::TransportTui::new(jack)? + ))?)?, + Sequencer { midi_from, midi_to, .. } => + engine.run(&jack.activate_with(|jack|Ok({ + let mut app = crate::Sequencer::new(jack)?; + let jack = jack.read().unwrap(); + let midi_in = jack.register_port("i", MidiIn::default())?; + let midi_out = jack.register_port("o", MidiOut::default())?; + connect_from(&jack, &midi_in, &midi_from)?; + connect_to(&jack, &midi_out, &midi_to)?; + app.player.midi_ins.push(midi_in); + app.player.midi_outs.push(midi_out); + app + }))?)?, + Sampler { midi_from, l_from, r_from, l_to, r_to, .. } => + engine.run(&jack.activate_with(|jack|Ok( + tek::SamplerTui { + cursor: (0, 0), + editing: None, + mode: None, + size: Measure::new(), + note_lo: 36.into(), + note_pt: 36.into(), + color: ItemPalette::from(Color::Rgb(64, 128, 32)), + state: tek::Sampler::new( + jack, &"sampler", + &midi_from, + &[&l_from, &r_from], + &[&l_to, &r_to], + )?, + } + ))?)?, + Groovebox { midi_from, midi_to, l_from, r_from, l_to, r_to, .. } => + engine.run(&jack.activate_with(|jack|Ok({ + let app = tek::Groovebox::new( + jack, + &midi_from, &midi_to, + &[&l_from, &r_from], + &[&l_to, &r_to], + )?; + if let Some(bpm) = cli.bpm { + app.clock().timebase.bpm.set(bpm); + } + if cli.sync_lead { + jack.read().unwrap().client().register_timebase_callback(false, |mut state|{ + app.clock().playhead.update_from_sample(state.position.frame() as f64); + state.position.bbt = Some(app.clock().bbt()); + state.position + })? + } else if cli.sync_follow { + jack.read().unwrap().client().register_timebase_callback(false, |state|{ + app.clock().playhead.update_from_sample(state.position.frame() as f64); + state.position + })? + } + app + }))?)?, + Arranger { scenes, tracks, track_width, midi_from, midi_to, .. } => + engine.run(&jack.activate_with(|jack|Ok({ + let mut app = crate::Arranger::try_from(jack)?; + app.color = ItemPalette::random(); + app.tracks_add(tracks, track_width, midi_from.as_slice(), midi_to.as_slice())?; + app.scenes_add(scenes)?; + app + }))?)?, + _ => todo!() + }) +} + +#[allow(unused)] +fn connect_from (jack: &JackConnection, input: &Port, ports: &[String]) -> Usually<()> { + for port in ports.iter() { + if let Some(port) = jack.port_by_name(port).as_ref() { + jack.client().connect_ports(port, input)?; + } else { + panic!("Missing MIDI output: {port}. Use jack_lsp to list all port names."); + } + } + Ok(()) +} + +#[allow(unused)] +fn connect_to (jack: &JackConnection, output: &Port, ports: &[String]) -> Usually<()> { + for port in ports.iter() { + if let Some(port) = jack.port_by_name(port).as_ref() { + jack.client().connect_ports(output, port)?; + } else { + panic!("Missing MIDI input: {port}. Use jack_lsp to list all port names."); + } + } + Ok(()) +} + +#[allow(unused)] +fn connect_audio_from (jack: &JackConnection, input: &Port, ports: &[String]) -> Usually<()> { + for port in ports.iter() { + if let Some(port) = jack.port_by_name(port).as_ref() { + jack.client().connect_ports(port, input)?; + } else { + panic!("Missing MIDI output: {port}. Use jack_lsp to list all port names."); + } + } + Ok(()) +} + +#[allow(unused)] +fn connect_audio_to (jack: &JackConnection, output: &Port, ports: &[String]) -> Usually<()> { + for port in ports.iter() { + if let Some(port) = jack.port_by_name(port).as_ref() { + jack.client().connect_ports(output, port)?; + } else { + panic!("Missing MIDI input: {port}. Use jack_lsp to list all port names."); + } + } + Ok(()) +} + + +#[test] fn verify_cli () { + use clap::CommandFactory; + TekCli::command().debug_assert(); +} diff --git a/src/arranger/arranger_scene.rs b/src/arranger/arranger_scene.rs index f0716f9f..e733a7d8 100644 --- a/src/arranger/arranger_scene.rs +++ b/src/arranger/arranger_scene.rs @@ -24,6 +24,16 @@ impl Arranger { pub fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> { self.selected.scene().and_then(|s|self.scenes.get_mut(s)) } + pub fn scenes_add (&mut self, n: usize) -> Usually<()> { + let scene_color_1 = ItemColor::random(); + let scene_color_2 = ItemColor::random(); + for i in 0..n { + let _scene = self.scene_add(None, Some( + scene_color_1.mix(scene_color_2, i as f32 / n as f32).into() + ))?; + } + Ok(()) + } } #[derive(Default, Debug, Clone)] pub struct ArrangerScene { /// Name of scene diff --git a/src/arranger/arranger_track.rs b/src/arranger/arranger_track.rs index ce5cc329..857514a5 100644 --- a/src/arranger/arranger_track.rs +++ b/src/arranger/arranger_track.rs @@ -23,6 +23,77 @@ impl Arranger { scene.clips.remove(index); } } + pub fn tracks_add ( + &mut self, + count: usize, + width: usize, + midi_from: &[impl AsRef], + midi_to: &[impl AsRef], + ) -> Usually<()> { + let jack = self.jack.clone(); + let track_color_1 = ItemColor::random(); + let track_color_2 = ItemColor::random(); + for i in 0..count { + let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into(); + let mut track = self.track_add(None, Some(color))?; + track.width = width; + + let name = &format!("{}I", &track.name); + let port = jack.read().unwrap().client().register_port(&name, MidiIn::default())?; + track.player.midi_ins.push(port); + + let name = &format!("{}O", &track.name); + let port = jack.read().unwrap().client().register_port(&name, MidiOut::default())?; + track.player.midi_outs.push(port); + } + for connection in midi_from.iter() { + let mut split = connection.as_ref().split("="); + let number = split.next().unwrap().trim(); + if let Ok(track) = number.parse::() { + if track < 1 { + panic!("Tracks are zero-indexed") + } + if track > count { + panic!("Tried to connect track {track} or {count}. Pass -t {track} to increase number of tracks.") + } + if let Some(port) = split.next() { + if let Some(port) = jack.read().unwrap().client().port_by_name(port).as_ref() { + jack.read().unwrap().client().connect_ports(port, &self.tracks[track-1].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 midi_to.iter() { + let mut split = connection.as_ref().split("="); + let number = split.next().unwrap().trim(); + if let Ok(track) = number.parse::() { + if track < 1 { + panic!("Tracks are zero-indexed") + } + if track > count { + panic!("Tried to connect track {track} or {count}. Pass -t {track} to increase number of tracks.") + } + if let Some(port) = split.next() { + if let Some(port) = jack.read().unwrap().client().port_by_name(port).as_ref() { + jack.read().unwrap().client().connect_ports(&self.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(()) + } } #[derive(Debug)] pub struct ArrangerTrack { /// Name of track diff --git a/src/sequencer.rs b/src/sequencer.rs index d5af1db7..de785aec 100644 --- a/src/sequencer.rs +++ b/src/sequencer.rs @@ -23,30 +23,32 @@ pub struct Sequencer { pub midi_buf: Vec>>, pub perf: PerfModel, } -from_jack!(|jack|Sequencer { - let clock = Clock::from(jack); - let phrase = Arc::new(RwLock::new(MidiClip::new( - "Clip", true, 4 * clock.timebase.ppq.get() as usize, - None, Some(ItemColor::random().into()) - ))); - Self { - _jack: jack.clone(), +impl Sequencer { + pub fn new (jack: &Arc>) -> Usually { + let clock = Clock::from(jack); + let phrase = Arc::new(RwLock::new(MidiClip::new( + "Clip", true, 4 * clock.timebase.ppq.get() as usize, + None, Some(ItemColor::random().into()) + ))); + Ok(Self { + _jack: jack.clone(), - pool: PoolModel::from(&phrase), - editor: MidiEditor::from(&phrase), - player: MidiPlayer::from((&clock, &phrase)), + pool: PoolModel::from(&phrase), + editor: MidiEditor::from(&phrase), + player: MidiPlayer::from((&clock, &phrase)), - compact: true, - transport: true, - selectors: true, - size: Measure::new(), - midi_buf: vec![vec![];65536], - note_buf: vec![], - perf: PerfModel::default(), - status: true, - clock, + compact: true, + transport: true, + selectors: true, + size: Measure::new(), + midi_buf: vec![vec![];65536], + note_buf: vec![], + perf: PerfModel::default(), + status: true, + clock, + }) } -}); +} render!(TuiOut: (self: Sequencer) => self.size.of( Bsp::s(self.toolbar_view(), Bsp::n(self.selector_view(),