diff --git a/Cargo.toml b/Cargo.toml index 9a39543a..db9dfecc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "bloop" +edition = "2021" [dependencies] jack = "0.10" diff --git a/src/main.rs b/src/main.rs index faddaf8d..9c85aacf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,10 +6,12 @@ use clap::{Parser, Subcommand}; use std::error::Error; //pub mod sequence; +pub mod prelude; pub mod engine; pub mod transport; pub mod mixer; pub mod looper; +pub mod sampler; fn main () -> Result<(), Box> { let cli = Cli::parse(); @@ -20,6 +22,8 @@ fn main () -> Result<(), Box> { crate::mixer::Mixer::run_tui(), Command::Looper => crate::looper::Looper::run_tui(), + Command::Sampler => + crate::sampler::Sampler::run_tui(), } } @@ -38,4 +42,6 @@ pub enum Command { Mixer, /// Control the looper Looper, + /// Control the sampler + Sampler, } diff --git a/src/mixer.rs b/src/mixer.rs index 6d884a2c..e948be61 100644 --- a/src/mixer.rs +++ b/src/mixer.rs @@ -1,69 +1,59 @@ -use std::error::Error; -use std::io::{stdout, Write}; -use std::thread::spawn; -use std::time::Duration; -use std::sync::{ - Arc, - atomic::{AtomicBool, Ordering}, - mpsc::{channel, Sender, Receiver} -}; +use crate::prelude::*; + +// TODO: +// - Meters: propagate clipping: +// - If one stage clips, all stages after it are marked red +// - If one track clips, all tracks that feed from it are marked red? pub struct Mixer { - exit: bool, - cols: u16, - rows: u16, - tracks: Vec, - selected_track: usize, + jack: Jack, + exit: bool, + stdout: Stdout, + cols: u16, + rows: u16, + tracks: Vec, + selected_track: usize, selected_column: usize, } -pub struct Track { - name: String, - gain: f64, - level: f64, - pan: f64, - route: String, -} - impl Mixer { - pub fn new () -> Self { - Self { - exit: false, - cols: 0, - rows: 0, + pub fn new () -> Result> { + let (client, status) = Client::new( + "bloop-mixer", + ClientOptions::NO_START_SERVER + )?; + let jack = client.activate_async( + Notifications, + ClosureProcessHandler::new(Box::new( + move |_: &Client, _: &ProcessScope| -> Control { + Control::Continue + }) as BoxControl + Send> + ) + )?; + Ok(Self { + exit: false, + stdout: std::io::stdout(), + cols: 0, + rows: 0, selected_column: 0, selected_track: 1, - tracks: vec! [ - Track { - name: "Track 1".into(), gain: 0.0, level: 0.0, pan: 0.0, route: "Bus 1".into() - }, - Track { - name: "Track 2".into(), gain: 0.0, level: 0.0, pan: 0.0, route: "Bus 2".into() - }, - Track { - name: "Track 3".into(), gain: 0.0, level: 0.0, pan: 0.0, route: "Bus 1".into() - }, - Track { - name: "Track 4".into(), gain: 0.0, level: 0.0, pan: 0.0, route: "Bus 2".into() - }, - Track { - name: "Bus 1".into(), gain: 0.0, level: 0.0, pan: 0.0, route: "Mix".into() - }, - Track { - name: "Bus 2".into(), gain: 0.0, level: 0.0, pan: 0.0, route: "Mix".into() - }, - Track { - name: "Mix".into(), gain: 0.0, level: 0.0, pan: 0.0, route: "Output".into() - }, - ] - } + tracks: vec![ + Track::new(&jack.as_client(), 1, "Kick")?, + Track::new(&jack.as_client(), 1, "Snare")?, + Track::new(&jack.as_client(), 2, "Hihats")?, + Track::new(&jack.as_client(), 2, "Sample")?, + Track::new(&jack.as_client(), 2, "Bus 1")?, + Track::new(&jack.as_client(), 2, "Bus 2")?, + Track::new(&jack.as_client(), 2, "Mix")?, + ], + jack, + }) } pub fn run_tui () -> Result<(), Box> { - let mut app = Self::new(); + let mut app = Self::new()?; let sleep = std::time::Duration::from_millis(16); - let mut stdout = std::io::stdout(); crossterm::terminal::enable_raw_mode()?; let (tx, input) = channel::(); let exited = Arc::new(AtomicBool::new(false)); @@ -84,9 +74,10 @@ impl Mixer { } }); loop { - app.render(&mut stdout)?; + app.render()?; app.handle(input.recv()?)?; if app.exit { + app.stdout.queue(cursor::Hide)?.flush()?; crossterm::terminal::disable_raw_mode()?; break } @@ -95,7 +86,6 @@ impl Mixer { } fn handle (&mut self, event: crossterm::event::Event) -> Result<(), Box> { - use crossterm::event::{Event, KeyCode, KeyModifiers}; if let Event::Key(event) = event { match event.code { @@ -107,7 +97,7 @@ impl Mixer { KeyCode::Down => { self.selected_track = (self.selected_track + 1) % self.tracks.len(); println!("{}", self.selected_track); - } + }, KeyCode::Up => { if self.selected_track == 0 { self.selected_track = self.tracks.len() - 1; @@ -115,7 +105,21 @@ impl Mixer { self.selected_track = self.selected_track - 1; } println!("{}", self.selected_track); - } + }, + KeyCode::Left => { + if self.selected_column == 0 { + self.selected_column = 6 + } else { + self.selected_column = self.selected_column - 1; + } + }, + KeyCode::Right => { + if self.selected_column == 6 { + self.selected_column = 0 + } else { + self.selected_column = self.selected_column + 1; + } + }, _ => { println!("{event:?}"); } @@ -124,54 +128,167 @@ impl Mixer { Ok(()) } - fn render (&mut self, stdout: &mut std::io::Stdout) -> Result<(), Box> { - use crossterm::{*, style::{*, Stylize}}; + fn render (&mut self) -> Result<(), Box> { let (cols, rows) = terminal::size()?; - if true || cols != self.cols || rows != self.rows { // TODO perf - self.cols = cols; - self.rows = rows; - stdout - .queue(terminal::Clear(terminal::ClearType::All))? - .queue(cursor::Hide)? + self.cols = cols; + self.rows = rows; + self.stdout.queue(terminal::Clear(terminal::ClearType::All))?; + self.stdout.queue(cursor::Hide)?; + self.render_toolbar()?; + self.render_table()?; + self.render_meters()?; + self.stdout.flush()?; + Ok(()) + } - .queue(cursor::MoveTo(1, 0))? - .queue(PrintStyledContent("[Arrows]".yellow().bold()))? - .queue(cursor::MoveTo(1, 1))? - .queue(PrintStyledContent("Navigate".yellow()))? + fn render_toolbar (&mut self) -> Result<(), Box> { + self.stdout + .queue(cursor::MoveTo(1, 0))? + .queue(PrintStyledContent("[Arrows]".yellow().bold()))? + .queue(cursor::MoveTo(1, 1))? + .queue(PrintStyledContent("Navigate".yellow()))? - .queue(cursor::MoveTo(11, 0))? - .queue(PrintStyledContent("[+/-]".yellow().bold()))? - .queue(cursor::MoveTo(11, 1))? - .queue(PrintStyledContent("Adjust value".yellow()))? + .queue(cursor::MoveTo(11, 0))? + .queue(PrintStyledContent("[+/-]".yellow().bold()))? + .queue(cursor::MoveTo(11, 1))? + .queue(PrintStyledContent("Adjust value".yellow()))? - .queue(cursor::MoveTo(25, 0))? - .queue(PrintStyledContent("[Ins/Del]".yellow().bold()))? - .queue(cursor::MoveTo(25, 1))? - .queue(PrintStyledContent("Add/remove track".yellow()))? + .queue(cursor::MoveTo(25, 0))? + .queue(PrintStyledContent("[Ins/Del]".yellow().bold()))? + .queue(cursor::MoveTo(25, 1))? + .queue(PrintStyledContent("Add/remove track".yellow()))?; + Ok(()) + } - .queue(cursor::MoveTo(0, 3))?.queue(Print( - " Name Gain Pre Level Pan Post Route"))?; - - for (i, track) in self.tracks.iter().enumerate() { - let row = 4 + i as u16; - let mut content = format!( - " {:7} █ {:.1}dB █ [ ] █ {:.1}dB C █ [ ] {:7} ", - track.name, - track.gain, - track.level, - track.route, - ).bold(); - if i == self.selected_track { - content = content.reverse(); + fn render_table (&mut self) -> Result<(), Box> { + self.stdout + .queue(cursor::MoveTo(0, 3))? + .queue(Print( + " Name Gain FX1 Pan Level FX2 Route"))?; + for (i, track) in self.tracks.iter().enumerate() { + let row = 4 + i as u16; + for (j, (column, field)) in [ + (0, format!(" {:7} ", track.name)), + (12, format!(" {:.1}dB ", track.gain)), + (22, format!(" [ ] ")), + (30, format!(" C ")), + (35, format!(" {:.1}dB ", track.level)), + (45, format!(" [ ] ")), + (51, format!(" {:7} ", track.route)), + ].into_iter().enumerate() { + self.stdout.queue(cursor::MoveTo(column, row))?; + if self.selected_track == i && self.selected_column == j { + self.stdout.queue(PrintStyledContent(field.to_string().bold().reverse()))?; + } else { + self.stdout.queue(PrintStyledContent(field.to_string().bold()))?; } - stdout - .queue(cursor::MoveTo(0, row))? - .queue(PrintStyledContent(content))?; } + } + Ok(()) + } - stdout - .flush()?; + fn render_meters (&mut self) -> Result<(), Box> { + for (i, track) in self.tracks.iter().enumerate() { + let row = 4 + i as u16; + self.stdout + .queue(cursor::MoveTo(10, row))? + .queue(PrintStyledContent("▁".green()))? + .queue(cursor::MoveTo(20, row))? + .queue(PrintStyledContent("▁".green()))? + .queue(cursor::MoveTo(28, row))? + .queue(PrintStyledContent("▁".green()))? + .queue(cursor::MoveTo(43, row))? + .queue(PrintStyledContent("▁".green()))?; } Ok(()) } } + +pub struct Track { + name: String, + channels: u8, + input_ports: Vec>, + pre_gain_meter: f64, + gain: f64, + insert_ports: Vec>, + return_ports: Vec>, + post_gain_meter: f64, + post_insert_meter: f64, + level: f64, + pan: f64, + output_ports: Vec>, + post_fader_meter: f64, + route: String, +} + +impl Track { + pub fn new (jack: &Client, channels: u8, name: &str) -> Result> { + let mut input_ports = vec![]; + let mut insert_ports = vec![]; + let mut return_ports = vec![]; + let mut output_ports = vec![]; + for channel in 1..=channels { + input_ports.push(jack.register_port(&format!("{name} [input {channel}]"), AudioIn::default())?); + output_ports.push(jack.register_port(&format!("{name} [out {channel}]"), AudioOut::default())?); + let insert_port = jack.register_port(&format!("{name} [pre {channel}]"), AudioOut::default())?; + let return_port = jack.register_port(&format!("{name} [insert {channel}]"), AudioIn::default())?; + jack.connect_ports(&insert_port, &return_port)?; + insert_ports.push(insert_port); + return_ports.push(return_port); + } + Ok(Self { + name: name.into(), + channels, + input_ports, + pre_gain_meter: 0.0, + gain: 0.0, + post_gain_meter: 0.0, + insert_ports, + return_ports, + post_insert_meter: 0.0, + level: 0.0, + pan: 0.0, + post_fader_meter: 0.0, + route: "---".into(), + output_ports, + }) + } +} + +struct Notifications; + +impl NotificationHandler for Notifications { + fn thread_init (&self, _: &Client) { + } + + fn shutdown (&mut self, status: ClientStatus, reason: &str) { + } + + fn freewheel (&mut self, _: &Client, is_enabled: bool) { + } + + fn sample_rate (&mut self, _: &Client, _: Frames) -> Control { + Control::Quit + } + + fn client_registration (&mut self, _: &Client, name: &str, is_reg: bool) { + } + + fn port_registration (&mut self, _: &Client, port_id: PortId, is_reg: bool) { + } + + fn port_rename (&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control { + Control::Continue + } + + fn ports_connected (&mut self, _: &Client, id_a: PortId, id_b: PortId, are: bool) { + } + + fn graph_reorder (&mut self, _: &Client) -> Control { + Control::Continue + } + + fn xrun (&mut self, _: &Client) -> Control { + Control::Continue + } +} diff --git a/src/prelude.rs b/src/prelude.rs new file mode 100644 index 00000000..caddb81f --- /dev/null +++ b/src/prelude.rs @@ -0,0 +1,34 @@ +pub use std::error::Error; +pub use std::io::{stdout, Stdout, Write}; +pub use std::thread::spawn; +pub use std::time::Duration; +pub use std::sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + mpsc::{channel, Sender, Receiver} +}; +pub use crossterm::{ + QueueableCommand, + cursor, + terminal, + style::*, +}; +pub use jack::{ + AsyncClient, + AudioIn, + AudioOut, + Client, + ClientOptions, + ClientStatus, + ClosureProcessHandler, + Control, + Frames, + NotificationHandler, + Port, + PortId, + ProcessScope, +}; +pub type Jack = AsyncClient< + N, + ClosureProcessHandler Control + Send>> +>; diff --git a/src/sampler.rs b/src/sampler.rs new file mode 100644 index 00000000..07380331 --- /dev/null +++ b/src/sampler.rs @@ -0,0 +1,247 @@ +use crate::prelude::*; + +pub struct Sampler { + jack: Jack, + exit: bool, + stdout: Stdout, + cols: u16, + rows: u16, + samples: Vec, + selected_sample: usize, + selected_column: usize, +} + +impl Sampler { + + pub fn new () -> Result> { + let (client, status) = Client::new( + "bloop-sampler", + ClientOptions::NO_START_SERVER + )?; + let jack = client.activate_async( + Notifications, + ClosureProcessHandler::new(Box::new( + move |_: &Client, _: &ProcessScope| -> Control { + Control::Continue + }) as BoxControl + Send> + ) + )?; + Ok(Self { + exit: false, + stdout: std::io::stdout(), + cols: 0, + rows: 0, + selected_sample: 0, + selected_column: 0, + samples: vec![ + Sample::new("Kick")?, + Sample::new("Snare")?, + ], + jack, + }) + } + + pub fn run_tui () -> Result<(), Box> { + let mut app = Self::new()?; + let sleep = std::time::Duration::from_millis(16); + crossterm::terminal::enable_raw_mode()?; + let (tx, input) = channel::(); + let exited = Arc::new(AtomicBool::new(false)); + // Spawn the input thread + let exit_input_thread = exited.clone(); + spawn(move || { + loop { + // Exit if flag is set + if exit_input_thread.fetch_and(true, Ordering::Relaxed) { + break + } + // Listen for events and send them to the main thread + if crossterm::event::poll(Duration::from_millis(100)).is_ok() { + if tx.send(crossterm::event::read().unwrap()).is_err() { + break + } + } + } + }); + loop { + app.render()?; + app.handle(input.recv()?)?; + if app.exit { + app.stdout.queue(cursor::Hide)?.flush()?; + crossterm::terminal::disable_raw_mode()?; + break + } + } + Ok(()) + } + + fn handle (&mut self, event: crossterm::event::Event) -> Result<(), Box> { + use crossterm::event::{Event, KeyCode, KeyModifiers}; + if let Event::Key(event) = event { + match event.code { + KeyCode::Char('c') => { + if event.modifiers == KeyModifiers::CONTROL { + self.exit = true; + } + }, + KeyCode::Down => { + self.selected_sample = (self.selected_sample + 1) % self.samples.len(); + println!("{}", self.selected_sample); + }, + KeyCode::Up => { + if self.selected_sample == 0 { + self.selected_sample = self.samples.len() - 1; + } else { + self.selected_sample = self.selected_sample - 1; + } + println!("{}", self.selected_sample); + }, + KeyCode::Left => { + if self.selected_column == 0 { + self.selected_column = 6 + } else { + self.selected_column = self.selected_column - 1; + } + }, + KeyCode::Right => { + if self.selected_column == 6 { + self.selected_column = 0 + } else { + self.selected_column = self.selected_column + 1; + } + }, + _ => { + println!("{event:?}"); + } + } + } + Ok(()) + } + + fn render (&mut self) -> Result<(), Box> { + let (cols, rows) = terminal::size()?; + self.cols = cols; + self.rows = rows; + self.stdout.queue(terminal::Clear(terminal::ClearType::All))?; + self.stdout.queue(cursor::Hide)?; + self.render_toolbar()?; + self.render_table()?; + self.render_meters()?; + self.stdout.flush()?; + Ok(()) + } + + fn render_toolbar (&mut self) -> Result<(), Box> { + self.stdout + .queue(cursor::MoveTo(1, 0))? + .queue(PrintStyledContent("[Arrows]".yellow().bold()))? + .queue(cursor::MoveTo(1, 1))? + .queue(PrintStyledContent("Navigate".yellow()))? + + .queue(cursor::MoveTo(12, 0))? + .queue(PrintStyledContent("[Enter]".yellow().bold()))? + .queue(cursor::MoveTo(12, 1))? + .queue(PrintStyledContent("Play sample".yellow()))? + + .queue(cursor::MoveTo(25, 0))? + .queue(PrintStyledContent("[Ins/Del]".yellow().bold()))? + .queue(cursor::MoveTo(25, 1))? + .queue(PrintStyledContent("Add/remove sample".yellow()))?; + Ok(()) + } + + fn render_table (&mut self) -> Result<(), Box> { + self.stdout + .queue(cursor::MoveTo(0, 3))? + .queue(Print( + " Name Rate Trigger Route"))?; + for (i, sample) in self.samples.iter().enumerate() { + let row = 4 + i as u16; + for (j, (column, field)) in [ + (0, format!(" {:7} ", sample.name)), + (9, format!(" {:.1}Hz ", sample.rate)), + (18, format!(" MIDI C10 36 ")), + (33, format!(" {:.1}dB -> Output ", sample.gain)), + ].into_iter().enumerate() { + self.stdout.queue(cursor::MoveTo(column, row))?; + if self.selected_sample == i && self.selected_column == j { + self.stdout.queue(PrintStyledContent(field.to_string().bold().reverse()))?; + } else { + self.stdout.queue(PrintStyledContent(field.to_string().bold()))?; + } + } + } + Ok(()) + } + + fn render_meters (&mut self) -> Result<(), Box> { + for (i, sample) in self.samples.iter().enumerate() { + let row = 4 + i as u16; + self.stdout + .queue(cursor::MoveTo(32, row))? + .queue(PrintStyledContent("▁".green()))?; + } + Ok(()) + } + +} + +pub struct Sample { + name: String, + rate: u32, + gain: f64, + channels: u8, + data: Vec>, +} + +impl Sample { + + pub fn new (name: &str) -> Result> { + Ok(Self { + name: name.into(), + rate: 44100, + channels: 1, + gain: 0.0, + data: vec![vec![]] + }) + } + +} + +struct Notifications; + +impl NotificationHandler for Notifications { + fn thread_init (&self, _: &Client) { + } + + fn shutdown (&mut self, status: ClientStatus, reason: &str) { + } + + fn freewheel (&mut self, _: &Client, is_enabled: bool) { + } + + fn sample_rate (&mut self, _: &Client, _: Frames) -> Control { + Control::Quit + } + + fn client_registration (&mut self, _: &Client, name: &str, is_reg: bool) { + } + + fn port_registration (&mut self, _: &Client, port_id: PortId, is_reg: bool) { + } + + fn port_rename (&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control { + Control::Continue + } + + fn ports_connected (&mut self, _: &Client, id_a: PortId, id_b: PortId, are: bool) { + } + + fn graph_reorder (&mut self, _: &Client) -> Control { + Control::Continue + } + + fn xrun (&mut self, _: &Client) -> Control { + Control::Continue + } +}