extern crate clap; extern crate jack; extern crate crossterm; pub mod engine; pub mod sequence; use clap::Parser; use std::error::Error; use std::sync::{Arc, Mutex}; use std::io::Write; use crossterm::QueueableCommand; use crossterm::style::Stylize; use crate::engine::jack::Jack; use crate::sequence::Time; use crate::sequence::midi::{PPQ, MIDISequence, MIDIEvent}; // bloop \ // --jack-client-name blooper // --jack-audio-out L=system/playback_1 \ // --jack-audio-out R=system/playback_2 \ // --jack-midi-in 1="nanoKEY Studio [20] (capture): nanoKEY Studio nanoKEY Studio _" fn main () -> Result<(), Box> { let cli = Cli::parse(); let app = App::init(&cli)?; let sleep = std::time::Duration::from_millis(16); let mut stdout = std::io::stdout(); loop { app.render(&mut stdout)?; std::thread::sleep(sleep); }; Ok(()) } struct App { jack: Arc>, time: u64, root: MIDISequence, stdout: std::io::Stdout, } impl App { fn init (options: &Cli) -> Result> { let mut root = MIDISequence::new("/", 4 * PPQ); root.add(00 * PPQ, MIDIEvent::NoteOn(36, 100)) .add(01 * PPQ, MIDIEvent::NoteOn(40, 100)) .add(02 * PPQ - PPQ / 2, MIDIEvent::NoteOn(36, 100)) .add(02 * PPQ + PPQ / 2, MIDIEvent::NoteOn(36, 100)) .add(03 * PPQ, MIDIEvent::NoteOn(40, 100)) .add(00 * PPQ / 4, MIDIEvent::NoteOn(44, 100)) .add(01 * PPQ / 4, MIDIEvent::NoteOn(44, 100)) .add(02 * PPQ / 4, MIDIEvent::NoteOn(44, 100)) .add(03 * PPQ / 4, MIDIEvent::NoteOn(44, 100)) .add(04 * PPQ / 4, MIDIEvent::NoteOn(44, 100)) .add(06 * PPQ / 4, MIDIEvent::NoteOn(44, 100)) .add(07 * PPQ / 4, MIDIEvent::NoteOn(44, 100)) .add(09 * PPQ / 4, MIDIEvent::NoteOn(44, 100)) .add(10 * PPQ / 4, MIDIEvent::NoteOn(44, 100)) .add(11 * PPQ / 4, MIDIEvent::NoteOn(44, 100)) .add(13 * PPQ / 4, MIDIEvent::NoteOn(44, 100)) .add(14 * PPQ / 4, MIDIEvent::NoteOn(44, 100)) .add(15 * PPQ / 4, MIDIEvent::NoteOn(44, 100)); Ok(Self {std::thread::sleep(std::time::Duration::from_millis(10)) jack: Jack::init_from_cli(&options)?, time: 0, root, stdout: std::io::stdout(), }) } fn render (&self, stdout: &mut std::io::Stdout) -> Result<(), Box> { use crossterm::*; let zoom = 8; let (cols, rows) = terminal::size()?; let transport = self.jack .lock() .expect("Failed to lock engine") .transport.as_ref() .expect("Failed to get transport") .query() .expect("Failed to query transport"); let state = transport.state; let frame = transport.pos.frame(); let rate = transport.pos.frame_rate(); let time_string = if let Some(rate) = rate { let second = (frame as f64) / (rate as f64); let seconds = second.floor(); let msec = ((second - seconds) * 1000f64) as u64; let seconds = second as u64 % 60; let minutes = second as u64 / 60; format!("{minutes}:{seconds:02}.{msec:03}") } else { "".to_string() }; let mut x_offset = cols / 2 - 10; let beat_string = if let Some(rate) = rate { let second = (frame as f64) / (rate as f64); let minute = second / 60f64; let bpm = 120f64; let div = 4; let beats = minute * bpm; let bars = beats as u64 / div as u64; let beat = beats as u64 % div as u64 + 1; x_offset -= ((beats as f64 % div as f64) * 8.0) as u16; format!("{bars} {beat}/{div}") } else { "".to_string() }; stdout .queue(terminal::Clear(terminal::ClearType::All))? .queue(cursor::Hide)? .queue(cursor::MoveTo(1, 0))? .queue(style::PrintStyledContent("bloop!".yellow()))? .queue(cursor::MoveTo(1, 1))? .queue(style::PrintStyledContent("v0.0.1".white()))? .queue(cursor::MoveTo(10, 0))? .queue(style::PrintStyledContent("rate".yellow()))? .queue(cursor::MoveTo(10, 1))? .queue(style::PrintStyledContent(format!("{}", rate.unwrap_or(0)).white()))? .queue(cursor::MoveTo(18, 0))? .queue(style::PrintStyledContent("state".yellow()))? .queue(cursor::MoveTo(18, 1))? .queue(style::PrintStyledContent(format!("{state:?}").white()))? .queue(cursor::MoveTo(28, 0))? .queue(style::PrintStyledContent("time".yellow()))? .queue(cursor::MoveTo(28, 1))? .queue(style::PrintStyledContent(format!("{time_string}").white()))? .queue(cursor::MoveTo(40, 0))? .queue(style::PrintStyledContent("beats".yellow()))? .queue(cursor::MoveTo(40, 1))? .queue(style::PrintStyledContent(format!("{beat_string}").white()))? .queue(cursor::MoveTo(x_offset, 3.max(rows / 2 - 2)))?; self.root.render(stdout, zoom)?; stdout .queue(cursor::MoveTo(0, rows - 2))? .queue(style::PrintStyledContent(" SPACE".green()))? .queue(cursor::MoveTo(0, rows - 1))? .queue(style::PrintStyledContent(" play ".yellow()))? .queue(cursor::MoveTo(8, rows - 2))? .queue(style::PrintStyledContent(" HOME".green()))? .queue(cursor::MoveTo(8, rows - 1))? .queue(style::PrintStyledContent(" sync ".yellow()))? //.queue(style::PrintStyledContent(" ↑↓←→".green()))? //.queue(style::PrintStyledContent(" navigate ".yellow()))? .queue(cursor::MoveTo(14, rows - 2))? .queue(style::PrintStyledContent(" `".green()))? .queue(cursor::MoveTo(14, rows - 1))? .queue(style::PrintStyledContent(" markers ".yellow()))? .queue(cursor::MoveTo(23, rows - 2))? .queue(style::PrintStyledContent(" ,".green()))? .queue(cursor::MoveTo(23, rows - 1))? .queue(style::PrintStyledContent(" routing ".yellow()))? .queue(cursor::MoveTo(32, rows - 2))? .queue(style::PrintStyledContent(" a".green()))? .queue(cursor::MoveTo(32, rows - 1))? .queue(style::PrintStyledContent(" add audio ".yellow()))? .queue(cursor::MoveTo(43, rows - 2))? .queue(style::PrintStyledContent(" m".green()))? .queue(cursor::MoveTo(43, rows - 1))? .queue(style::PrintStyledContent(" add MIDI ".yellow()))? .queue(cursor::MoveTo(54, rows - 2))? .queue(style::PrintStyledContent(" o".green()))? .queue(cursor::MoveTo(54, rows - 1))? .queue(style::PrintStyledContent(" add OSC ".yellow()))? .flush()?; Ok(()) } } #[derive(Debug, Parser)] #[command(version, about, long_about = None)] pub struct Cli { #[arg(long="jack-client-name", default_value="blooper")] jack_client_name: String, #[arg(long="jack-audio-in")] jack_audio_ins: Vec, #[arg(long="jack-audio-out")] jack_audio_outs: Vec, #[arg(long="jack-midi-in")] jack_midi_ins: Vec, #[arg(long="jack-midi-out")] jack_midi_outs: Vec, } enum Command { Play, Stop, Rec, Dub, Trim, Jump, }