#![allow(clippy::unit_arg)] #![feature(adt_const_params, associated_type_defaults, closure_lifetime_binder, if_let_guard, impl_trait_in_assoc_type, trait_alias, type_alias_impl_trait, type_changing_struct_update)] #[cfg(test)] mod tek_test; mod tek_struct; pub use self::tek_struct::*; mod tek_trait; pub use self::tek_trait::*; mod tek_type; pub use self::tek_type::*; mod tek_impls; pub(crate) use ConnectName::*; pub(crate) use ConnectScope::*; pub(crate) use ConnectStatus::*; pub(crate) use JackState::*; #[allow(unused)] pub(crate) use ::{ std::{ cmp::Ord, collections::BTreeMap, error::Error, ffi::OsString, fmt::{Write, Debug, Formatter}, fs::File, ops::{Add, Sub, Mul, Div, Rem}, path::{Path, PathBuf}, sync::{Arc, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}}, thread::{spawn, JoinHandle}, }, xdg::BaseDirectories, atomic_float::*, tengri::tui::ratatui::{ self, prelude::{Rect, Style, Stylize, Buffer, Modifier, buffer::Cell, Color::{self, *}}, widgets::{Widget, canvas::{Canvas, Line}}, }, tengri::tui::crossterm::{ self, event::{Event, KeyEvent, KeyCode::{self, *}}, }, }; pub extern crate tengri; pub(crate) use tengri::{*, input::*, output::*, tui::*}; pub extern crate jack; pub(crate) use ::jack::{*, contrib::{*, ClosureProcessHandler}}; pub extern crate midly; pub(crate) use ::midly::{Smf, TrackEventKind, MidiMessage, Error as MidiError, num::*, live::*}; pub extern crate atomic_float; pub(crate) use atomic_float::AtomicF64; #[cfg(feature = "sampler")] pub(crate) use symphonia::{ core::{ formats::Packet, codecs::{Decoder, CODEC_TYPE_NULL}, //errors::Error as SymphoniaError, io::MediaSourceStream, probe::Hint, audio::SampleBuffer, }, default::get_codecs, }; #[cfg(feature = "lv2_gui")] use ::winit::{ application::ApplicationHandler, event::WindowEvent, event_loop::{ActiveEventLoop, ControlFlow, EventLoop}, window::{Window, WindowId}, platform::x11::EventLoopBuilderExtX11 }; /// Command-line entrypoint. #[cfg(feature = "cli")] pub fn main () -> Usually<()> { use clap::Parser; Cli::parse().run() } /// Create a new application from a backend, project, config, and mode /// /// ``` /// let jack = tek::Jack::new(&"test_tek").expect("failed to connect to jack"); /// let proj = Default::default(); /// let conf = Default::default(); /// let tek = tek::tek(&jack, proj, conf, ""); /// ``` pub fn tek ( jack: &Jack<'static>, project: Arrangement, config: Config, mode: impl AsRef ) -> App { App { color: ItemTheme::random(), dialog: Dialog::welcome(), jack: jack.clone(), mode: config.get_mode(mode).expect("failed to find mode"), config, project, ..Default::default() } } fn tek_confirm (state: &mut App) -> Perhaps { Ok(match &state.dialog { Dialog::Menu(index, items) => { let callback = items.0[*index].1.clone(); callback(state)?; None }, _ => todo!(), }) } fn tek_inc (state: &mut App, axis: &ControlAxis) -> Perhaps { Ok(match (&state.dialog, axis) { (Dialog::None, _) => todo!(), (Dialog::Menu(_, _), ControlAxis::Y) => AppCommand::SetDialog { dialog: state.dialog.menu_next() } .execute(state)?, _ => todo!() }) } fn tek_dec (state: &mut App, axis: &ControlAxis) -> Perhaps { Ok(match (&state.dialog, axis) { (Dialog::None, _) => None, (Dialog::Menu(_, _), ControlAxis::Y) => AppCommand::SetDialog { dialog: state.dialog.menu_prev() } .execute(state)?, _ => todo!() }) } pub fn load_view (views: &Views, name: &impl AsRef, body: &impl Language) -> Usually<()> { views.write().unwrap().insert(name.as_ref().into(), body.src()?.unwrap_or_default().into()); Ok(()) } pub fn load_mode (modes: &Modes, name: &impl AsRef, body: &impl Language) -> Usually<()> { let mut mode = Mode::default(); body.each(|item|mode.add(item))?; modes.write().unwrap().insert(name.as_ref().into(), Arc::new(mode)); Ok(()) } pub fn load_bind (binds: &Binds, name: &impl AsRef, body: &impl Language) -> Usually<()> { let mut map = Bind::new(); body.each(|item|if item.expr().head() == Ok(Some("see")) { // TODO Ok(()) } else if let Ok(Some(_word)) = item.expr().head().word() { if let Some(key) = TuiEvent::from_dsl(item.expr()?.head()?)? { map.add(key, Binding { commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(), condition: None, description: None, source: None }); Ok(()) } else if Some(":char") == item.expr()?.head()? { // TODO return Ok(()) } else { return Err(format!("Config::load_bind: invalid key: {:?}", item.expr()?.head()?).into()) } } else { return Err(format!("Config::load_bind: unexpected: {item:?}").into()) })?; binds.write().unwrap().insert(name.as_ref().into(), map); Ok(()) } /// CLI banner. pub(crate) const HEADER: &'static str = r#" ~ █▀█▀█ █▀▀█ █ █ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~ █ █▀ █▀▀▄ ~ v0.4.0, 2026 winter (or is it) ~ ~ ▀ █▀▀█ ▀ ▀ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ "#; fn collect_commands ( app: &App, input: &TuiIn ) -> Usually> { let mut commands = vec![]; for id in app.mode.keys.iter() { if let Some(event_map) = app.config.binds.clone().read().unwrap().get(id.as_ref()) && let Some(bindings) = event_map.query(input.event()) { for binding in bindings { for command in binding.commands.iter() { if let Some(command) = app.namespace(command)? as Option { commands.push(command) } } } } } Ok(commands) } fn execute_commands ( app: &mut App, commands: Vec ) -> Usually)>> { let mut history = vec![]; for command in commands.into_iter() { let result = command.execute(app); match result { Err(err) => { history.push((command, None)); return Err(err) } Ok(undo) => { history.push((command, undo)); } }; } Ok(history) } pub fn tek_jack_process (app: &mut App, client: &Client, scope: &ProcessScope) -> Control { let t0 = app.perf.get_t0(); app.clock().update_from_scope(scope).unwrap(); let midi_in = app.project.midi_input_collect(scope); if let Some(editor) = &app.editor() { let mut pitch: Option = None; for port in midi_in.iter() { for event in port.iter() { if let (_, Ok(LiveEvent::Midi {message: MidiMessage::NoteOn {key, ..}, ..})) = event { pitch = Some(key.clone()); } } } if let Some(pitch) = pitch { editor.set_note_pos(pitch.as_int() as usize); } } let result = app.project.process_tracks(client, scope); app.perf.update_from_jack_scope(t0, scope); result } pub fn tek_jack_event (app: &mut App, event: JackEvent) { use JackEvent::*; match event { SampleRate(sr) => { app.clock().timebase.sr.set(sr as f64); }, PortRegistration(_id, true) => { //let port = app.jack().port_by_id(id); //println!("\rport add: {id} {port:?}"); //println!("\rport add: {id}"); }, PortRegistration(_id, false) => { /*println!("\rport del: {id}")*/ }, PortsConnected(_a, _b, true) => { /*println!("\rport conn: {a} {b}")*/ }, PortsConnected(_a, _b, false) => { /*println!("\rport disc: {a} {b}")*/ }, ClientRegistration(_id, true) => {}, ClientRegistration(_id, false) => {}, ThreadInit => {}, XRun => {}, GraphReorder => {}, _ => { panic!("{event:?}"); } } } pub fn tek_show_version () { println!("todo version"); } pub fn tek_print_config (config: &Config) { use ::ansi_term::Color::*; println!("{:?}", config.dirs); for (k, v) in config.views.read().unwrap().iter() { println!("{} {} {v}", Green.paint("VIEW"), Green.bold().paint(format!("{k:<16}"))); } for (k, v) in config.binds.read().unwrap().iter() { println!("{} {}", Green.paint("BIND"), Green.bold().paint(format!("{k:<16}"))); for (k, v) in v.0.iter() { print!("{} ", &Yellow.paint(match &k.0 { Event::Key(KeyEvent { modifiers, .. }) => format!("{:>16}", format!("{modifiers}")), _ => unimplemented!() })); print!("{}", &Yellow.bold().paint(match &k.0 { Event::Key(KeyEvent { code, .. }) => format!("{:<10}", format!("{code}")), _ => unimplemented!() })); for v in v.iter() { print!(" => {:?}", v.commands); print!(" {}", v.condition.as_ref().map(|x|format!("{x:?}")).unwrap_or_default()); println!(" {}", v.description.as_ref().map(|x|x.as_ref()).unwrap_or_default()); //println!(" {:?}", v.source); } } } for (k, v) in config.modes.read().unwrap().iter() { println!(); for v in v.name.iter() { print!("{}", Green.bold().paint(format!("{v} "))); } for v in v.info.iter() { print!("\n{}", Green.paint(format!("{v}"))); } print!("\n{} {}", Blue.paint("TOOL"), Green.bold().paint(format!("{k:<16}"))); print!("\n{}", Blue.paint("KEYS")); for v in v.keys.iter() { print!("{}", Green.paint(format!(" {v}"))); } println!(); for (k, v) in v.modes.read().unwrap().iter() { print!("{} {} {:?}", Blue.paint("MODE"), Green.bold().paint(format!("{k:<16}")), v.name); print!(" INFO={:?}", v.info); print!(" VIEW={:?}", v.view); println!(" KEYS={:?}", v.keys); } print!("{}", Blue.paint("VIEW")); for v in v.view.iter() { print!("{}", Green.paint(format!(" {v}"))); } println!(); } } pub fn tek_print_status (project: &Arrangement) { println!("Name: {:?}", &project.name); println!("JACK: {:?}", &project.jack); println!("Buffer: {:?}", &project.clock.chunk); println!("Sample rate: {:?}", &project.clock.timebase.sr); println!("MIDI PPQ: {:?}", &project.clock.timebase.ppq); println!("Tempo: {:?}", &project.clock.timebase.bpm); println!("Quantize: {:?}", &project.clock.quant); println!("Launch: {:?}", &project.clock.sync); println!("Playhead: {:?}us", &project.clock.playhead.usec); println!("Playhead: {:?}s", &project.clock.playhead.sample); println!("Playhead: {:?}p", &project.clock.playhead.pulse); println!("Started: {:?}", &project.clock.started); println!("Tracks:"); for (i, t) in project.tracks.iter().enumerate() { println!(" Track {i}: {} {} {:?} {:?}", t.name, t.width, &t.sequencer.play_clip, &t.sequencer.next_clip); } println!("Scenes:"); for (i, t) in project.scenes.iter().enumerate() { println!(" Scene {i}: {} {:?}", &t.name, &t.clips); } println!("MIDI Ins: {:?}", &project.midi_ins); println!("MIDI Outs: {:?}", &project.midi_outs); println!("Audio Ins: {:?}", &project.audio_ins); println!("Audio Outs: {:?}", &project.audio_outs); // TODO git integration // TODO dawvert integration } pub const DEFAULT_PPQ: f64 = 96.0; /// FIXME: remove this and use PPQ from timebase everywhere: pub const PPQ: usize = 96; /// (pulses, name), assuming 96 PPQ pub const NOTE_DURATIONS: [(usize, &str);26] = [ (1, "1/384"), (2, "1/192"), (3, "1/128"), (4, "1/96"), (6, "1/64"), (8, "1/48"), (12, "1/32"), (16, "1/24"), (24, "1/16"), (32, "1/12"), (48, "1/8"), (64, "1/6"), (96, "1/4"), (128, "1/3"), (192, "1/2"), (256, "2/3"), (384, "1/1"), (512, "4/3"), (576, "3/2"), (768, "2/1"), (1152, "3/1"), (1536, "4/1"), (2304, "6/1"), (3072, "8/1"), (3456, "9/1"), (6144, "16/1"), ]; pub const NOTE_NAMES: [&str; 128] = [ "C0", "C#0", "D0", "D#0", "E0", "F0", "F#0", "G0", "G#0", "A0", "A#0", "B0", "C1", "C#1", "D1", "D#1", "E1", "F1", "F#1", "G1", "G#1", "A1", "A#1", "B1", "C2", "C#2", "D2", "D#2", "E2", "F2", "F#2", "G2", "G#2", "A2", "A#2", "B2", "C3", "C#3", "D3", "D#3", "E3", "F3", "F#3", "G3", "G#3", "A3", "A#3", "B3", "C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4", "C5", "C#5", "D5", "D#5", "E5", "F5", "F#5", "G5", "G#5", "A5", "A#5", "B5", "C6", "C#6", "D6", "D#6", "E6", "F6", "F#6", "G6", "G#6", "A6", "A#6", "B6", "C7", "C#7", "D7", "D#7", "E7", "F7", "F#7", "G7", "G#7", "A7", "A#7", "B7", "C8", "C#8", "D8", "D#8", "E8", "F8", "F#8", "G8", "G#8", "A8", "A#8", "B8", "C9", "C#9", "D9", "D#9", "E9", "F9", "F#9", "G9", "G#9", "A9", "A#9", "B9", "C10", "C#10", "D10", "D#10", "E10", "F10", "F#10", "G10", ]; /// Return boxed iterator of MIDI events pub fn parse_midi_input <'a> (input: ::jack::MidiIter<'a>) -> Box, &'a [u8])> + 'a> { Box::new(input.map(|::jack::RawMidi { time, bytes }|( time as usize, LiveEvent::parse(bytes).unwrap(), bytes ))) } /// Add "all notes off" to the start of a buffer. pub fn all_notes_off (output: &mut [Vec>]) { let mut buf = vec![]; let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() }; let evt = LiveEvent::Midi { channel: 0.into(), message: msg }; evt.write(&mut buf).unwrap(); output[0].push(buf); } /// Update notes_in array pub fn update_keys (keys: &mut[bool;128], message: &MidiMessage) { match message { MidiMessage::NoteOn { key, .. } => { keys[key.as_int() as usize] = true; } MidiMessage::NoteOff { key, .. } => { keys[key.as_int() as usize] = false; }, _ => {} } } /// Returns the next shorter length pub fn note_duration_prev (pulses: usize) -> usize { for (length, _) in NOTE_DURATIONS.iter().rev() { if *length < pulses { return *length } } pulses } /// Returns the next longer length pub fn note_duration_next (pulses: usize) -> usize { for (length, _) in NOTE_DURATIONS.iter() { if *length > pulses { return *length } } pulses } pub fn note_duration_to_name (pulses: usize) -> &'static str { for (length, name) in NOTE_DURATIONS.iter() { if *length == pulses { return name } } "" } pub fn note_pitch_to_name (n: usize) -> &'static str { if n > 127 { panic!("to_note_name({n}): must be 0-127"); } NOTE_NAMES[n] } //macro_rules! impl_port { //($Name:ident : $Spec:ident -> $Pair:ident |$jack:ident, $name:ident|$port:expr) => { //#[derive(Debug)] pub struct $Name { ///// Handle to JACK client, for receiving reconnect events. //jack: Jack<'static>, ///// Port name //name: Arc, ///// Port handle. //port: Port<$Spec>, ///// List of ports to connect to. //conn: Vec //} //impl AsRef> for $Name { //fn as_ref (&self) -> &Port<$Spec> { &self.port } //} //impl $Name { //pub fn new ($jack: &Jack, name: impl AsRef, connect: &[PortConnect]) //-> Usually //{ //let $name = name.as_ref(); //let jack = $jack.clone(); //let port = $port?; //let name = $name.into(); //let conn = connect.to_vec(); //let port = Self { jack, port, name, conn }; //port.connect_to_matching()?; //Ok(port) //} //pub fn name (&self) -> &Arc { &self.name } //pub fn port (&self) -> &Port<$Spec> { &self.port } //pub fn port_mut (&mut self) -> &mut Port<$Spec> { &mut self.port } //pub fn into_port (self) -> Port<$Spec> { self.port } //pub fn close (self) -> Usually<()> { //let Self { jack, port, .. } = self; //Ok(jack.with_client(|client|client.unregister_port(port))?) //} //} //impl HasJack<'static> for $Name { //fn jack (&self) -> &'static Jack<'static> { &self.jack } //} //impl JackPort<'static> for $Name { //type Port = $Spec; //type Pair = $Pair; //fn port (&self) -> &Port<$Spec> { &self.port } //} //impl ConnectTo<'static, &str> for $Name { //fn connect_to (&self, to: &str) -> Usually { //self.with_client(|c|if let Some(ref port) = c.port_by_name(to.as_ref()) { //self.connect_to(port) //} else { //Ok(Missing) //}) //} //} //impl ConnectTo<'static, &Port> for $Name { //fn connect_to (&self, port: &Port) -> Usually { //self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(&self.port, port) { //Connected //} else if let Ok(_) = c.connect_ports(port, &self.port) { //Connected //} else { //Mismatch //})) //} //} //impl ConnectTo<'static, &Port<$Pair>> for $Name { //fn connect_to (&self, port: &Port<$Pair>) -> Usually { //self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(&self.port, port) { //Connected //} else if let Ok(_) = c.connect_ports(port, &self.port) { //Connected //} else { //Mismatch //})) //} //} //impl ConnectAuto<'static> for $Name { //fn connections (&self) -> &[PortConnect] { //&self.conn //} //} //}; //} pub fn swap_value ( target: &mut T, value: &T, returned: impl Fn(T)->U ) -> Perhaps { if *target == *value { Ok(None) } else { let mut value = value.clone(); std::mem::swap(target, &mut value); Ok(Some(returned(value))) } } pub fn toggle_bool ( target: &mut bool, value: &Option, returned: impl Fn(Option)->U ) -> Perhaps { let mut value = value.unwrap_or(!*target); if value == *target { Ok(None) } else { std::mem::swap(target, &mut value); Ok(Some(returned(Some(value)))) } } pub fn device_kinds () -> &'static [&'static str] { &[ #[cfg(feature = "sampler")] "Sampler", #[cfg(feature = "lv2")] "Plugin (LV2)", ] } impl>> HasDevices for T { fn devices (&self) -> &Vec { self.get() } fn devices_mut (&mut self) -> &mut Vec { self.get_mut() } } impl Device { pub fn name (&self) -> &str { match self { Self::Sampler(sampler) => sampler.name.as_ref(), _ => todo!(), } } pub fn midi_ins (&self) -> &[MidiInput] { match self { //Self::Sampler(Sampler { midi_in, .. }) => &[midi_in], _ => todo!() } } pub fn midi_outs (&self) -> &[MidiOutput] { match self { Self::Sampler(_) => &[], _ => todo!() } } pub fn audio_ins (&self) -> &[AudioInput] { match self { Self::Sampler(Sampler { audio_ins, .. }) => audio_ins.as_slice(), _ => todo!() } } pub fn audio_outs (&self) -> &[AudioOutput] { match self { Self::Sampler(Sampler { audio_outs, .. }) => audio_outs.as_slice(), _ => todo!() } } } audio!(|self: DeviceAudio<'a>, client, scope|{ use Device::*; match self.0 { Mute => { Control::Continue }, Bypass => { /*TODO*/ Control::Continue }, #[cfg(feature = "sampler")] Sampler(sampler) => sampler.process(client, scope), #[cfg(feature = "lv2")] Lv2(lv2) => lv2.process(client, scope), #[cfg(feature = "vst2")] Vst2 => { todo!() }, // TODO #[cfg(feature = "vst3")] Vst3 => { todo!() }, // TODO #[cfg(feature = "clap")] Clap => { todo!() }, // TODO #[cfg(feature = "sf2")] Sf2 => { todo!() }, // TODO } }); //take!(DeviceCommand|state: Arrangement, iter|state.selected_device().as_ref() //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); #[macro_export] macro_rules! has_clip { (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { impl $(<$($L),*$($T $(: $U)?),*>)? HasMidiClip for $Struct $(<$($L),*$($T),*>)? { fn clip (&$self) -> Option>> { $cb } } } } fn to_key (note: usize) -> &'static str { match note % 12 { 11 | 9 | 7 | 5 | 4 | 2 | 0 => "████▌", 10 | 8 | 6 | 3 | 1 => " ", _ => unreachable!(), } } pub(crate) fn note_y_iter (note_lo: usize, note_hi: usize, y0: u16) -> impl Iterator { (note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n)) } #[cfg(feature = "lv2_gui")] pub fn run_lv2_ui (mut ui: LV2PluginUI) -> Usually> { Ok(spawn(move||{ let event_loop = EventLoop::builder().with_x11().with_any_thread(true).build().unwrap(); event_loop.set_control_flow(ControlFlow::Wait); event_loop.run_app(&mut ui).unwrap() })) } #[cfg(feature = "lv2_gui")] fn lv2_ui_instantiate (kind: &str) { //let host = Suil } pub fn mix_summing ( buffer: &mut [Vec], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>, ) -> bool { let channels = buffer.len(); for index in 0..frames { if let Some(frame) = next() { for (channel, sample) in frame.iter().enumerate() { let channel = channel % channels; buffer[channel][index] += sample * gain; } } else { return false } } true } pub fn mix_average ( buffer: &mut [Vec], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>, ) -> bool { let channels = buffer.len(); for index in 0..frames { if let Some(frame) = next() { for (channel, sample) in frame.iter().enumerate() { let channel = channel % channels; let value = buffer[channel][index]; buffer[channel][index] = (value + sample * gain) / 2.0; } } else { return false } } true } pub fn to_log10 (samples: &[f32]) -> f32 { let total: f32 = samples.iter().map(|x|x.abs()).sum(); let count = samples.len() as f32; 10. * (total / count).log10() } pub fn to_rms (samples: &[f32]) -> f32 { let sum = samples.iter() .map(|s|*s) .reduce(|sum, sample|sum + sample.abs()) .unwrap_or(0.0); (sum / samples.len() as f32).sqrt() } fn read_sample_data (_: &str) -> Usually<(usize, Vec>)> { todo!(); } fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { let (mut subdirs, mut files) = std::fs::read_dir(dir)? .fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{ let entry = entry.expect("failed to read drectory entry"); let meta = entry.metadata().expect("failed to read entry metadata"); if meta.is_file() { files.push(entry.file_name()); } else if meta.is_dir() { subdirs.push(entry.file_name()); } (subdirs, files) }); subdirs.sort(); files.sort(); Ok((subdirs, files)) } pub(crate) fn track_width (_index: usize, track: &Track) -> u16 { track.width as u16 } #[cfg(feature = "vst2")] fn set_vst_plugin ( host: &Arc>>, _path: &str ) -> Usually { let mut loader = ::vst::host::PluginLoader::load( &std::path::Path::new("/nix/store/ij3sz7nqg5l7v2dygdvzy3w6cj62bd6r-helm-0.9.0/lib/lxvst/helm.so"), host.clone() )?; Ok(PluginKind::VST2 { instance: loader.instance()? }) } def_command!(AppCommand: |app: App| { Nop => Ok(None), Confirm => tek_confirm(app), Cancel => todo!(), // TODO delegate: Inc { axis: ControlAxis } => tek_inc(app, axis), Dec { axis: ControlAxis } => tek_dec(app, axis), SetDialog { dialog: Dialog } => { swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog }) }, }); def_command!(ClockCommand: |clock: Clock| { SeekUsec { usec: f64 } => { clock.playhead.update_from_usec(*usec); Ok(None) }, SeekSample { sample: f64 } => { clock.playhead.update_from_sample(*sample); Ok(None) }, SeekPulse { pulse: f64 } => { clock.playhead.update_from_pulse(*pulse); Ok(None) }, SetBpm { bpm: f64 } => Ok(Some( Self::SetBpm { bpm: clock.timebase().bpm.set(*bpm) })), SetQuant { quant: f64 } => Ok(Some( Self::SetQuant { quant: clock.quant.set(*quant) })), SetSync { sync: f64 } => Ok(Some( Self::SetSync { sync: clock.sync.set(*sync) })), Play { position: Option } => { clock.play_from(*position)?; Ok(None) /* TODO Some(Pause(previousPosition)) */ }, Pause { position: Option } => { clock.pause_at(*position)?; Ok(None) }, TogglePlayback { position: u32 } => Ok(if clock.is_rolling() { clock.pause_at(Some(*position))?; None } else { clock.play_from(Some(*position))?; None }), }); def_command!(DeviceCommand: |device: Device| {}); def_command!(ClipCommand: |clip: MidiClip| { SetColor { color: Option } => { //(SetColor [t: usize, s: usize, c: ItemTheme] //clip.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o))))); //("color" [a: usize, b: usize] Some(Self::SetColor(a.unwrap(), b.unwrap(), ItemTheme::random()))) todo!() }, SetLoop { looping: Option } => { //(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}")) //("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap()))) todo!() } }); def_command!(BrowseCommand: |browse: Browse| { SetVisible => Ok(None), SetPath { address: PathBuf } => Ok(None), SetSearch { filter: Arc } => Ok(None), SetCursor { cursor: usize } => Ok(None), }); def_command!(MidiEditCommand: |editor: MidiEditor| { Show { clip: Option>> } => { editor.set_clip(clip.as_ref()); editor.redraw(); Ok(None) }, DeleteNote => { editor.redraw(); todo!() }, AppendNote { advance: bool } => { editor.put_note(*advance); editor.redraw(); Ok(None) }, SetNotePos { pos: usize } => { editor.set_note_pos((*pos).min(127)); editor.redraw(); Ok(None) }, SetNoteLen { len: usize } => { editor.set_note_len(*len); editor.redraw(); Ok(None) }, SetNoteScroll { scroll: usize } => { editor.set_note_lo((*scroll).min(127)); editor.redraw(); Ok(None) }, SetTimePos { pos: usize } => { editor.set_time_pos(*pos); editor.redraw(); Ok(None) }, SetTimeScroll { scroll: usize } => { editor.set_time_start(*scroll); editor.redraw(); Ok(None) }, SetTimeZoom { zoom: usize } => { editor.set_time_zoom(*zoom); editor.redraw(); Ok(None) }, SetTimeLock { lock: bool } => { editor.set_time_lock(*lock); editor.redraw(); Ok(None) }, // TODO: 1-9 seek markers that by default start every 8th of the clip }); def_command!(PoolCommand: |pool: Pool| { // Toggle visibility of pool Show { visible: bool } => { pool.visible = *visible; Ok(Some(Self::Show { visible: !visible })) }, // Select a clip from the clip pool Select { index: usize } => { pool.set_clip_index(*index); Ok(None) }, // Update the contents of the clip pool Clip { command: PoolClipCommand } => Ok(command.execute(pool)?.map(|command|Self::Clip{command})), // Rename a clip Rename { command: RenameCommand } => Ok(command.delegate(pool, |command|Self::Rename{command})?), // Change the length of a clip Length { command: CropCommand } => Ok(command.delegate(pool, |command|Self::Length{command})?), // Import from file Import { command: BrowseCommand } => Ok(if let Some(browse) = pool.browse.as_mut() { command.delegate(browse, |command|Self::Import{command})? } else { None }), // Export to file Export { command: BrowseCommand } => Ok(if let Some(browse) = pool.browse.as_mut() { command.delegate(browse, |command|Self::Export{command})? } else { None }), }); def_command!(PoolClipCommand: |pool: Pool| { Delete { index: usize } => { let index = *index; let clip = pool.clips_mut().remove(index).read().unwrap().clone(); Ok(Some(Self::Add { index, clip })) }, Swap { index: usize, other: usize } => { let index = *index; let other = *other; pool.clips_mut().swap(index, other); Ok(Some(Self::Swap { index, other })) }, Export { index: usize, path: PathBuf } => { todo!("export clip to midi file"); }, Add { index: usize, clip: MidiClip } => { let index = *index; let mut index = index; let clip = Arc::new(RwLock::new(clip.clone())); let mut clips = pool.clips_mut(); if index >= clips.len() { index = clips.len(); clips.push(clip) } else { clips.insert(index, clip); } Ok(Some(Self::Delete { index })) }, Import { index: usize, path: PathBuf } => { let index = *index; let bytes = std::fs::read(&path)?; let smf = Smf::parse(bytes.as_slice())?; let mut t = 0u32; let mut events = vec![]; for track in smf.tracks.iter() { for event in track.iter() { t += event.delta.as_int(); if let TrackEventKind::Midi { channel, message } = event.kind { events.push((t, channel.as_int(), message)); } } } let mut clip = MidiClip::new("imported", true, t as usize + 1, None, None); for event in events.iter() { clip.notes[event.0 as usize].push(event.2); } Ok(Self::Add { index, clip }.execute(pool)?) }, SetName { index: usize, name: Arc } => { let index = *index; let clip = &mut pool.clips_mut()[index]; let old_name = clip.read().unwrap().name.clone(); clip.write().unwrap().name = name.clone(); Ok(Some(Self::SetName { index, name: old_name })) }, SetLength { index: usize, length: usize } => { let index = *index; let clip = &mut pool.clips_mut()[index]; let old_len = clip.read().unwrap().length; clip.write().unwrap().length = *length; Ok(Some(Self::SetLength { index, length: old_len })) }, SetColor { index: usize, color: ItemColor } => { let index = *index; let mut color = ItemTheme::from(*color); std::mem::swap(&mut color, &mut pool.clips()[index].write().unwrap().color); Ok(Some(Self::SetColor { index, color: color.base })) }, }); def_command!(RenameCommand: |pool: Pool| { Begin => unreachable!(), Cancel => { if let Some(PoolMode::Rename(clip, ref mut old_name)) = pool.mode_mut().clone() { pool.clips()[clip].write().unwrap().name = old_name.clone().into(); } Ok(None) }, Confirm => { if let Some(PoolMode::Rename(_clip, ref mut old_name)) = pool.mode_mut().clone() { let old_name = old_name.clone(); *pool.mode_mut() = None; return Ok(Some(Self::Set { value: old_name })) } Ok(None) }, Set { value: Arc } => { if let Some(PoolMode::Rename(clip, ref mut _old_name)) = pool.mode_mut().clone() { pool.clips()[clip].write().unwrap().name = value.clone(); } Ok(None) }, }); def_command!(CropCommand: |pool: Pool| { Begin => unreachable!(), Cancel => { if let Some(PoolMode::Length(..)) = pool.mode_mut().clone() { *pool.mode_mut() = None; } Ok(None) }, Set { length: usize } => { if let Some(PoolMode::Length(clip, ref mut length, ref mut _focus)) = pool.mode_mut().clone() { let old_length; { let clip = pool.clips()[clip].clone();//.write().unwrap(); old_length = Some(clip.read().unwrap().length); clip.write().unwrap().length = *length; } *pool.mode_mut() = None; return Ok(old_length.map(|length|Self::Set { length })) } Ok(None) }, Next => { if let Some(PoolMode::Length(_clip, ref mut _length, ref mut focus)) = pool.mode_mut().clone() { focus.next() }; Ok(None) }, Prev => { if let Some(PoolMode::Length(_clip, ref mut _length, ref mut focus)) = pool.mode_mut().clone() { focus.prev() }; Ok(None) }, Inc => { if let Some(PoolMode::Length(_clip, ref mut length, ref mut focus)) = pool.mode_mut().clone() { match focus { ClipLengthFocus::Bar => { *length += 4 * PPQ }, ClipLengthFocus::Beat => { *length += PPQ }, ClipLengthFocus::Tick => { *length += 1 }, } } Ok(None) }, Dec => { if let Some(PoolMode::Length(_clip, ref mut length, ref mut focus)) = pool.mode_mut().clone() { match focus { ClipLengthFocus::Bar => { *length = length.saturating_sub(4 * PPQ) }, ClipLengthFocus::Beat => { *length = length.saturating_sub(PPQ) }, ClipLengthFocus::Tick => { *length = length.saturating_sub(1) }, } } Ok(None) } }); def_command!(MidiInputCommand: |port: MidiInput| { Close => todo!(), Connect { midi_out: Arc } => todo!(), }); def_command!(MidiOutputCommand: |port: MidiOutput| { Close => todo!(), Connect { midi_in: Arc } => todo!(), }); def_command!(AudioInputCommand: |port: AudioInput| { Close => todo!(), Connect { audio_out: Arc } => todo!(), }); def_command!(AudioOutputCommand: |port: AudioOutput| { Close => todo!(), Connect { audio_in: Arc } => todo!(), }); def_command!(SamplerCommand: |sampler: Sampler| { RecordToggle { slot: usize } => { let slot = *slot; let recording = sampler.recording.as_ref().map(|x|x.0); let _ = Self::RecordFinish.execute(sampler)?; // autoslice: continue recording at next slot if recording != Some(slot) { Self::RecordBegin { slot }.execute(sampler) } else { Ok(None) } }, RecordBegin { slot: usize } => { let slot = *slot; sampler.recording = Some(( slot, Some(Arc::new(RwLock::new(Sample::new( "Sample", 0, 0, vec![vec![];sampler.audio_ins.len()] )))) )); Ok(None) }, RecordFinish => { let _prev_sample = sampler.recording.as_mut().map(|(index, sample)|{ std::mem::swap(sample, &mut sampler.samples.0[*index]); sample }); // TODO: undo Ok(None) }, RecordCancel => { sampler.recording = None; Ok(None) }, PlaySample { slot: usize } => { let slot = *slot; if let Some(ref sample) = sampler.samples.0[slot] { sampler.voices.write().unwrap().push(Sample::play(sample, 0, &u7::from(128))); } Ok(None) }, StopSample { slot: usize } => { let slot = *slot; todo!(); Ok(None) }, }); def_command!(FileBrowserCommand: |sampler: Sampler|{ //("begin" [] Some(Self::Begin)) //("cancel" [] Some(Self::Cancel)) //("confirm" [] Some(Self::Confirm)) //("select" [i: usize] Some(Self::Select(i.expect("no index")))) //("chdir" [p: PathBuf] Some(Self::Chdir(p.expect("no path")))) //("filter" [f: Arc] Some(Self::Filter(f.expect("no filter"))))) }); def_command!(SceneCommand: |scene: Scene| { SetSize { size: usize } => { todo!() }, SetZoom { size: usize } => { todo!() }, SetName { name: Arc } => swap_value(&mut scene.name, name, |name|Self::SetName{name}), SetColor { color: ItemTheme } => swap_value(&mut scene.color, color, |color|Self::SetColor{color}), }); def_command!(TrackCommand: |track: Track| { Stop => { track.sequencer.enqueue_next(None); Ok(None) }, SetMute { mute: Option } => todo!(), SetSolo { solo: Option } => todo!(), SetSize { size: usize } => todo!(), SetZoom { zoom: usize } => todo!(), SetName { name: Arc } => swap_value(&mut track.name, name, |name|Self::SetName { name }), SetColor { color: ItemTheme } => swap_value(&mut track.color, color, |color|Self::SetColor { color }), SetRec { rec: Option } => toggle_bool(&mut track.sequencer.recording, rec, |rec|Self::SetRec { rec }), SetMon { mon: Option } => toggle_bool(&mut track.sequencer.monitoring, mon, |mon|Self::SetMon { mon }), }); pub(crate) use self::size::*; mod size { use crate::*; /// Define a type alias for iterators of sized items (columns). macro_rules! def_sizes_iter { ($Type:ident => $($Item:ty),+) => { pub trait $Type<'a> = Iterator + Send + Sync + 'a; } } def_sizes_iter!(InputsSizes => MidiInput); def_sizes_iter!(OutputsSizes => MidiOutput); def_sizes_iter!(PortsSizes => Arc, [Connect]); def_sizes_iter!(ScenesSizes => Scene); def_sizes_iter!(TracksSizes => Track); } pub(crate) use self::view::*; mod view { use crate::*; pub fn view_logo () -> impl Content { Fixed::XY(32, 7, Tui::bold(true, Tui::fg(Rgb(240,200,180), col!{ Fixed::Y(1, ""), Fixed::Y(1, ""), Fixed::Y(1, "~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~"), Fixed::Y(1, Bsp::e("~~~~ ║ ~ ╟─╌ ~╟─< ~~ ", Bsp::e(Tui::fg(Rgb(230,100,40), "v0.3.0"), " ~~"))), Fixed::Y(1, "~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~"), }))) } pub fn view_transport ( play: bool, bpm: Arc>, beat: Arc>, time: Arc>, ) -> impl Content { let theme = ItemTheme::G[96]; Tui::bg(Black, row!(Bsp::a( Fill::XY(Align::w(button_play_pause(play))), Fill::XY(Align::e(row!( FieldH(theme, "BPM", bpm), FieldH(theme, "Beat", beat), FieldH(theme, "Time", time), ))) ))) } pub fn view_status ( sel: Option>, sr: Arc>, buf: Arc>, lat: Arc>, ) -> impl Content { let theme = ItemTheme::G[96]; Tui::bg(Black, row!(Bsp::a( Fill::XY(Align::w(sel.map(|sel|FieldH(theme, "Selected", sel)))), Fill::XY(Align::e(row!( FieldH(theme, "SR", sr), FieldH(theme, "Buf", buf), FieldH(theme, "Lat", lat), ))) ))) } pub fn button_play_pause (playing: bool) -> impl Content { let compact = true;//self.is_editing(); Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, Either::new(compact, Thunk::new(move|to: &mut TuiOut|to.place(&Fixed::X(9, Either::new(playing, Tui::fg(Rgb(0, 255, 0), " PLAYING "), Tui::fg(Rgb(255, 128, 0), " STOPPED "))) )), Thunk::new(move|to: &mut TuiOut|to.place(&Fixed::X(5, Either::new(playing, Tui::fg(Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), Tui::fg(Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",)))) )) ) ) } #[cfg(feature = "track")] pub fn view_track_row_section ( _theme: ItemTheme, button: impl Content, button_add: impl Content, content: impl Content, ) -> impl Content { Bsp::w(Fill::Y(Fixed::X(4, Align::nw(button_add))), Bsp::e(Fixed::X(20, Fill::Y(Align::nw(button))), Fill::XY(Align::c(content)))) } pub fn wrap (bg: Color, fg: Color, content: impl Content) -> impl Content { let left = Tui::fg_bg(bg, Reset, Fixed::X(1, Repeat::Y("▐"))); let right = Tui::fg_bg(bg, Reset, Fixed::X(1, Repeat::Y("▌"))); Bsp::e(left, Bsp::w(right, Tui::fg_bg(fg, bg, content))) } pub fn view_meter <'a> (label: &'a str, value: f32) -> impl Content + 'a { col!( FieldH(ItemTheme::G[128], label, format!("{:>+9.3}", value)), Fixed::XY(if value >= 0.0 { 13 } else if value >= -1.0 { 12 } else if value >= -2.0 { 11 } else if value >= -3.0 { 10 } else if value >= -4.0 { 9 } else if value >= -6.0 { 8 } else if value >= -9.0 { 7 } else if value >= -12.0 { 6 } else if value >= -15.0 { 5 } else if value >= -20.0 { 4 } else if value >= -25.0 { 3 } else if value >= -30.0 { 2 } else if value >= -40.0 { 1 } else { 0 }, 1, Tui::bg(if value >= 0.0 { Red } else if value >= -3.0 { Yellow } else { Green }, ()))) } pub fn view_meters (values: &[f32;2]) -> impl Content + use<'_> { let left = format!("L/{:>+9.3}", values[0]); let right = format!("R/{:>+9.3}", values[1]); Bsp::s(left, right) } pub fn draw_info (sample: Option<&Arc>>) -> impl Content + use<'_> { When::new(sample.is_some(), Thunk::new(move|to: &mut TuiOut|{ let sample = sample.unwrap().read().unwrap(); let theme = sample.color; to.place(&row!( FieldH(theme, "Name", format!("{:<10}", sample.name.clone())), FieldH(theme, "Length", format!("{:<8}", sample.channels[0].len())), FieldH(theme, "Start", format!("{:<8}", sample.start)), FieldH(theme, "End", format!("{:<8}", sample.end)), FieldH(theme, "Trans", "0"), FieldH(theme, "Gain", format!("{}", sample.gain)), )) })) } pub fn draw_info_v (sample: Option<&Arc>>) -> impl Content + use<'_> { Either::new(sample.is_some(), Thunk::new(move|to: &mut TuiOut|{ let sample = sample.unwrap().read().unwrap(); let theme = sample.color; to.place(&Fixed::X(20, col!( Fill::X(Align::w(FieldH(theme, "Name ", format!("{:<10}", sample.name.clone())))), Fill::X(Align::w(FieldH(theme, "Length", format!("{:<8}", sample.channels[0].len())))), Fill::X(Align::w(FieldH(theme, "Start ", format!("{:<8}", sample.start)))), Fill::X(Align::w(FieldH(theme, "End ", format!("{:<8}", sample.end)))), Fill::X(Align::w(FieldH(theme, "Trans ", "0"))), Fill::X(Align::w(FieldH(theme, "Gain ", format!("{}", sample.gain)))), ))) }), Thunk::new(|to: &mut TuiOut|to.place(&Tui::fg(Red, col!( Tui::bold(true, "× No sample."), "[r] record", "[Shift-F9] import", ))))) } pub fn draw_status (sample: Option<&Arc>>) -> impl Content { Tui::bold(true, Tui::fg(Tui::g(224), sample .map(|sample|{ let sample = sample.read().unwrap(); format!("Sample {}-{}", sample.start, sample.end) }) .unwrap_or_else(||"No sample".to_string()))) } pub fn view_track_header (theme: ItemTheme, content: impl Content) -> impl Content { Fixed::X(12, Tui::bg(theme.darker.rgb, Fill::X(Align::e(content)))) } pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, ports: &'a [T]) -> impl Content + use<'a, T> { let ins = ports.len() as u16; let frame = Outer(true, Style::default().fg(Tui::g(96))); let iter = move||ports.iter(); let names = Map::south(1, iter, move|port, index|Fill::Y(Align::w(format!(" {index} {}", port.port_name())))); let field = FieldV(theme, title, names); Fixed::XY(20, 1 + ins, frame.enclose(Fixed::XY(20, 1 + ins, field))) } pub fn per_track_top <'a, T: Content + 'a, U: TracksSizes<'a>> ( tracks: impl Fn() -> U + Send + Sync + 'a, callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a ) -> impl Content + 'a { Align::x(Tui::bg(Reset, Map::new(tracks, move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ let width = (x2 - x1) as u16; map_east(x1 as u16, width, Fixed::X(width, Tui::fg_bg( track.color.lightest.rgb, track.color.base.rgb, callback(index, track))))}))) } pub fn per_track <'a, T: Content + 'a, U: TracksSizes<'a>> ( tracks: impl Fn() -> U + Send + Sync + 'a, callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a ) -> impl Content + 'a { per_track_top(tracks, move|index, track|Fill::Y(Align::y(callback(index, track)))) } pub fn io_ports <'a, T: PortsSizes<'a>> ( fg: Color, bg: Color, iter: impl Fn()->T + Send + Sync + 'a ) -> impl Content + 'a { Map::new(iter, move|( _index, name, connections, y, y2 ): (usize, &'a Arc, &'a [Connect], usize, usize), _| map_south(y as u16, (y2-y) as u16, Bsp::s( Fill::Y(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(Bsp::e(&" 󰣲 ", name))))), Map::new(||connections.iter(), move|connect: &'a Connect, index|map_south(index as u16, 1, Fill::Y(Align::w(Tui::bold(false, Tui::fg_bg(fg, bg, &connect.info))))))))) } pub fn draw_header (state: &Lv2, to: &mut TuiOut, x: u16, y: u16, w: u16) { let style = Style::default().gray(); let label1 = format!(" {}", state.name); to.blit(&label1, x + 1, y, Some(style.white().bold())); if let Some(ref path) = state.path { let label2 = format!("{}…", &path[..((w as usize - 10).min(path.len()))]); to.blit(&label2, x + 2 + label1.len() as u16, y, Some(style.not_dim())); } //Ok(Rect { x, y, width: w, height: 1 }) } pub fn draw_sample ( to: &mut TuiOut, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool ) -> Usually { let style = if focus { Style::default().green() } else { Style::default() }; if focus { to.blit(&"🬴", x+1, y, Some(style.bold())); } let label1 = format!("{:3} {:12}", note.map(|n|n.to_string()).unwrap_or(String::default()), sample.name); let label2 = format!("{:>6} {:>6} +0.0", sample.start, sample.end); to.blit(&label1, x+2, y, Some(style.bold())); to.blit(&label2, x+3+label1.len()as u16, y, Some(style)); Ok(label1.len() + label2.len() + 4) } }