use crate::*; use std::fmt::Write; use std::path::PathBuf; use std::ffi::OsString; audio!(App: tek_jack_process, tek_jack_event); audio!(Lv2: lv2_jack_process); audio!(Sampler: sampler_jack_process); /// Command-line configuration. #[cfg(feature = "cli")] impl Cli { pub fn run (&self) -> Usually<()> { if let Action::Version = self.action { return Ok(tek_show_version()) } let mut config = Config::new(None); config.init()?; if let Action::Config = self.action { tek_print_config(&config); } else if let Action::List = self.action { todo!("list sessions") } else if let Action::Resume = self.action { todo!("resume session") } else if let Action::New { name, bpm, tracks, scenes, sync_lead, sync_follow, midi_from, midi_from_re, midi_to, midi_to_re, left_from, right_from, left_to, right_to, .. } = &self.action { // Connect to JACK let name = name.as_ref().map_or("tek", |x|x.as_str()); let jack = Jack::new(&name)?; // Collect MIDI IO: let midi_ins = Connect::collect(&midi_from, &[] as &[&str], &midi_from_re).iter().enumerate() .map(|(index, connect)|jack.midi_in(&format!("M/{index}"), &[connect.clone()])) .collect::, _>>()?; let midi_outs = Connect::collect(&midi_to, &[] as &[&str], &midi_to_re).iter().enumerate() .map(|(index, connect)|jack.midi_out(&format!("{index}/M"), &[connect.clone()])) .collect::, _>>()?; // TODO: Collect audio IO: let empty = &[] as &[&str]; let left_froms = Connect::collect(&left_from, empty, empty); let left_tos = Connect::collect(&left_to, empty, empty); let right_froms = Connect::collect(&right_from, empty, empty); let right_tos = Connect::collect(&right_to, empty, empty); let _audio_froms = &[left_froms.as_slice(), right_froms.as_slice()]; let _audio_tos = &[left_tos.as_slice(), right_tos.as_slice()]; // Create initial project: let clock = Clock::new(&jack, *bpm)?; let mut project = Arrangement::new(&jack, None, clock, vec![], vec![], midi_ins, midi_outs); project.tracks_add(tracks.unwrap_or(0), None, &[], &[])?; project.scenes_add(scenes.unwrap_or(0))?; if matches!(self.action, Action::Status) { // Show status and exit tek_print_status(&project); return Ok(()) } // Initialize the app state let app = tek(&jack, project, config, ":menu"); if matches!(self.action, Action::Headless) { // TODO: Headless mode (daemon + client over IPC, then over network...) println!("todo headless"); } else { // Run the [Tui] and [Jack] threads with the [App] state. Tui::run(true, jack.run(move|jack|{ jack.sync_lead(*sync_lead, |mut state|{ let clock = app.clock(); clock.playhead.update_from_sample(state.position.frame() as f64); state.position.bbt = Some(clock.bbt()); state.position })?; jack.sync_follow(*sync_follow)?; Ok(app) })?)?; } } Ok(()) } } impl HasJack<'static> for App { fn jack (&self) -> &Jack<'static> { &self.jack } } impl HasClipsSize for App { fn clips_size (&self) -> &Measure { &self.project.size_inner } } impl HasTrackScroll for App { fn track_scroll (&self) -> usize { self.project.track_scroll() } } impl HasSceneScroll for App { fn scene_scroll (&self) -> usize { self.project.scene_scroll() } } impl Default for AppCommand { fn default () -> Self { Self::Nop } } impl Default for Binding { fn default () -> Self { Self { ..Default::default() } } } impl ScenesView for App { fn w_side (&self) -> u16 { 20 } fn w_mid (&self) -> u16 { (self.measure_width() as u16).saturating_sub(self.w_side()) } fn h_scenes (&self) -> u16 { (self.measure_height() as u16).saturating_sub(20) } } impl Default for MenuItem { fn default () -> Self { Self("".into(), Arc::new(Box::new(|_|Ok(())))) } } impl PartialEq for MenuItem { fn eq (&self, other: &Self) -> bool { self.0 == other.0 } } impl AsRef> for MenuItems { fn as_ref (&self) -> &Arc<[MenuItem]> { &self.0 } } /// Default is always empty map regardless if `E` and `C` implement [Default]. impl Default for Bind { fn default () -> Self { Self(Default::default()) } } has!(Jack<'static>: |self: App|self.jack); has!(Pool: |self: App|self.pool); has!(Dialog: |self: App|self.dialog); has!(Clock: |self: App|self.project.clock); has!(Option: |self: App|self.project.editor); has!(Selection: |self: App|self.project.selection); has!(Vec: |self: App|self.project.midi_ins); has!(Vec: |self: App|self.project.midi_outs); has!(Vec: |self: App|self.project.scenes); has!(Vec: |self: App|self.project.tracks); has!(Measure: |self: App|self.size); has_clips!( |self: App|self.pool.clips); maybe_has!(Track: |self: App| { MaybeHas::::get(&self.project) }; { MaybeHas::::get_mut(&mut self.project) }); maybe_has!(Scene: |self: App| { MaybeHas::::get(&self.project) }; { MaybeHas::::get_mut(&mut self.project) }); impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) }); impl_debug!(Condition |self, w| { write!(w, "*") }); macro_rules!primitive(($T:ty: $name:ident)=>{ fn $name (src: impl Language) -> Perhaps<$T> { Ok(if let Some(src) = src.src()? { Some(to_number(src)? as $T) } else { None }) } }); primitive!(u8: try_to_u8); primitive!(u16: try_to_u16); primitive!(usize: try_to_usize); primitive!(isize: try_to_isize); namespace!(App: Arc { literal = |dsl|Ok(dsl.src()?.map(|x|x.into())); }); namespace!(App: u8 { literal = |dsl|try_to_u8(dsl); }); namespace!(App: u16 { literal = |dsl|try_to_u16(dsl); symbol = |app| { ":w/sidebar" => app.project.w_sidebar(app.editor().is_some()), ":h/sample-detail" => 6.max(app.measure_height() as u16 * 3 / 9), }; }); namespace!(App: isize { literal = |dsl|try_to_isize(dsl); }); namespace!(App: usize { literal = |dsl|try_to_usize(dsl); symbol = |app| { ":scene-count" => app.scenes().len(), ":track-count" => app.tracks().len(), ":device-kind" => app.dialog.device_kind().unwrap_or(0), ":device-kind/next" => app.dialog.device_kind_next().unwrap_or(0), ":device-kind/prev" => app.dialog.device_kind_prev().unwrap_or(0), }; }); // Provide boolean values. namespace!(App: bool { symbol = |app| { ":mode/editor" => app.project.editor.is_some(), ":focused/dialog" => !matches!(app.dialog, Dialog::None), ":focused/message" => matches!(app.dialog, Dialog::Message(..)), ":focused/add_device" => matches!(app.dialog, Dialog::Device(..)), ":focused/browser" => app.dialog.browser().is_some(), ":focused/pool/import" => matches!(app.pool.mode, Some(PoolMode::Import(..))), ":focused/pool/export" => matches!(app.pool.mode, Some(PoolMode::Export(..))), ":focused/pool/rename" => matches!(app.pool.mode, Some(PoolMode::Rename(..))), ":focused/pool/length" => matches!(app.pool.mode, Some(PoolMode::Length(..))), ":focused/clip" => !app.editor_focused() && matches!(app.selection(), Selection::TrackClip{..}), ":focused/track" => !app.editor_focused() && matches!(app.selection(), Selection::Track(..)), ":focused/scene" => !app.editor_focused() && matches!(app.selection(), Selection::Scene(..)), ":focused/mix" => !app.editor_focused() && matches!(app.selection(), Selection::Mix), }; }); // TODO: provide colors here namespace!(App: ItemTheme {}); namespace!(App: Dialog { symbol = |app| { ":dialog/none" => Dialog::None, ":dialog/options" => Dialog::Options, ":dialog/device" => Dialog::Device(0), ":dialog/device/prev" => Dialog::Device(0), ":dialog/device/next" => Dialog::Device(0), ":dialog/help" => Dialog::Help(0), ":dialog/save" => Dialog::Browse(BrowseTarget::SaveProject, Browse::new(None).unwrap().into()), ":dialog/load" => Dialog::Browse(BrowseTarget::LoadProject, Browse::new(None).unwrap().into()), ":dialog/import/clip" => Dialog::Browse(BrowseTarget::ImportClip(Default::default()), Browse::new(None).unwrap().into()), ":dialog/export/clip" => Dialog::Browse(BrowseTarget::ExportClip(Default::default()), Browse::new(None).unwrap().into()), ":dialog/import/sample" => Dialog::Browse(BrowseTarget::ImportSample(Default::default()), Browse::new(None).unwrap().into()), ":dialog/export/sample" => Dialog::Browse(BrowseTarget::ExportSample(Default::default()), Browse::new(None).unwrap().into()), }; }); namespace!(App: Selection { symbol = |app| { ":select/scene" => app.selection().select_scene(app.tracks().len()), ":select/scene/next" => app.selection().select_scene_next(app.scenes().len()), ":select/scene/prev" => app.selection().select_scene_prev(), ":select/track" => app.selection().select_track(app.tracks().len()), ":select/track/next" => app.selection().select_track_next(app.tracks().len()), ":select/track/prev" => app.selection().select_track_prev(), }; }); namespace!(App: Color { symbol = |app| { ":color/bg" => Color::Rgb(28, 32, 36), }; expression = |app| { "g" (n: u8) => Color::Rgb(n, n, n), "rgb" (r: u8, g: u8, b: u8) => Color::Rgb(r, g, b), }; }); namespace!(App: Option { symbol = |app| { ":editor/pitch" => Some((app.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into()) }; }); namespace!(App: Option { symbol = |app| { ":selected/scene" => app.selection().scene(), ":selected/track" => app.selection().track(), }; }); namespace!(App: Option>> { symbol = |app| { ":selected/clip" => if let Selection::TrackClip { track, scene } = app.selection() { app.scenes()[*scene].clips[*track].clone() } else { None } }; }); handle!(TuiIn: |self: App, input|{ let commands = collect_commands(self, input)?; let history = execute_commands(self, commands)?; self.history.extend(history.into_iter()); Ok(None) }); impl Config { const CONFIG_DIR: &'static str = "tek"; const CONFIG_SUB: &'static str = "v0"; const CONFIG: &'static str = "tek.edn"; const DEFAULTS: &'static str = include_str!("./tek.edn"); /// Create a new app configuration from a set of XDG base directories, pub fn new (dirs: Option) -> Self { let default = ||BaseDirectories::with_profile(Self::CONFIG_DIR, Self::CONFIG_SUB); let dirs = dirs.unwrap_or_else(default); Self { dirs, ..Default::default() } } /// Write initial contents of configuration. pub fn init (&mut self) -> Usually<()> { self.init_one(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|{ cfgs.add(&dsl)?; Ok(()) })?; Ok(()) } /// Write initial contents of a configuration file. pub fn init_one ( &mut self, path: &str, defaults: &str, mut each: impl FnMut(&mut Self, &str)->Usually<()> ) -> Usually<()> { if self.dirs.find_config_file(path).is_none() { //println!("Creating {path:?}"); std::fs::write(self.dirs.place_config_file(path)?, defaults)?; } Ok(if let Some(path) = self.dirs.find_config_file(path) { //println!("Loading {path:?}"); let src = std::fs::read_to_string(&path)?; src.as_str().each(move|item|each(self, item))?; } else { return Err(format!("{path}: not found").into()) }) } /// Add statements to configuration from [Dsl] source. pub fn add (&mut self, dsl: impl Language) -> Usually<&mut Self> { dsl.each(|item|self.add_one(item))?; Ok(self) } fn add_one (&self, item: impl Language) -> Usually<()> { if let Some(expr) = item.expr()? { let head = expr.head()?; let tail = expr.tail()?; let name = tail.head()?; let body = tail.tail()?; //println!("Config::load: {} {} {}", head.unwrap_or_default(), name.unwrap_or_default(), body.unwrap_or_default()); match head { Some("mode") if let Some(name) = name => load_mode(&self.modes, &name, &body)?, Some("keys") if let Some(name) = name => load_bind(&self.binds, &name, &body)?, Some("view") if let Some(name) = name => load_view(&self.views, &name, &body)?, _ => return Err(format!("Config::load: expected view/keys/mode, got: {item:?}").into()) } Ok(()) } else { return Err(format!("Config::load: expected expr, got: {item:?}").into()) } } pub fn get_mode (&self, mode: impl AsRef) -> Option>>> { self.modes.clone().read().unwrap().get(mode.as_ref()).cloned() } } // Each mode contains a view, so here we should be drawing it. // I'm not sure what's going on with this code, though. impl Draw for Mode { fn draw (&self, _to: &mut TuiOut) { //self.content().draw(to) } } impl Mode> { /// Add a definition to the mode. /// /// Supported definitions: /// /// - (name ...) -> name /// - (info ...) -> description /// - (keys ...) -> key bindings /// - (mode ...) -> submode /// - ... -> view /// /// ``` /// let mut mode: tek::Mode> = Default::default(); /// mode.add("(name hello)").unwrap(); /// ``` pub fn add (&mut self, dsl: impl Language) -> Usually<()> { Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() { //println!("Mode::add: {head} {:?}", expr.tail()); let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); match head { "name" => self.add_name(tail)?, "info" => self.add_info(tail)?, "keys" => self.add_keys(tail)?, "mode" => self.add_mode(tail)?, _ => self.add_view(tail)?, }; } else if let Ok(Some(word)) = dsl.word() { self.add_view(word); } else { return Err(format!("Mode::add: unexpected: {dsl:?}").into()); }) //DslParse(dsl, ||Err(format!("Mode::add: unexpected: {dsl:?}").into())) //.word(|word|self.add_view(word)) //.expr(|expr|expr.head(|head|{ ////println!("Mode::add: {head} {:?}", expr.tail()); //let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); //match head { //"name" => self.add_name(tail), //"info" => self.add_info(tail), //"keys" => self.add_keys(tail)?, //"mode" => self.add_mode(tail)?, //_ => self.add_view(tail), //}; //})) } fn add_name (&mut self, dsl: impl Language) -> Perhaps<()> { Ok(dsl.src()?.map(|src|self.name.push(src.into()))) } fn add_info (&mut self, dsl: impl Language) -> Perhaps<()> { Ok(dsl.src()?.map(|src|self.info.push(src.into()))) } fn add_view (&mut self, dsl: impl Language) -> Perhaps<()> { Ok(dsl.src()?.map(|src|self.view.push(src.into()))) } fn add_keys (&mut self, dsl: impl Language) -> Perhaps<()> { Ok(Some(dsl.each(|expr|{ self.keys.push(expr.trim().into()); Ok(()) })?)) } fn add_mode (&mut self, dsl: impl Language) -> Perhaps<()> { Ok(Some(if let Some(id) = dsl.head()? { load_mode(&self.modes, &id, &dsl.tail())?; } else { return Err(format!("Mode::add: self: incomplete: {dsl:?}").into()); })) } } impl Bind { /// Create a new event map pub fn new () -> Self { Default::default() } /// Add a binding to an owned event map. pub fn def (mut self, event: E, binding: Binding) -> Self { self.add(event, binding); self } /// Add a binding to an event map. pub fn add (&mut self, event: E, binding: Binding) -> &mut Self { if !self.0.contains_key(&event) { self.0.insert(event.clone(), Default::default()); } self.0.get_mut(&event).unwrap().push(binding); self } /// Return the binding(s) that correspond to an event. pub fn query (&self, event: &E) -> Option<&[Binding]> { self.0.get(event).map(|x|x.as_slice()) } /// Return the first binding that corresponds to an event, considering conditions. pub fn dispatch (&self, event: &E) -> Option<&Binding> { self.query(event) .map(|bb|bb.iter().filter(|b|b.condition.as_ref().map(|c|(c.0)()).unwrap_or(true)).next()) .flatten() } } impl Binding { /// FIXME: Load an individual command binding from a dizzle. /// /// ``` /// let binding = tek::Binding::<()>::from_dsl("foo bar").unwrap(); /// ``` pub fn from_dsl (dsl: impl Language) -> Usually { let command: Option = None; let condition: Option = None; let description: Option> = None; let source: Option> = None; if let Some(command) = command { Ok(Self { commands: [command].into(), condition, description, source }) } else { Err(format!("no command in {dsl:?}").into()) } } } impl App { pub fn update_clock (&self) { ClockView::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80) } /// Set modal dialog. pub fn set_dialog (&mut self, mut dialog: Dialog) -> Dialog { std::mem::swap(&mut self.dialog, &mut dialog); dialog } /// Set picked device in device pick dialog. pub fn device_pick (&mut self, index: usize) { self.dialog = Dialog::Device(index); } pub fn add_device (&mut self, index: usize) -> Usually<()> { match index { 0 => { let name = self.jack.with_client(|c|c.name().to_string()); let midi = self.project.track().expect("no active track").sequencer.midi_outs[0].port_name(); let track = self.track().expect("no active track"); let port = format!("{}/Sampler", &track.name); let connect = Connect::exact(format!("{name}:{midi}")); let sampler = if let Ok(sampler) = Sampler::new( &self.jack, &port, &[connect], &[&[], &[]], &[&[], &[]] ) { self.dialog = Dialog::None; Device::Sampler(sampler) } else { self.dialog = Dialog::Message("Failed to add device.".into()); return Err("failed to add device".into()) }; let track = self.track_mut().expect("no active track"); track.devices.push(sampler); Ok(()) }, 1 => { todo!(); Ok(()) }, _ => unreachable!(), } } /// Return reference to content browser if open. pub fn browser (&self) -> Option<&Browse> { if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None } } /// Is a MIDI editor currently focused? pub fn editor_focused (&self) -> bool { false } /// Toggle MIDI editor. pub fn toggle_editor (&mut self, value: Option) { //FIXME: self.editing.store(value.unwrap_or_else(||!self.is_editing()), Relaxed); let value = value.unwrap_or_else(||!self.editor().is_some()); if value { // Create new clip in pool when entering empty cell if let Selection::TrackClip { track, scene } = *self.selection() && let Some(scene) = self.project.scenes.get_mut(scene) && let Some(slot) = scene.clips.get_mut(track) && slot.is_none() && let Some(track) = self.project.tracks.get_mut(track) { let (_index, mut clip) = self.pool.add_new_clip(); // autocolor: new clip colors from scene and track color let color = track.color.base.mix(scene.color.base, 0.5); clip.write().unwrap().color = ItemColor::random_near(color, 0.2).into(); if let Some(editor) = &mut self.project.editor { editor.set_clip(Some(&clip)); } *slot = Some(clip.clone()); //Some(clip) } else { //None } } else if let Selection::TrackClip { track, scene } = *self.selection() && let Some(scene) = self.project.scenes.get_mut(scene) && let Some(slot) = scene.clips.get_mut(track) && let Some(clip) = slot.as_mut() { // Remove clip from arrangement when exiting empty clip editor let mut swapped = None; if clip.read().unwrap().count_midi_messages() == 0 { std::mem::swap(&mut swapped, slot); } if let Some(clip) = swapped { self.pool.delete_clip(&clip.read().unwrap()); } } } } impl Dialog { pub fn welcome () -> Self { Self::Menu(1, MenuItems([ MenuItem("Resume session".into(), Arc::new(Box::new(|_|Ok(())))), MenuItem("Create new session".into(), Arc::new(Box::new(|app|Ok({ app.dialog = Dialog::None; app.mode = app.config.modes.clone().read().unwrap().get(":arranger").cloned().unwrap(); })))), MenuItem("Load old session".into(), Arc::new(Box::new(|_|Ok(())))), ].into())) } pub fn menu_next (&self) -> Self { match self { Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.0.len()), items.clone()), _ => Self::None } } pub fn menu_prev (&self) -> Self { match self { Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.0.len()), items.clone()), _ => Self::None } } pub fn menu_selected (&self) -> Option { if let Self::Menu(selected, _) = self { Some(*selected) } else { None } } pub fn device_kind (&self) -> Option { if let Self::Device(index) = self { Some(*index) } else { None } } pub fn device_kind_next (&self) -> Option { self.device_kind().map(|index|(index + 1) % device_kinds().len()) } pub fn device_kind_prev (&self) -> Option { self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1))) } pub fn message (&self) -> Option<&str> { todo!() } pub fn browser (&self) -> Option<&Arc> { todo!() } pub fn browser_target (&self) -> Option<&BrowseTarget> { todo!() } } impl<'a> Namespace<'a, AppCommand> for App { symbols!('a |app| -> AppCommand { "x/inc" => AppCommand::Inc { axis: ControlAxis::X }, "x/dec" => AppCommand::Dec { axis: ControlAxis::X }, "y/inc" => AppCommand::Inc { axis: ControlAxis::Y }, "y/dec" => AppCommand::Dec { axis: ControlAxis::Y }, "confirm" => AppCommand::Confirm, "cancel" => AppCommand::Cancel, }); } impl Draw for App { fn draw (&self, to: &mut TuiOut) { if let Some(e) = self.error.read().unwrap().as_ref() { to.place_at(to.area(), e); } for (_index, dsl) in self.mode.view.iter().enumerate() { if let Err(e) = self.view(to, dsl) { *self.error.write().unwrap() = Some(format!("{e}").into()); break; } } } } impl View for App { fn view_expr <'a> (&'a self, to: &mut TuiOut, expr: &'a impl Expression) -> Usually<()> { if evaluate_output_expression(self, to, expr)? || evaluate_output_expression_tui(self, to, expr)? { Ok(()) } else { Err(format!("App::view_expr: unexpected: {expr:?}").into()) } } fn view_word <'a> (&'a self, to: &mut TuiOut, dsl: &'a impl Expression) -> Usually<()> { let mut frags = dsl.src()?.unwrap().split("/"); match frags.next() { Some(":logo") => to.place(&view_logo()), Some(":status") => to.place(&Fixed::Y(1, "TODO: Status Bar")), Some(":meters") => match frags.next() { Some("input") => to.place(&Tui::bg(Rgb(30, 30, 30), Fill::Y(Align::s("Input Meters")))), Some("output") => to.place(&Tui::bg(Rgb(30, 30, 30), Fill::Y(Align::s("Output Meters")))), _ => panic!() }, Some(":tracks") => match frags.next() { None => to.place(&"TODO tracks"), Some("names") => to.place(&self.project.view_track_names(self.color.clone())),//Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Names")))), Some("inputs") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Inputs")))), Some("devices") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Devices")))), Some("outputs") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Outputs")))), _ => panic!() }, Some(":scenes") => match frags.next() { None => to.place(&"TODO scenes"), Some(":scenes/names") => to.place(&"TODO Scene Names"), _ => panic!() }, Some(":editor") => to.place(&"TODO Editor"), Some(":dialog") => match frags.next() { Some("menu") => to.place(&if let Dialog::Menu(selected, items) = &self.dialog { let items = items.clone(); let selected = selected; Some(Fill::XY(Thunk::new(move|to: &mut TuiOut|{ for (index, MenuItem(item, _)) in items.0.iter().enumerate() { to.place(&Push::Y((2 * index) as u16, Tui::fg_bg( if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) }, if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) }, Fixed::Y(2, Align::n(Fill::X(item))) ))); } }))) } else { None }), _ => unimplemented!("App::view_word: {dsl:?} ({frags:?})"), }, Some(":templates") => to.place(&{ let modes = self.config.modes.clone(); let height = (modes.read().unwrap().len() * 2) as u16; Fixed::Y(height, Min::X(30, Thunk::new(move |to: &mut TuiOut|{ for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() { let bg = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) }; let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or(""); let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or(""); let fg1 = Rgb(224, 192, 128); let fg2 = Rgb(224, 128, 32); let field_name = Fill::X(Align::w(Tui::fg(fg1, name))); let field_id = Fill::X(Align::e(Tui::fg(fg2, id))); let field_info = Fill::X(Align::w(info)); to.place(&Push::Y((2 * index) as u16, Fixed::Y(2, Fill::X(Tui::bg(bg, Bsp::s( Bsp::a(field_name, field_id), field_info)))))); } }))) }), Some(":sessions") => to.place(&Fixed::Y(6, Min::X(30, Thunk::new(|to: &mut TuiOut|{ let fg = Rgb(224, 192, 128); for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { let bg = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; to.place(&Push::Y((2 * index) as u16, &Fixed::Y(2, Fill::X(Tui::bg(bg, Align::w(Tui::fg(fg, name))))))); } })))), Some(":browse/title") => to.place(&Fill::X(Align::w(FieldV(ItemColor::default(), match self.dialog.browser_target().unwrap() { BrowseTarget::SaveProject => "Save project:", BrowseTarget::LoadProject => "Load project:", BrowseTarget::ImportSample(_) => "Import sample:", BrowseTarget::ExportSample(_) => "Export sample:", BrowseTarget::ImportClip(_) => "Import clip:", BrowseTarget::ExportClip(_) => "Export clip:", }, Shrink::X(3, Fixed::Y(1, Tui::fg(Tui::g(96), Repeat::X("🭻")))))))), Some(":device") => { let selected = self.dialog.device_kind().unwrap(); to.place(&Bsp::s(Tui::bold(true, "Add device"), Map::south(1, move||device_kinds().iter(), move|_label: &&'static str, i|{ let bg = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }; let lb = if i == selected { "[ " } else { " " }; let rb = if i == selected { " ]" } else { " " }; Fill::X(Tui::bg(bg, Bsp::e(lb, Bsp::w(rb, "FIXME device name")))) }))) }, Some(":debug") => to.place(&Fixed::Y(1, format!("[{:?}]", to.area()))), Some(_) => { let views = self.config.views.read().unwrap(); if let Some(dsl) = views.get(dsl.src()?.unwrap()) { let dsl = dsl.clone(); std::mem::drop(views); self.view(to, &dsl)? } else { unimplemented!("{dsl:?}"); } }, _ => unreachable!() } Ok(()) } } impl Gettable for AtomicBool { fn get (&self) -> bool { self.load(Relaxed) } } impl InteriorMutable for AtomicBool { fn set (&self, value: bool) -> bool { self.swap(value, Relaxed) } } impl Gettable for AtomicUsize { fn get (&self) -> usize { self.load(Relaxed) } } impl InteriorMutable for AtomicUsize { fn set (&self, value: usize) -> usize { self.swap(value, Relaxed) } } //impl>> MaybeHas for U { //fn get (&self) -> Option<&T> { //Has::>::get(self).as_ref() //} //} impl Default for MidiCursor { fn default () -> Self { Self { time_pos: Arc::new(0.into()), note_pos: Arc::new(36.into()), note_len: Arc::new(24.into()), } } } impl NotePoint for MidiCursor { fn note_len (&self) -> &AtomicUsize { &self.note_len } fn note_pos (&self) -> &AtomicUsize { &self.note_pos } } impl TimePoint for MidiCursor { fn time_pos (&self) -> &AtomicUsize { self.time_pos.as_ref() } } impl MidiPoint for T {} from!(MidiSelection: |data:(usize, bool)| Self { time_len: Arc::new(0.into()), note_axis: Arc::new(0.into()), note_lo: Arc::new(0.into()), time_axis: Arc::new(0.into()), time_start: Arc::new(0.into()), time_zoom: Arc::new(data.0.into()), time_lock: Arc::new(data.1.into()), }); impl MidiRange for T {} impl TimeRange for MidiSelection { fn time_len (&self) -> &AtomicUsize { &self.time_len } fn time_zoom (&self) -> &AtomicUsize { &self.time_zoom } fn time_lock (&self) -> &AtomicBool { &self.time_lock } fn time_start (&self) -> &AtomicUsize { &self.time_start } fn time_axis (&self) -> &AtomicUsize { &self.time_axis } } impl NoteRange for MidiSelection { fn note_lo (&self) -> &AtomicUsize { &self.note_lo } fn note_axis (&self) -> &AtomicUsize { &self.note_axis } } impl Moment { pub fn zero (timebase: &Arc) -> Self { Self { usec: 0.into(), sample: 0.into(), pulse: 0.into(), timebase: timebase.clone() } } pub fn from_usec (timebase: &Arc, usec: f64) -> Self { Self { usec: usec.into(), sample: timebase.sr.usecs_to_sample(usec).into(), pulse: timebase.usecs_to_pulse(usec).into(), timebase: timebase.clone(), } } pub fn from_sample (timebase: &Arc, sample: f64) -> Self { Self { sample: sample.into(), usec: timebase.sr.samples_to_usec(sample).into(), pulse: timebase.samples_to_pulse(sample).into(), timebase: timebase.clone(), } } pub fn from_pulse (timebase: &Arc, pulse: f64) -> Self { Self { pulse: pulse.into(), sample: timebase.pulses_to_sample(pulse).into(), usec: timebase.pulses_to_usec(pulse).into(), timebase: timebase.clone(), } } #[inline] pub fn update_from_usec (&self, usec: f64) { self.usec.set(usec); self.pulse.set(self.timebase.usecs_to_pulse(usec)); self.sample.set(self.timebase.sr.usecs_to_sample(usec)); } #[inline] pub fn update_from_sample (&self, sample: f64) { self.usec.set(self.timebase.sr.samples_to_usec(sample)); self.pulse.set(self.timebase.samples_to_pulse(sample)); self.sample.set(sample); } #[inline] pub fn update_from_pulse (&self, pulse: f64) { self.usec.set(self.timebase.pulses_to_usec(pulse)); self.pulse.set(pulse); self.sample.set(self.timebase.pulses_to_sample(pulse)); } #[inline] pub fn format_beat (&self) -> Arc { self.timebase.format_beats_1(self.pulse.get()).into() } } impl LaunchSync { pub fn next (&self) -> f64 { note_duration_next(self.get() as usize) as f64 } pub fn prev (&self) -> f64 { note_duration_prev(self.get() as usize) as f64 } } impl Quantize { pub fn next (&self) -> f64 { note_duration_next(self.get() as usize) as f64 } pub fn prev (&self) -> f64 { note_duration_prev(self.get() as usize) as f64 } } impl Iterator for TicksIterator { type Item = (usize, usize); fn next (&mut self) -> Option { loop { if self.sample > self.end { return None } let spp = self.spp; let sample = self.sample as f64; let start = self.start; let end = self.end; self.sample += 1; //println!("{spp} {sample} {start} {end}"); let jitter = sample.rem_euclid(spp); // ramps let next_jitter = (sample + 1.0).rem_euclid(spp); if jitter > next_jitter { // at crossing: let time = (sample as usize) % (end as usize-start as usize); let tick = (sample / spp) as usize; return Some((time, tick)) } } } } impl Timebase { /// Specify sample rate, BPM and PPQ pub fn new ( s: impl Into, b: impl Into, p: impl Into ) -> Self { Self { sr: s.into(), bpm: b.into(), ppq: p.into() } } /// Iterate over ticks between start and end. #[inline] pub fn pulses_between_samples (&self, start: usize, end: usize) -> TicksIterator { TicksIterator { spp: self.samples_per_pulse(), sample: start, start, end } } /// Return the duration fo a beat in microseconds #[inline] pub fn usec_per_beat (&self) -> f64 { 60_000_000f64 / self.bpm.get() } /// Return the number of beats in a second #[inline] pub fn beat_per_second (&self) -> f64 { self.bpm.get() / 60f64 } /// Return the number of microseconds corresponding to a note of the given duration #[inline] pub fn note_to_usec (&self, (num, den): (f64, f64)) -> f64 { 4.0 * self.usec_per_beat() * num / den } /// Return duration of a pulse in microseconds (BPM-dependent) #[inline] pub fn pulse_per_usec (&self) -> f64 { self.ppq.get() / self.usec_per_beat() } /// Return duration of a pulse in microseconds (BPM-dependent) #[inline] pub fn usec_per_pulse (&self) -> f64 { self.usec_per_beat() / self.ppq.get() } /// Return number of pulses to which a number of microseconds corresponds (BPM-dependent) #[inline] pub fn usecs_to_pulse (&self, usec: f64) -> f64 { usec * self.pulse_per_usec() } /// Convert a number of pulses to a sample number (SR- and BPM-dependent) #[inline] pub fn pulses_to_usec (&self, pulse: f64) -> f64 { pulse / self.usec_per_pulse() } /// Return number of pulses in a second (BPM-dependent) #[inline] pub fn pulses_per_second (&self) -> f64 { self.beat_per_second() * self.ppq.get() } /// Return fraction of a pulse to which a sample corresponds (SR- and BPM-dependent) #[inline] pub fn pulses_per_sample (&self) -> f64 { self.usec_per_pulse() / self.sr.usec_per_sample() } /// Return number of samples in a pulse (SR- and BPM-dependent) #[inline] pub fn samples_per_pulse (&self) -> f64 { self.sr.get() / self.pulses_per_second() } /// Convert a number of pulses to a sample number (SR- and BPM-dependent) #[inline] pub fn pulses_to_sample (&self, p: f64) -> f64 { self.pulses_per_sample() * p } /// Convert a number of samples to a pulse number (SR- and BPM-dependent) #[inline] pub fn samples_to_pulse (&self, s: f64) -> f64 { s / self.pulses_per_sample() } /// Return the number of samples corresponding to a note of the given duration #[inline] pub fn note_to_samples (&self, note: (f64, f64)) -> f64 { self.usec_to_sample(self.note_to_usec(note)) } /// Return the number of samples corresponding to the given number of microseconds #[inline] pub fn usec_to_sample (&self, usec: f64) -> f64 { usec * self.sr.get() / 1000f64 } /// Return the quantized position of a moment in time given a step #[inline] pub fn quantize (&self, step: (f64, f64), time: f64) -> (f64, f64) { let step = self.note_to_usec(step); (time / step, time % step) } /// Quantize a collection of events #[inline] pub fn quantize_into + Sized, T> ( &self, step: (f64, f64), events: E ) -> Vec<(f64, f64)> { events.map(|(time, event)|(self.quantize(step, time).0, event)).collect() } /// Format a number of pulses into Beat.Bar.Pulse starting from 0 #[inline] pub fn format_beats_0 (&self, pulse: f64) -> Arc { let pulse = pulse as usize; let ppq = self.ppq.get() as usize; let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) }; format!("{}.{}.{pulses:02}", beats / 4, beats % 4).into() } /// Format a number of pulses into Beat.Bar starting from 0 #[inline] pub fn format_beats_0_short (&self, pulse: f64) -> Arc { let pulse = pulse as usize; let ppq = self.ppq.get() as usize; let beats = if ppq > 0 { pulse / ppq } else { 0 }; format!("{}.{}", beats / 4, beats % 4).into() } /// Format a number of pulses into Beat.Bar.Pulse starting from 1 #[inline] pub fn format_beats_1 (&self, pulse: f64) -> Arc { let mut string = String::with_capacity(16); self.format_beats_1_to(&mut string, pulse).expect("failed to format {pulse} into beat"); string.into() } /// Format a number of pulses into Beat.Bar.Pulse starting from 1 #[inline] pub fn format_beats_1_to (&self, w: &mut impl std::fmt::Write, pulse: f64) -> Result<(), std::fmt::Error> { let pulse = pulse as usize; let ppq = self.ppq.get() as usize; let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) }; write!(w, "{}.{}.{pulses:02}", beats / 4 + 1, beats % 4 + 1) } /// Format a number of pulses into Beat.Bar.Pulse starting from 1 #[inline] pub fn format_beats_1_short (&self, pulse: f64) -> Arc { let pulse = pulse as usize; let ppq = self.ppq.get() as usize; let beats = if ppq > 0 { pulse / ppq } else { 0 }; format!("{}.{}", beats / 4 + 1, beats % 4 + 1).into() } } impl Default for Timebase { fn default () -> Self { Self::new(48000f64, 150f64, DEFAULT_PPQ) } } impl SampleRate { /// Return the duration of a sample in microseconds (floating) #[inline] pub fn usec_per_sample (&self) -> f64 { 1_000_000f64 / self.get() } /// Return the duration of a sample in microseconds (floating) #[inline] pub fn sample_per_usec (&self) -> f64 { self.get() / 1_000_000f64 } /// Convert a number of samples to microseconds (floating) #[inline] pub fn samples_to_usec (&self, samples: f64) -> f64 { self.usec_per_sample() * samples } /// Convert a number of microseconds to samples (floating) #[inline] pub fn usecs_to_sample (&self, usecs: f64) -> f64 { self.sample_per_usec() * usecs } } impl Microsecond { #[inline] pub fn format_msu (&self) -> Arc { let usecs = self.get() as usize; let (seconds, msecs) = (usecs / 1000000, usecs / 1000 % 1000); let (minutes, seconds) = (seconds / 60, seconds % 60); format!("{minutes}:{seconds:02}:{msecs:03}").into() } } /// Implement an arithmetic operation for a unit of time #[macro_export] macro_rules! impl_op { ($T:ident, $Op:ident, $method:ident, |$a:ident,$b:ident|{$impl:expr}) => { impl $Op for $T { type Output = Self; #[inline] fn $method (self, other: Self) -> Self::Output { let $a = self.get(); let $b = other.get(); Self($impl.into()) } } impl $Op for $T { type Output = Self; #[inline] fn $method (self, other: usize) -> Self::Output { let $a = self.get(); let $b = other as f64; Self($impl.into()) } } impl $Op for $T { type Output = Self; #[inline] fn $method (self, other: f64) -> Self::Output { let $a = self.get(); let $b = other; Self($impl.into()) } } } } /// Define and implement a unit of time #[macro_export] macro_rules! impl_time_unit { ($T:ident) => { impl Gettable for $T { fn get (&self) -> f64 { self.0.load(Relaxed) } } impl InteriorMutable for $T { fn set (&self, value: f64) -> f64 { let old = self.get(); self.0.store(value, Relaxed); old } } impl TimeUnit for $T {} impl_op!($T, Add, add, |a, b|{a + b}); impl_op!($T, Sub, sub, |a, b|{a - b}); impl_op!($T, Mul, mul, |a, b|{a * b}); impl_op!($T, Div, div, |a, b|{a / b}); impl_op!($T, Rem, rem, |a, b|{a % b}); impl From for $T { fn from (value: f64) -> Self { Self(value.into()) } } impl From for $T { fn from (value: usize) -> Self { Self((value as f64).into()) } } impl From<$T> for f64 { fn from (value: $T) -> Self { value.get() } } impl From<$T> for usize { fn from (value: $T) -> Self { value.get() as usize } } impl From<&$T> for f64 { fn from (value: &$T) -> Self { value.get() } } impl From<&$T> for usize { fn from (value: &$T) -> Self { value.get() as usize } } impl Clone for $T { fn clone (&self) -> Self { Self(self.get().into()) } } } } impl_time_unit!(SampleCount); impl_time_unit!(SampleRate); impl_time_unit!(Microsecond); impl_time_unit!(Quantize); impl_time_unit!(Ppq); impl_time_unit!(Pulse); impl_time_unit!(Bpm); impl_time_unit!(LaunchSync); /// Implement [Jack] constructor and methods impl<'j> Jack<'j> { /// Register new [Client] and wrap it for shared use. pub fn new_run + Audio + Send + Sync + 'static> ( name: &impl AsRef, init: impl FnOnce(Jack<'j>)->Usually ) -> Usually>> { Jack::new(name)?.run(init) } pub fn new (name: &impl AsRef) -> Usually { let client = Client::new(name.as_ref(), ClientOptions::NO_START_SERVER)?.0; Ok(Jack(Arc::new(RwLock::new(JackState::Inactive(client))))) } pub fn run + Audio + Send + Sync + 'static> (self, init: impl FnOnce(Self)->Usually) -> Usually>> { let client_state = self.0.clone(); let app: Arc> = Arc::new(RwLock::new(init(self)?)); let mut state = Activating; std::mem::swap(&mut*client_state.write().unwrap(), &mut state); if let Inactive(client) = state { // This is the misc notifications handler. It's a struct that wraps a [Box] // which performs type erasure on a callback that takes [JackEvent], which is // one of the available misc notifications. let notify = JackNotify(Box::new({ let app = app.clone(); move|event|(&mut*app.write().unwrap()).handle(event) }) as BoxedJackEventHandler); // This is the main processing handler. It's a struct that wraps a [Box] // which performs type erasure on a callback that takes [Client] and [ProcessScope] // and passes them down to the `app`'s `process` callback, which in turn // implements audio and MIDI input and output on a realtime basis. let process = ClosureProcessHandler::new(Box::new({ let app = app.clone(); move|c: &_, s: &_|if let Ok(mut app) = app.write() { app.process(c, s) } else { Control::Quit } }) as BoxedAudioHandler); // Launch a client with the two handlers. *client_state.write().unwrap() = Active( client.activate_async(notify, process)? ); } else { unreachable!(); } Ok(app) } /// Run something with the client. pub fn with_client (&self, op: impl FnOnce(&Client)->T) -> T { match &*self.0.read().unwrap() { Inert => panic!("jack client not activated"), Inactive(client) => op(client), Activating => panic!("jack client has not finished activation"), Active(client) => op(client.as_client()), } } } impl<'j> HasJack<'j> for Jack<'j> { fn jack (&self) -> &Jack<'j> { self } } impl<'j> HasJack<'j> for &Jack<'j> { fn jack (&self) -> &Jack<'j> { self } } impl NotificationHandler for JackNotify { fn thread_init(&self, _: &Client) { self.0(JackEvent::ThreadInit); } unsafe fn shutdown(&mut self, status: ClientStatus, reason: &str) { self.0(JackEvent::Shutdown(status, reason.into())); } fn freewheel(&mut self, _: &Client, enabled: bool) { self.0(JackEvent::Freewheel(enabled)); } fn sample_rate(&mut self, _: &Client, frames: Frames) -> Control { self.0(JackEvent::SampleRate(frames)); Control::Quit } fn client_registration(&mut self, _: &Client, name: &str, reg: bool) { self.0(JackEvent::ClientRegistration(name.into(), reg)); } fn port_registration(&mut self, _: &Client, id: PortId, reg: bool) { self.0(JackEvent::PortRegistration(id, reg)); } fn port_rename(&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control { self.0(JackEvent::PortRename(id, old.into(), new.into())); Control::Continue } fn ports_connected(&mut self, _: &Client, a: PortId, b: PortId, are: bool) { self.0(JackEvent::PortsConnected(a, b, are)); } fn graph_reorder(&mut self, _: &Client) -> Control { self.0(JackEvent::GraphReorder); Control::Continue } fn xrun(&mut self, _: &Client) -> Control { self.0(JackEvent::XRun); Control::Continue } } impl JackPerfModel for PerfModel { fn update_from_jack_scope (&self, t0: Option, scope: &ProcessScope) { if let Some(t0) = t0 { let t1 = self.clock.raw(); self.used.store( self.clock.delta_as_nanos(t0, t1) as f64, Relaxed, ); self.window.store( scope.cycle_times().unwrap().period_usecs as f64, Relaxed, ); } } } has!(Jack<'static>: |self: Arrangement|self.jack); has!(Measure: |self: Arrangement|self.size); #[cfg(feature = "editor")] has!(Option: |self: Arrangement|self.editor); #[cfg(feature = "port")] has!(Vec: |self: Arrangement|self.midi_ins); #[cfg(feature = "port")] has!(Vec: |self: Arrangement|self.midi_outs); #[cfg(feature = "clock")] has!(Clock: |self: Arrangement|self.clock); #[cfg(feature = "select")] has!(Selection: |self: Arrangement|self.selection); #[cfg(feature = "track")] impl TracksView for Arrangement {} // -> to auto? #[cfg(all(feature = "select", feature = "track"))] has!(Vec: |self: Arrangement|self.tracks); #[cfg(all(feature = "select", feature = "track"))] maybe_has!(Track: |self: Arrangement| { Has::::get(self).track().map(|index|Has::>::get(self).get(index)).flatten() }; { Has::::get(self).track().map(|index|Has::>::get_mut(self).get_mut(index)).flatten() }); #[cfg(all(feature = "select", feature = "scene"))] has!(Vec: |self: Arrangement|self.scenes); #[cfg(all(feature = "select", feature = "scene"))] maybe_has!(Scene: |self: Arrangement| { Has::::get(self).track().map(|index|Has::>::get(self).get(index)).flatten() }; { Has::::get(self).track().map(|index|Has::>::get_mut(self).get_mut(index)).flatten() }); #[cfg(feature = "select")] impl Arrangement { #[cfg(feature = "clip")] fn selected_clip (&self) -> Option { todo!() } #[cfg(feature = "scene")] fn selected_scene (&self) -> Option { todo!() } #[cfg(feature = "track")] fn selected_track (&self) -> Option { todo!() } #[cfg(feature = "port")] fn selected_midi_in (&self) -> Option { todo!() } #[cfg(feature = "port")] fn selected_midi_out (&self) -> Option { todo!() } fn selected_device (&self) -> Option { todo!() } fn unselect (&self) -> Selection { Selection::Nothing } } #[cfg(feature = "vst2")] impl ::vst::host::Host for Plugin {} impl HasJack<'static> for Arrangement { fn jack (&self) -> &Jack<'static> { &self.jack } } impl Arrangement { /// Create a new arrangement. pub fn new ( jack: &Jack<'static>, name: Option>, clock: Clock, tracks: Vec, scenes: Vec, midi_ins: Vec, midi_outs: Vec, ) -> Self { Self { clock, tracks, scenes, midi_ins, midi_outs, jack: jack.clone(), name: name.unwrap_or_default(), color: ItemTheme::random(), selection: Selection::TrackClip { track: 0, scene: 0 }, ..Default::default() } } /// Width of display pub fn w (&self) -> u16 { self.size.w() as u16 } /// Width allocated for sidebar. pub fn w_sidebar (&self, is_editing: bool) -> u16 { self.w() / if is_editing { 16 } else { 8 } as u16 } /// Width available to display tracks. pub fn w_tracks_area (&self, is_editing: bool) -> u16 { self.w().saturating_sub(self.w_sidebar(is_editing)) } /// Height of display pub fn h (&self) -> u16 { self.size.h() as u16 } /// Height taken by visible device slots. pub fn h_devices (&self) -> u16 { 2 //1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) } } #[cfg(feature = "track")] impl Arrangement { /// Get the active track pub fn get_track (&self) -> Option<&Track> { let index = self.selection().track()?; Has::>::get(self).get(index) } /// Get a mutable reference to the active track pub fn get_track_mut (&mut self) -> Option<&mut Track> { let index = self.selection().track()?; Has::>::get_mut(self).get_mut(index) } /// Add multiple tracks pub fn tracks_add ( &mut self, count: usize, width: Option, mins: &[Connect], mouts: &[Connect], ) -> Usually<()> { 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 track = self.track_add(None, Some(color), mins, mouts)?.1; if let Some(width) = width { track.width = width; } } Ok(()) } /// Add a track pub fn track_add ( &mut self, name: Option<&str>, color: Option, mins: &[Connect], mouts: &[Connect], ) -> Usually<(usize, &mut Track)> { let name: Arc = name.map_or_else( ||format!("trk{:02}", self.track_last).into(), |x|x.to_string().into() ); self.track_last += 1; let track = Track { width: (name.len() + 2).max(12), color: color.unwrap_or_else(ItemTheme::random), sequencer: Sequencer::new( &format!("{name}"), self.jack(), Some(self.clock()), None, mins, mouts )?, name, ..Default::default() }; self.tracks_mut().push(track); let len = self.tracks().len(); let index = len - 1; for scene in self.scenes_mut().iter_mut() { while scene.clips.len() < len { scene.clips.push(None); } } Ok((index, &mut self.tracks_mut()[index])) } pub fn view_inputs (&self, _theme: ItemTheme) -> impl Content + '_ { Bsp::s( Fixed::Y(1, self.view_inputs_header()), Thunk::new(|to: &mut TuiOut|{ for (index, port) in self.midi_ins().iter().enumerate() { to.place(&Push::X(index as u16 * 10, Fixed::Y(1, self.view_inputs_row(port)))) } }) ) } fn view_inputs_header (&self) -> impl Content + '_ { Bsp::e(Fixed::X(20, Align::w(button_3("i", "nput ", format!("{}", self.midi_ins.len()), false))), Bsp::w(Fixed::X(4, button_2("I", "+", false)), Thunk::new(move|to: &mut TuiOut|for (_index, track, x1, _x2) in self.tracks_with_sizes() { #[cfg(feature = "track")] to.place(&Push::X(x1 as u16, Tui::bg(track.color.dark.rgb, Align::w(Fixed::X(track.width as u16, row!( Either::new(track.sequencer.monitoring, Tui::fg(Green, "mon "), "mon "), Either::new(track.sequencer.recording, Tui::fg(Red, "rec "), "rec "), Either::new(track.sequencer.overdub, Tui::fg(Yellow, "dub "), "dub "), )))))) }))) } fn view_inputs_row (&self, port: &MidiInput) -> impl Content { Bsp::e(Fixed::X(20, Align::w(Bsp::e(" ● ", Tui::bold(true, Tui::fg(Rgb(255,255,255), port.port_name()))))), Bsp::w(Fixed::X(4, ()), Thunk::new(move|to: &mut TuiOut|for (_index, track, _x1, _x2) in self.tracks_with_sizes() { #[cfg(feature = "track")] to.place(&Tui::bg(track.color.darker.rgb, Align::w(Fixed::X(track.width as u16, row!( Either::new(track.sequencer.monitoring, Tui::fg(Green, " ● "), " · "), Either::new(track.sequencer.recording, Tui::fg(Red, " ● "), " · "), Either::new(track.sequencer.overdub, Tui::fg(Yellow, " ● "), " · "), ))))) }))) } pub fn view_outputs (&self, theme: ItemTheme) -> impl Content { let mut h = 1; for output in self.midi_outs().iter() { h += 1 + output.connections.len(); } let h = h as u16; let list = Bsp::s( Fixed::Y(1, Fill::X(Align::w(button_3("o", "utput", format!("{}", self.midi_outs.len()), false)))), Fixed::Y(h - 1, Fill::XY(Align::nw(Thunk::new(|to: &mut TuiOut|{ for (_index, port) in self.midi_outs().iter().enumerate() { to.place(&Fixed::Y(1,Fill::X(Bsp::e( Align::w(Bsp::e(" ● ", Tui::fg(Rgb(255,255,255),Tui::bold(true, port.port_name())))), Fill::X(Align::e(format!("{}/{} ", port.port().get_connections().len(), port.connections.len()))))))); for (index, conn) in port.connections.iter().enumerate() { to.place(&Fixed::Y(1, Fill::X(Align::w(format!(" c{index:02}{}", conn.info()))))); } } }))))); Fixed::Y(h, view_track_row_section(theme, list, button_2("O", "+", false), Tui::bg(theme.darker.rgb, Align::w(Fill::X( Thunk::new(|to: &mut TuiOut|{ for (index, track, _x1, _x2) in self.tracks_with_sizes() { to.place(&Fixed::X(track_width(index, track), Thunk::new(|to: &mut TuiOut|{ to.place(&Fixed::Y(1, Align::w(Bsp::e( Either::new(true, Tui::fg(Green, "play "), "play "), Either::new(false, Tui::fg(Yellow, "solo "), "solo "), )))); for (_index, port) in self.midi_outs().iter().enumerate() { to.place(&Fixed::Y(1, Align::w(Bsp::e( Either::new(true, Tui::fg(Green, " ● "), " · "), Either::new(false, Tui::fg(Yellow, " ● "), " · "), )))); for (_index, _conn) in port.connections.iter().enumerate() { to.place(&Fixed::Y(1, Fill::X(""))); } }})))}})))))) } pub fn view_track_devices (&self, theme: ItemTheme) -> impl Content { let mut h = 2u16; for track in self.tracks().iter() { h = h.max(track.devices.len() as u16 * 2); } view_track_row_section(theme, button_3("d", "evice", format!("{}", self.track().map(|t|t.devices.len()).unwrap_or(0)), false), button_2("D", "+", false), Thunk::new(move|to: &mut TuiOut|for (index, track, _x1, _x2) in self.tracks_with_sizes() { to.place(&Fixed::XY(track_width(index, track), h + 1, Tui::bg(track.color.dark.rgb, Align::nw(Map::south(2, move||0..h, |_, _index|Fixed::XY(track.width as u16, 2, Tui::fg_bg( ItemTheme::G[32].lightest.rgb, ItemTheme::G[32].dark.rgb, Align::nw(format!(" · {}", "--"))))))))); })) } } #[cfg(feature = "scene")] impl Arrangement { /// Get the active scene pub fn get_scene (&self) -> Option<&Scene> { let index = self.selection().scene()?; Has::>::get(self).get(index) } /// Get a mutable reference to the active scene pub fn get_scene_mut (&mut self) -> Option<&mut Scene> { let index = self.selection().scene()?; Has::>::get_mut(self).get_mut(index) } } #[cfg(feature = "scene")] impl ScenesView for Arrangement { fn h_scenes (&self) -> u16 { (self.measure_height() as u16).saturating_sub(20) } fn w_side (&self) -> u16 { (self.measure_width() as u16 * 2 / 10).max(20) } fn w_mid (&self) -> u16 { (self.measure_width() as u16).saturating_sub(2 * self.w_side()).max(40) } } #[cfg(feature = "clip")] impl Arrangement { /// Get the active clip pub fn get_clip (&self) -> Option>> { self.get_scene()?.clips.get(self.selection().track()?)?.clone() } /// Put a clip in a slot pub fn clip_put ( &mut self, track: usize, scene: usize, clip: Option>> ) -> Option>> { let old = self.scenes[scene].clips[track].clone(); self.scenes[scene].clips[track] = clip; old } /// Change the color of a clip, returning the previous one pub fn clip_set_color (&self, track: usize, scene: usize, color: ItemTheme) -> Option { self.scenes[scene].clips[track].as_ref().map(|clip|{ let mut clip = clip.write().unwrap(); let old = clip.color.clone(); clip.color = color.clone(); panic!("{color:?} {old:?}"); old }) } /// Toggle looping for the active clip pub fn toggle_loop (&mut self) { if let Some(clip) = self.get_clip() { clip.write().unwrap().toggle_loop() } } } #[cfg(feature = "sampler")] impl Arrangement { /// Get the first sampler of the active track pub fn sampler (&self) -> Option<&Sampler> { self.get_track()?.sampler(0) } /// Get the first sampler of the active track pub fn sampler_mut (&mut self) -> Option<&mut Sampler> { self.get_track_mut()?.sampler_mut(0) } } impl HasClipsSize for Arrangement { fn clips_size (&self) -> &Measure { &self.size_inner } } impl PartialEq for BrowseTarget { fn eq (&self, other: &Self) -> bool { match self { Self::ImportSample(_) => false, Self::ExportSample(_) => false, Self::ImportClip(_) => false, Self::ExportClip(_) => false, t => matches!(other, t) } } } impl Browse { pub fn new (cwd: Option) -> Usually { let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? }; let mut dirs = vec![]; let mut files = vec![]; for entry in std::fs::read_dir(&cwd)? { let entry = entry?; let name = entry.file_name(); let decoded = name.clone().into_string().unwrap_or_else(|_|"".to_string()); let meta = entry.metadata()?; if meta.is_dir() { dirs.push((name, format!("📁 {decoded}"))); } else if meta.is_file() { files.push((name, format!("📄 {decoded}"))); } } Ok(Self { cwd, dirs, files, ..Default::default() }) } pub fn len (&self) -> usize { self.dirs.len() + self.files.len() } pub fn is_dir (&self) -> bool { self.index < self.dirs.len() } pub fn is_file (&self) -> bool { self.index >= self.dirs.len() } pub fn path (&self) -> PathBuf { self.cwd.join(if self.is_dir() { &self.dirs[self.index].0 } else if self.is_file() { &self.files[self.index - self.dirs.len()].0 } else { unreachable!() }) } pub fn chdir (&self) -> Usually { Self::new(Some(self.path())) } } impl Browse { fn _todo_stub_path_buf (&self) -> PathBuf { todo!() } fn _todo_stub_usize (&self) -> usize { todo!() } fn _todo_stub_arc_str (&self) -> Arc { todo!() } } impl HasContent for Browse { fn content (&self) -> impl Content { Map::south(1, ||EntriesIterator { offset: 0, index: 0, length: self.dirs.len() + self.files.len(), browser: self, }, |entry, _index|Fill::X(Align::w(entry))) } } impl<'a> Iterator for EntriesIterator<'a> { type Item = Modify<&'a str>; fn next (&mut self) -> Option { let dirs = self.browser.dirs.len(); let files = self.browser.files.len(); let index = self.index; if self.index < dirs { self.index += 1; Some(Tui::bold(true, self.browser.dirs[index].1.as_str())) } else if self.index < dirs + files { self.index += 1; Some(Tui::bold(false, self.browser.files[index - dirs].1.as_str())) } else { None } } } impl MidiClip { pub fn new ( name: impl AsRef, looped: bool, length: usize, notes: Option, color: Option, ) -> Self { Self { uuid: uuid::Uuid::new_v4(), name: name.as_ref().into(), ppq: PPQ, length, notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]), looped, loop_start: 0, loop_length: length, percussive: true, color: color.unwrap_or_else(ItemTheme::random) } } pub fn count_midi_messages (&self) -> usize { let mut count = 0; for tick in self.notes.iter() { count += tick.len(); } count } pub fn set_length (&mut self, length: usize) { self.length = length; self.notes = vec![Vec::with_capacity(16);length]; } pub fn duplicate (&self) -> Self { let mut clone = self.clone(); clone.uuid = uuid::Uuid::new_v4(); clone } pub fn toggle_loop (&mut self) { self.looped = !self.looped; } pub fn record_event (&mut self, pulse: usize, message: MidiMessage) { if pulse >= self.length { panic!("extend clip first") } self.notes[pulse].push(message); } /// Check if a range `start..end` contains MIDI Note On `k` pub fn contains_note_on (&self, k: u7, start: usize, end: usize) -> bool { for events in self.notes[start.max(0)..end.min(self.notes.len())].iter() { for event in events.iter() { if let MidiMessage::NoteOn {key,..} = event { if *key == k { return true } } } } false } pub fn stop_all () -> Self { Self::new( "Stop", false, 1, Some(vec![vec![MidiMessage::Controller { controller: 123.into(), value: 0.into() }]]), Some(ItemColor::from_rgb(Color::Rgb(32, 32, 32)).into()) ) } } impl PartialEq for MidiClip { fn eq (&self, other: &Self) -> bool { self.uuid == other.uuid } } impl Eq for MidiClip {} impl MidiClip { fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } fn _todo_bool_stub_ (&self) -> bool { todo!() } fn _todo_usize_stub_ (&self) -> usize { todo!() } fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } fn _todo_opt_item_theme_stub (&self) -> Option { todo!() } } //take!(ClipCommand |state: Arrangement, iter|state.selected_clip().as_ref() //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); impl> HasClock for T { fn clock (&self) -> &Clock { self.get() } fn clock_mut (&mut self) -> &mut Clock { self.get_mut() } } impl std::fmt::Debug for Clock { fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.debug_struct("Clock") .field("timebase", &self.timebase) .field("chunk", &self.chunk) .field("quant", &self.quant) .field("sync", &self.sync) .field("global", &self.global) .field("playhead", &self.playhead) .field("started", &self.started) .finish() } } impl Clock { pub fn new (jack: &Jack<'static>, bpm: Option) -> Usually { let (chunk, transport) = jack.with_client(|c|(c.buffer_size(), c.transport())); let timebase = Arc::new(Timebase::default()); let clock = Self { quant: Arc::new(24.into()), sync: Arc::new(384.into()), transport: Arc::new(Some(transport)), chunk: Arc::new((chunk as usize).into()), global: Arc::new(Moment::zero(&timebase)), playhead: Arc::new(Moment::zero(&timebase)), offset: Arc::new(Moment::zero(&timebase)), started: RwLock::new(None).into(), timebase, midi_in: Arc::new(RwLock::new(Some(MidiInput::new(jack, &"M/clock", &[])?))), midi_out: Arc::new(RwLock::new(Some(MidiOutput::new(jack, &"clock/M", &[])?))), click_out: Arc::new(RwLock::new(Some(AudioOutput::new(jack, &"click", &[])?))), ..Default::default() }; if let Some(bpm) = bpm { clock.timebase.bpm.set(bpm); } Ok(clock) } pub fn timebase (&self) -> &Arc { &self.timebase } /// Current sample rate pub fn sr (&self) -> &SampleRate { &self.timebase.sr } /// Current tempo pub fn bpm (&self) -> &Bpm { &self.timebase.bpm } /// Current MIDI resolution pub fn ppq (&self) -> &Ppq { &self.timebase.ppq } /// Next pulse that matches launch sync (for phrase switchover) pub fn next_launch_pulse (&self) -> usize { let sync = self.sync.get() as usize; let pulse = self.playhead.pulse.get() as usize; if pulse % sync == 0 { pulse } else { (pulse / sync + 1) * sync } } /// Start playing, optionally seeking to a given location beforehand pub fn play_from (&self, start: Option) -> Usually<()> { if let Some(transport) = self.transport.as_ref() { if let Some(start) = start { transport.locate(start)?; } transport.start()?; } Ok(()) } /// Pause, optionally seeking to a given location afterwards pub fn pause_at (&self, pause: Option) -> Usually<()> { if let Some(transport) = self.transport.as_ref() { transport.stop()?; if let Some(pause) = pause { transport.locate(pause)?; } } Ok(()) } /// Is currently paused? pub fn is_stopped (&self) -> bool { self.started.read().unwrap().is_none() } /// Is currently playing? pub fn is_rolling (&self) -> bool { self.started.read().unwrap().is_some() } /// Update chunk size pub fn set_chunk (&self, n_frames: usize) { self.chunk.store(n_frames, Relaxed); } pub fn update_from_scope (&self, scope: &ProcessScope) -> Usually<()> { // Store buffer length self.set_chunk(scope.n_frames() as usize); // Store reported global frame and usec let CycleTimes { current_frames, current_usecs, .. } = scope.cycle_times()?; self.global.sample.set(current_frames as f64); self.global.usec.set(current_usecs as f64); let mut started = self.started.write().unwrap(); // If transport has just started or just stopped, // update starting point: if let Some(transport) = self.transport.as_ref() { match (transport.query_state()?, started.as_ref()) { (TransportState::Rolling, None) => { let moment = Moment::zero(&self.timebase); moment.sample.set(current_frames as f64); moment.usec.set(current_usecs as f64); *started = Some(moment); }, (TransportState::Stopped, Some(_)) => { *started = None; }, _ => {} }; } self.playhead.update_from_sample(started.as_ref() .map(|started|current_frames as f64 - started.sample.get()) .unwrap_or(0.)); Ok(()) } pub fn bbt (&self) -> PositionBBT { let pulse = self.playhead.pulse.get() as i32; let ppq = self.timebase.ppq.get() as i32; let bpm = self.timebase.bpm.get(); let bar = (pulse / ppq) / 4; PositionBBT { bar: 1 + bar, beat: 1 + (pulse / ppq) % 4, tick: (pulse % ppq), bar_start_tick: (bar * 4 * ppq) as f64, beat_type: 4., beats_per_bar: 4., beats_per_minute: bpm, ticks_per_beat: ppq as f64 } } pub fn next_launch_instant (&self) -> Moment { Moment::from_pulse(self.timebase(), self.next_launch_pulse() as f64) } /// Get index of first sample to populate. /// /// Greater than 0 means that the first pulse of the clip /// falls somewhere in the middle of the chunk. pub fn get_sample_offset (&self, scope: &ProcessScope, started: &Moment) -> usize{ (scope.last_frame_time() as usize).saturating_sub( started.sample.get() as usize + self.started.read().unwrap().as_ref().unwrap().sample.get() as usize ) } // Get iterator that emits sample paired with pulse. // // * Sample: index into output buffer at which to write MIDI event // * Pulse: index into clip from which to take the MIDI event // // Emitted for each sample of the output buffer that corresponds to a MIDI pulse. pub fn get_pulses (&self, scope: &ProcessScope, offset: usize) -> TicksIterator { self.timebase().pulses_between_samples(offset, offset + scope.n_frames() as usize) } } impl Clock { fn _todo_provide_u32 (&self) -> u32 { todo!() } fn _todo_provide_opt_u32 (&self) -> Option { todo!() } fn _todo_provide_f64 (&self) -> f64 { todo!() } } impl Command for ClockCommand { fn execute (&self, state: &mut T) -> Perhaps { self.execute(state.clock_mut()) // awesome } } impl Default for ClockView { fn default () -> Self { let mut beat = String::with_capacity(16); let _ = write!(beat, "{}", Self::BEAT_EMPTY); let mut time = String::with_capacity(16); let _ = write!(time, "{}", Self::TIME_EMPTY); let mut bpm = String::with_capacity(16); let _ = write!(bpm, "{}", Self::BPM_EMPTY); Self { beat: Memo::new(None, beat), time: Memo::new(None, time), bpm: Memo::new(None, bpm), sr: Memo::new(None, String::with_capacity(16)), buf: Memo::new(None, String::with_capacity(16)), lat: Memo::new(None, String::with_capacity(16)), } } } impl ClockView { pub const BEAT_EMPTY: &'static str = "-.-.--"; pub const TIME_EMPTY: &'static str = "-.---s"; pub const BPM_EMPTY: &'static str = "---.---"; //pub fn track_counter (cache: &Arc>, track: usize, tracks: usize) //-> Arc> //{ //let data = (track, tracks); //cache.write().unwrap().trks.update(Some(data), rewrite!(buf, "{}/{}", data.0, data.1)); //cache.read().unwrap().trks.view.clone() //} //pub fn scene_add (cache: &Arc>, scene: usize, scenes: usize, is_editing: bool) //-> impl Content //{ //let data = (scene, scenes); //cache.write().unwrap().scns.update(Some(data), rewrite!(buf, "({}/{})", data.0, data.1)); //button_3("S", "add scene", cache.read().unwrap().scns.view.clone(), is_editing) //} pub fn update_clock (cache: &Arc>, clock: &Clock, compact: bool) { let rate = clock.timebase.sr.get(); let chunk = clock.chunk.load(Relaxed) as f64; let lat = chunk / rate * 1000.; let delta = |start: &Moment|clock.global.usec.get() - start.usec.get(); let mut cache = cache.write().unwrap(); cache.buf.update(Some(chunk), rewrite!(buf, "{chunk}")); cache.lat.update(Some(lat), rewrite!(buf, "{lat:.1}ms")); cache.sr.update(Some((compact, rate)), |buf,_,_|{ buf.clear(); if compact { write!(buf, "{:.1}kHz", rate / 1000.) } else { write!(buf, "{:.0}Hz", rate) } }); if let Some(now) = clock.started.read().unwrap().as_ref().map(delta) { let pulse = clock.timebase.usecs_to_pulse(now); let time = now/1000000.; let bpm = clock.timebase.bpm.get(); cache.beat.update(Some(pulse), |buf, _, _|{ buf.clear(); clock.timebase.format_beats_1_to(buf, pulse) }); cache.time.update(Some(time), rewrite!(buf, "{:.3}s", time)); cache.bpm.update(Some(bpm), rewrite!(buf, "{:.3}", bpm)); } else { cache.beat.update(None, rewrite!(buf, "{}", ClockView::BEAT_EMPTY)); cache.time.update(None, rewrite!(buf, "{}", ClockView::TIME_EMPTY)); cache.bpm.update(None, rewrite!(buf, "{}", ClockView::BPM_EMPTY)); } } //pub fn view_h2 (&self) -> impl Content { //let cache = self.project.clock.view_cache.clone(); //let cache = cache.read().unwrap(); //add(&Fixed::x(15, Align::w(Bsp::s( //FieldH(theme, "Beat", cache.beat.view.clone()), //FieldH(theme, "Time", cache.time.view.clone()), //)))); //add(&Fixed::x(13, Align::w(Bsp::s( //Fill::X(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))), //Fill::X(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))), //)))); //add(&Fixed::x(12, Align::w(Bsp::s( //Fill::X(Align::w(FieldH(theme, "Buf", cache.buf.view.clone()))), //Fill::X(Align::w(FieldH(theme, "Lat", cache.lat.view.clone()))), //)))); //add(&Bsp::s( //Fill::X(Align::w(FieldH(theme, "Selected", Align::w(self.selection().describe( //self.tracks(), //self.scenes() //))))), //Fill::X(Align::w(FieldH(theme, format!("History ({})", self.history.len()), //self.history.last().map(|last|Fill::X(Align::w(format!("{:?}", last.0))))))) //)); ////if let Some(last) = self.history.last() { ////add(&FieldV(theme, format!("History ({})", self.history.len()), ////Fill::X(Align::w(format!("{:?}", last.0))))); ////} //} } impl>> HasEditor for T {} has!(Measure: |self: MidiEditor|self.size); impl Default for MidiEditor { fn default () -> Self { Self { size: Measure::new(0, 0), mode: PianoHorizontal::new(None), } } } impl std::fmt::Debug for MidiEditor { fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.debug_struct("MidiEditor").field("mode", &self.mode).finish() } } from!(MidiEditor: |clip: &Arc>| { let model = Self::from(Some(clip.clone())); model.redraw(); model }); from!(MidiEditor: |clip: Option>>| { let mut model = Self::default(); *model.clip_mut() = clip; model.redraw(); model }); impl MidiEditor { /// Put note at current position pub fn put_note (&mut self, advance: bool) { let mut redraw = false; if let Some(clip) = self.clip() { let mut clip = clip.write().unwrap(); let note_start = self.get_time_pos(); let note_pos = self.get_note_pos(); let note_len = self.get_note_len(); let note_end = note_start + (note_len.saturating_sub(1)); let key: u7 = u7::from(note_pos as u8); let vel: u7 = 100.into(); let length = clip.length; let note_end = note_end % length; let note_on = MidiMessage::NoteOn { key, vel }; if !clip.notes[note_start].iter().any(|msg|*msg == note_on) { clip.notes[note_start].push(note_on); } let note_off = MidiMessage::NoteOff { key, vel }; if !clip.notes[note_end].iter().any(|msg|*msg == note_off) { clip.notes[note_end].push(note_off); } if advance { self.set_time_pos((note_end + 1) % clip.length); } redraw = true; } if redraw { self.mode.redraw(); } } fn _todo_opt_clip_stub (&self) -> Option>> { todo!() } fn clip_length (&self) -> usize { self.clip().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1) } fn note_length (&self) -> usize { self.get_note_len() } fn note_pos (&self) -> usize { self.get_note_pos() } fn note_pos_next (&self) -> usize { self.get_note_pos() + 1 } fn note_pos_next_octave (&self) -> usize { self.get_note_pos() + 12 } fn note_pos_prev (&self) -> usize { self.get_note_pos().saturating_sub(1) } fn note_pos_prev_octave (&self) -> usize { self.get_note_pos().saturating_sub(12) } fn note_len (&self) -> usize { self.get_note_len() } fn note_len_next (&self) -> usize { self.get_note_len() + 1 } fn note_len_prev (&self) -> usize { self.get_note_len().saturating_sub(1) } fn note_range (&self) -> usize { self.get_note_axis() } fn note_range_next (&self) -> usize { self.get_note_axis() + 1 } fn note_range_prev (&self) -> usize { self.get_note_axis().saturating_sub(1) } fn time_zoom (&self) -> usize { self.get_time_zoom() } fn time_zoom_next (&self) -> usize { self.get_time_zoom() + 1 } fn time_zoom_next_fine (&self) -> usize { self.get_time_zoom() + 1 } fn time_zoom_prev (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } fn time_zoom_prev_fine (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } fn time_lock (&self) -> bool { self.get_time_lock() } fn time_lock_toggled (&self) -> bool { !self.get_time_lock() } fn time_pos (&self) -> usize { self.get_time_pos() } fn time_pos_next (&self) -> usize { (self.get_time_pos() + self.get_note_len()) % self.clip_length() } fn time_pos_next_fine (&self) -> usize { (self.get_time_pos() + 1) % self.clip_length() } fn time_pos_prev (&self) -> usize { let step = self.get_note_len(); self.get_time_pos().overflowing_sub(step) .0.min(self.clip_length().saturating_sub(step)) } fn time_pos_prev_fine (&self) -> usize { self.get_time_pos().overflowing_sub(1) .0.min(self.clip_length().saturating_sub(1)) } pub fn clip_status (&self) -> impl Content + '_ { let (_color, name, length, looped) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { (clip.color, clip.name.clone(), clip.length, clip.looped) } else { (ItemTheme::G[64], String::new().into(), 0, false) }; Fixed::X(20, col!( Fill::X(Align::w(Bsp::e( button_2("f2", "name ", false), Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{name} "))))))), Fill::X(Align::w(Bsp::e( button_2("l", "ength ", false), Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{length} "))))))), Fill::X(Align::w(Bsp::e( button_2("r", "epeat ", false), Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{looped} "))))))), )) } pub fn edit_status (&self) -> impl Content + '_ { let (_color, length) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { (clip.color, clip.length) } else { (ItemTheme::G[64], 0) }; let time_pos = self.get_time_pos(); let time_zoom = self.get_time_zoom(); let time_lock = if self.get_time_lock() { "[lock]" } else { " " }; let note_pos = self.get_note_pos(); let note_name = format!("{:4}", note_pitch_to_name(note_pos)); let note_pos = format!("{:>3}", note_pos); let note_len = format!("{:>4}", self.get_note_len()); Fixed::X(20, col!( Fill::X(Align::w(Bsp::e( button_2("t", "ime ", false), Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{length} /{time_zoom} +{time_pos} "))))))), Fill::X(Align::w(Bsp::e( button_2("z", "lock ", false), Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{time_lock}"))))))), Fill::X(Align::w(Bsp::e( button_2("x", "note ", false), Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{note_name} {note_pos} {note_len}"))))))), )) } } impl TimeRange for MidiEditor { fn time_len (&self) -> &AtomicUsize { self.mode.time_len() } fn time_zoom (&self) -> &AtomicUsize { self.mode.time_zoom() } fn time_lock (&self) -> &AtomicBool { self.mode.time_lock() } fn time_start (&self) -> &AtomicUsize { self.mode.time_start() } fn time_axis (&self) -> &AtomicUsize { self.mode.time_axis() } } impl NoteRange for MidiEditor { fn note_lo (&self) -> &AtomicUsize { self.mode.note_lo() } fn note_axis (&self) -> &AtomicUsize { self.mode.note_axis() } } impl NotePoint for MidiEditor { fn note_len (&self) -> &AtomicUsize { self.mode.note_len() } fn note_pos (&self) -> &AtomicUsize { self.mode.note_pos() } } impl TimePoint for MidiEditor { fn time_pos (&self) -> &AtomicUsize { self.mode.time_pos() } } impl MidiViewer for MidiEditor { fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { self.mode.buffer_size(clip) } fn redraw (&self) { self.mode.redraw() } fn clip (&self) -> &Option>> { self.mode.clip() } fn clip_mut (&mut self) -> &mut Option>> { self.mode.clip_mut() } fn set_clip (&mut self, p: Option<&Arc>>) { self.mode.set_clip(p) } } impl Draw for MidiEditor { fn draw (&self, to: &mut TuiOut) { self.content().draw(to) } } impl Layout for MidiEditor { fn layout (&self, to: XYWH) -> XYWH { self.content().layout(to) } } impl HasContent for MidiEditor { fn content (&self) -> impl Content { self.autoscroll(); /*self.autozoom();*/ self.size.of(&self.mode) } } has!(Measure:|self:PianoHorizontal|self.size); impl PianoHorizontal { pub fn new (clip: Option<&Arc>>) -> Self { let size = Measure::new(0, 0); let mut range = MidiSelection::from((12, true)); range.time_axis = size.x.clone(); range.note_axis = size.y.clone(); let piano = Self { keys_width: 5, size, range, buffer: RwLock::new(Default::default()).into(), point: MidiCursor::default(), clip: clip.cloned(), color: clip.as_ref().map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]), }; piano.redraw(); piano } } impl Draw for PianoHorizontal { fn draw (&self, to: &mut TuiOut) { self.content().draw(to) } } impl Layout for PianoHorizontal { fn layout (&self, to: XYWH) -> XYWH { self.content().layout(to) } } impl HasContent for PianoHorizontal { fn content (&self) -> impl Content { Bsp::s( Bsp::e(Fixed::X(5, format!("{}x{}", self.size.w(), self.size.h())), self.timeline()), Bsp::e(self.keys(), self.size.of(Bsp::b(Fill::XY(self.notes()), Fill::XY(self.cursor())))), ) } } impl PianoHorizontal { /// Draw the piano roll background. /// /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ fn draw_bg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize, note_len: usize, note_point: usize, time_point: usize) { for (y, note) in (0..=127).rev().enumerate() { for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) { let cell = buf.get_mut(x, y).unwrap(); if note == (127-note_point) || time == time_point { cell.set_bg(Rgb(0,0,0)); } else { cell.set_bg(clip.color.darkest.rgb); } if time % 384 == 0 { cell.set_fg(clip.color.darker.rgb); cell.set_char('│'); } else if time % 96 == 0 { cell.set_fg(clip.color.dark.rgb); cell.set_char('╎'); } else if time % note_len == 0 { cell.set_fg(clip.color.darker.rgb); cell.set_char('┊'); } else if (127 - note) % 12 == 0 { cell.set_fg(clip.color.darker.rgb); cell.set_char('='); } else if (127 - note) % 6 == 0 { cell.set_fg(clip.color.darker.rgb); cell.set_char('—'); } else { cell.set_fg(clip.color.darker.rgb); cell.set_char('·'); } } } } /// Draw the piano roll foreground. /// /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ fn draw_fg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize) { let style = Style::default().fg(clip.color.base.rgb);//.bg(Rgb(0, 0, 0)); let mut notes_on = [false;128]; for (x, time_start) in (0..clip.length).step_by(zoom).enumerate() { for (_y, note) in (0..=127).rev().enumerate() { if let Some(cell) = buf.get_mut(x, note) { if notes_on[note] { cell.set_char('▂'); cell.set_style(style); } } } let time_end = time_start + zoom; for time in time_start..time_end.min(clip.length) { for event in clip.notes[time].iter() { match event { MidiMessage::NoteOn { key, .. } => { let note = key.as_int() as usize; if let Some(cell) = buf.get_mut(x, note) { cell.set_char('█'); cell.set_style(style); } notes_on[note] = true }, MidiMessage::NoteOff { key, .. } => { notes_on[key.as_int() as usize] = false }, _ => {} } } } } } fn notes (&self) -> impl Content { let time_start = self.get_time_start(); let note_lo = self.get_note_lo(); let note_hi = self.get_note_hi(); let buffer = self.buffer.clone(); Thunk::new(move|to: &mut TuiOut|{ let source = buffer.read().unwrap(); let XYWH(x0, y0, w, _h) = to.area(); //if h as usize != note_axis { //panic!("area height mismatch: {h} <> {note_axis}"); //} for (area_x, screen_x) in (x0..x0+w).enumerate() { for (area_y, screen_y, _note) in note_y_iter(note_lo, note_hi, y0) { let source_x = time_start + area_x; let source_y = note_hi - area_y; // TODO: enable loop rollover: //let source_x = (time_start + area_x) % source.width.max(1); //let source_y = (note_hi - area_y) % source.height.max(1); let is_in_x = source_x < source.width; let is_in_y = source_y < source.height; if is_in_x && is_in_y { if let Some(source_cell) = source.get(source_x, source_y) { if let Some(cell) = to.buffer.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) { *cell = source_cell.clone(); } } } } } }) } fn cursor (&self) -> impl Content { let note_hi = self.get_note_hi(); let note_lo = self.get_note_lo(); let note_pos = self.get_note_pos(); let note_len = self.get_note_len(); let time_pos = self.get_time_pos(); let time_start = self.get_time_start(); let time_zoom = self.get_time_zoom(); let style = Some(Style::default().fg(self.color.lightest.rgb)); Thunk::new(move|to: &mut TuiOut|{ let XYWH(x0, y0, w, _) = to.area(); for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { if note == note_pos { for x in 0..w { let screen_x = x0 + x; let time_1 = time_start + x as usize * time_zoom; let time_2 = time_1 + time_zoom; if time_1 <= time_pos && time_pos < time_2 { to.blit(&"█", screen_x, screen_y, style); let tail = note_len as u16 / time_zoom as u16; for x_tail in (screen_x + 1)..(screen_x + tail) { to.blit(&"▂", x_tail, screen_y, style); } break } } break } } }) } fn keys (&self) -> impl Content { let state = self; let color = state.color; let note_lo = state.get_note_lo(); let note_hi = state.get_note_hi(); let note_pos = state.get_note_pos(); let key_style = Some(Style::default().fg(Rgb(192, 192, 192)).bg(Rgb(0, 0, 0))); let off_style = Some(Style::default().fg(Tui::g(255))); let on_style = Some(Style::default().fg(Rgb(255,0,0)).bg(color.base.rgb).bold()); Fill::Y(Fixed::X(self.keys_width, Thunk::new(move|to: &mut TuiOut|{ let XYWH(x, y0, _w, _h) = to.area(); for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { to.blit(&to_key(note), x, screen_y, key_style); if note > 127 { continue } if note == note_pos { to.blit(&format!("{:<5}", note_pitch_to_name(note)), x, screen_y, on_style) } else { to.blit(¬e_pitch_to_name(note), x, screen_y, off_style) }; } }))) } fn timeline (&self) -> impl Content + '_ { Fill::X(Fixed::Y(1, Thunk::new(move|to: &mut TuiOut|{ let XYWH(x, y, w, _h) = to.area(); let style = Some(Style::default().dim()); let length = self.clip.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) { let t = area_x as usize * self.time_zoom().get(); if t < length { to.blit(&"|", screen_x, y, style); } } }))) } } impl TimeRange for PianoHorizontal { fn time_len (&self) -> &AtomicUsize { self.range.time_len() } fn time_zoom (&self) -> &AtomicUsize { self.range.time_zoom() } fn time_lock (&self) -> &AtomicBool { self.range.time_lock() } fn time_start (&self) -> &AtomicUsize { self.range.time_start() } fn time_axis (&self) -> &AtomicUsize { self.range.time_axis() } } impl NoteRange for PianoHorizontal { fn note_lo (&self) -> &AtomicUsize { self.range.note_lo() } fn note_axis (&self) -> &AtomicUsize { self.range.note_axis() } } impl NotePoint for PianoHorizontal { fn note_len (&self) -> &AtomicUsize { self.point.note_len() } fn note_pos (&self) -> &AtomicUsize { self.point.note_pos() } } impl TimePoint for PianoHorizontal { fn time_pos (&self) -> &AtomicUsize { self.point.time_pos() } } impl MidiViewer for PianoHorizontal { fn clip (&self) -> &Option>> { &self.clip } fn clip_mut (&mut self) -> &mut Option>> { &mut self.clip } /// Determine the required space to render the clip. fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { (clip.length / self.range.time_zoom().get(), 128) } fn redraw (&self) { *self.buffer.write().unwrap() = if let Some(clip) = self.clip.as_ref() { let clip = clip.read().unwrap(); let buf_size = self.buffer_size(&clip); let mut buffer = BigBuffer::from(buf_size); let time_zoom = self.get_time_zoom(); self.time_len().set(clip.length); PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom,self.get_note_len(), self.get_note_pos(), self.get_time_pos()); PianoHorizontal::draw_fg(&mut buffer, &clip, time_zoom); buffer } else { Default::default() } } fn set_clip (&mut self, clip: Option<&Arc>>) { *self.clip_mut() = clip.cloned(); self.color = clip.map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]); self.redraw(); } } impl std::fmt::Debug for PianoHorizontal { fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { let buffer = self.buffer.read().unwrap(); f.debug_struct("PianoHorizontal") .field("time_zoom", &self.range.time_zoom) .field("buffer", &format!("{}x{}", buffer.width, buffer.height)) .finish() } } impl Default for OctaveVertical { fn default () -> Self { Self { on: [false; 12], colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)] } } } impl OctaveVertical { fn color (&self, pitch: usize) -> Color { let pitch = pitch % 12; self.colors[if self.on[pitch] { 2 } else { match pitch { 0 | 2 | 4 | 5 | 6 | 8 | 10 => 0, _ => 1 } }] } } impl HasContent for OctaveVertical { fn content (&self) -> impl Content + '_ { row!( Tui::fg_bg(self.color(0), self.color(1), "▙"), Tui::fg_bg(self.color(2), self.color(3), "▙"), Tui::fg_bg(self.color(4), self.color(5), "▌"), Tui::fg_bg(self.color(6), self.color(7), "▟"), Tui::fg_bg(self.color(8), self.color(9), "▟"), Tui::fg_bg(self.color(10), self.color(11), "▟"), ) } } impl Lv2 { pub fn new ( jack: &Jack<'static>, name: &str, uri: &str, ) -> Usually { let lv2_world = livi::World::with_load_bundle(&uri); let lv2_features = lv2_world.build_features(livi::FeaturesBuilder { min_block_length: 1, max_block_length: 65536, }); let lv2_plugin = lv2_world.iter_plugins().nth(0) .unwrap_or_else(||panic!("plugin not found: {uri}")); Ok(Self { jack: jack.clone(), name: name.into(), path: Some(String::from(uri).into()), selected: 0, mapping: false, midi_ins: vec![], midi_outs: vec![], audio_ins: vec![], audio_outs: vec![], lv2_instance: unsafe { lv2_plugin .instantiate(lv2_features.clone(), 48000.0) .expect(&format!("instantiate failed: {uri}")) }, lv2_port_list: lv2_plugin.ports().collect::>(), lv2_input_buffer: Vec::with_capacity(Self::INPUT_BUFFER), lv2_ui_thread: None, lv2_world, lv2_features, lv2_plugin, }) } const INPUT_BUFFER: usize = 1024; } fn lv2_jack_process ( Lv2 { midi_ins, midi_outs, audio_ins, audio_outs, lv2_features, lv2_instance, lv2_input_buffer, .. }: &mut Lv2, _client: &Client, scope: &ProcessScope ) -> Control { let urid = lv2_features.midi_urid(); lv2_input_buffer.clear(); for port in midi_ins.iter() { let mut atom = ::livi::event::LV2AtomSequence::new( &lv2_features, scope.n_frames() as usize ); for event in port.iter(scope) { match event.bytes.len() { 3 => atom.push_midi_event::<3>( event.time as i64, urid, &event.bytes[0..3] ).unwrap(), _ => {} } } lv2_input_buffer.push(atom); } let mut outputs = vec![]; for _ in midi_outs.iter() { outputs.push(::livi::event::LV2AtomSequence::new( lv2_features, scope.n_frames() as usize )); } let ports = ::livi::EmptyPortConnections::new() .with_atom_sequence_inputs(lv2_input_buffer.iter()) .with_atom_sequence_outputs(outputs.iter_mut()) .with_audio_inputs(audio_ins.iter().map(|o|o.as_slice(scope))) .with_audio_outputs(audio_outs.iter_mut().map(|o|o.as_mut_slice(scope))); unsafe { lv2_instance.run(scope.n_frames() as usize, ports).unwrap() }; Control::Continue } impl Draw for Lv2 { fn draw (&self, to: &mut TuiOut) { let area = to.area(); let XYWH(x, y, _, height) = area; let mut width = 20u16; let start = self.selected.saturating_sub((height as usize / 2).saturating_sub(1)); let end = start + height as usize - 2; //draw_box(buf, Rect { x, y, width, height }); for i in start..end { if let Some(port) = self.lv2_port_list.get(i) { let value = if let Some(value) = self.lv2_instance.control_input(port.index) { value } else { port.default_value }; //let label = &format!("C·· M·· {:25} = {value:.03}", port.name); let label = &format!("{:25} = {value:.03}", port.name); width = width.max(label.len() as u16 + 4); let style = if i == self.selected { Some(Style::default().green()) } else { None } ; to.blit(&label, x + 2, y + 1 + i as u16 - start as u16, style); } else { break } } draw_header(self, to, x, y, width); } } #[cfg(feature = "lv2_gui")] impl LV2PluginUI { pub fn new () -> Usually { Ok(Self { window: None }) } } #[cfg(feature = "lv2_gui")] impl ApplicationHandler for LV2PluginUI { fn resumed (&mut self, event_loop: &ActiveEventLoop) { self.window = Some(event_loop.create_window(Window::default_attributes()).unwrap()); } fn window_event (&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) { match event { WindowEvent::CloseRequested => { self.window.as_ref().unwrap().set_visible(false); event_loop.exit(); }, WindowEvent::RedrawRequested => { self.window.as_ref().unwrap().request_redraw(); } _ => (), } } } impl Layout for RmsMeter {} impl Draw for RmsMeter { fn draw (&self, to: &mut TuiOut) { let XYWH(x, y, w, h) = to.area(); let signal = f32::max(0.0, f32::min(100.0, self.0.abs())); let v = (signal * h as f32).ceil() as u16; let y2 = y + h; //to.blit(&format!("\r{v} {} {signal}", self.0), x * 30, y, Some(Style::default())); for y in y..(y + v) { for x in x..(x + w) { to.blit(&"▌", x, y2.saturating_sub(y), Some(Style::default().green())); } } } } impl Layout for Log10Meter {} impl Draw for Log10Meter { fn draw (&self, to: &mut TuiOut) { let XYWH(x, y, w, h) = to.area(); let signal = 100.0 - f32::max(0.0, f32::min(100.0, self.0.abs())); let v = (signal * h as f32 / 100.0).ceil() as u16; let y2 = y + h; //to.blit(&format!("\r{v} {} {signal}", self.0), x * 20, y, None); for y in y..(y + v) { for x in x..(x + w) { to.blit(&"▌", x, y2 - y, Some(Style::default().green())); } } } } impl Default for Pool { fn default () -> Self { //use PoolMode::*; Self { clip: 0.into(), mode: None, visible: true, #[cfg(feature = "clip")] clips: Arc::from(RwLock::from(vec![])), #[cfg(feature = "sampler")] samples: Arc::from(RwLock::from(vec![])), #[cfg(feature = "browse")] browse: None, } } } impl Pool { pub fn clip_index (&self) -> usize { self.clip.load(Relaxed) } pub fn set_clip_index (&self, value: usize) { self.clip.store(value, Relaxed); } pub fn mode (&self) -> &Option { &self.mode } pub fn mode_mut (&mut self) -> &mut Option { &mut self.mode } pub fn begin_clip_length (&mut self) { let length = self.clips()[self.clip_index()].read().unwrap().length; *self.mode_mut() = Some(PoolMode::Length( self.clip_index(), length, ClipLengthFocus::Bar )); } pub fn begin_clip_rename (&mut self) { let name = self.clips()[self.clip_index()].read().unwrap().name.clone(); *self.mode_mut() = Some(PoolMode::Rename( self.clip_index(), name )); } pub fn begin_import (&mut self) -> Usually<()> { *self.mode_mut() = Some(PoolMode::Import( self.clip_index(), Browse::new(None)? )); Ok(()) } pub fn begin_export (&mut self) -> Usually<()> { *self.mode_mut() = Some(PoolMode::Export( self.clip_index(), Browse::new(None)? )); Ok(()) } pub fn new_clip (&self) -> MidiClip { MidiClip::new("Clip", true, 4 * PPQ, None, Some(ItemTheme::random())) } pub fn cloned_clip (&self) -> MidiClip { let index = self.clip_index(); let mut clip = self.clips()[index].read().unwrap().duplicate(); clip.color = ItemTheme::random_near(clip.color, 0.25); clip } pub fn add_new_clip (&self) -> (usize, Arc>) { let clip = Arc::new(RwLock::new(self.new_clip())); let index = { let mut clips = self.clips.write().unwrap(); clips.push(clip.clone()); clips.len().saturating_sub(1) }; self.clip.store(index, Relaxed); (index, clip) } pub fn delete_clip (&mut self, clip: &MidiClip) -> bool { let index = self.clips.read().unwrap().iter().position(|x|*x.read().unwrap()==*clip); if let Some(index) = index { self.clips.write().unwrap().remove(index); return true } false } } impl ClipLengthFocus { pub fn next (&mut self) { use ClipLengthFocus::*; *self = match self { Bar => Beat, Beat => Tick, Tick => Bar, } } pub fn prev (&mut self) { use ClipLengthFocus::*; *self = match self { Bar => Tick, Beat => Bar, Tick => Beat, } } } impl ClipLength { pub fn _new (pulses: usize, focus: Option) -> Self { Self { ppq: PPQ, bpb: 4, pulses, focus } } pub fn bars (&self) -> usize { self.pulses / (self.bpb * self.ppq) } pub fn beats (&self) -> usize { (self.pulses % (self.bpb * self.ppq)) / self.ppq } pub fn ticks (&self) -> usize { self.pulses % self.ppq } pub fn bars_string (&self) -> Arc { format!("{}", self.bars()).into() } pub fn beats_string (&self) -> Arc { format!("{}", self.beats()).into() } pub fn ticks_string (&self) -> Arc { format!("{:>02}", self.ticks()).into() } } #[macro_export] macro_rules! has_clips { (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? { fn clips <'a> (&'a $self) -> std::sync::RwLockReadGuard<'a, ClipPool> { $cb.read().unwrap() } fn clips_mut <'a> (&'a $self) -> std::sync::RwLockWriteGuard<'a, ClipPool> { $cb.write().unwrap() } } } } has_clips!(|self: Pool|self.clips); has_clip!(|self: Pool|self.clips().get(self.clip_index()).map(|c|c.clone())); from!(Pool: |clip:&Arc>|{ let model = Self::default(); model.clips.write().unwrap().push(clip.clone()); model.clip.store(1, Relaxed); model }); impl Pool { fn _todo_usize_ (&self) -> usize { todo!() } fn _todo_bool_ (&self) -> bool { todo!() } fn _todo_clip_ (&self) -> MidiClip { todo!() } fn _todo_path_ (&self) -> PathBuf { todo!() } fn _todo_color_ (&self) -> ItemColor { todo!() } fn _todo_str_ (&self) -> Arc { todo!() } fn clip_new (&self) -> MidiClip { self.new_clip() } fn clip_cloned (&self) -> MidiClip { self.cloned_clip() } fn clip_index_current (&self) -> usize { 0 } fn clip_index_after (&self) -> usize { 0 } fn clip_index_previous (&self) -> usize { 0 } fn clip_index_next (&self) -> usize { 0 } fn color_random (&self) -> ItemColor { ItemColor::random() } } impl<'a> HasContent for PoolView<'a> { fn content (&self) -> impl Content { let Self(pool) = self; //let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||Tui::g(32).into()); //let on_bg = |x|x;//Bsp::b(Repeat(" "), Tui::bg(color.darkest.rgb, x)); //let border = |x|x;//Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)).enclose(x); //let height = pool.clips.read().unwrap().len() as u16; Fixed::X(20, Fill::Y(Align::n(Map::new( ||pool.clips().clone().into_iter(), move|clip: Arc>, i: usize|{ let item_height = 1; let item_offset = i as u16 * item_height; let selected = i == pool.clip_index(); let MidiClip { ref name, color, length, .. } = *clip.read().unwrap(); let bg = if selected { color.light.rgb } else { color.base.rgb }; let fg = color.lightest.rgb; let name = if false { format!(" {i:>3}") } else { format!(" {i:>3} {name}") }; let length = if false { String::default() } else { format!("{length} ") }; Fixed::Y(1, map_south(item_offset, item_height, Tui::bg(bg, lay!( Fill::X(Align::w(Tui::fg(fg, Tui::bold(selected, name)))), Fill::X(Align::e(Tui::fg(fg, Tui::bold(selected, length)))), Fill::X(Align::w(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "▶"))))), Fill::X(Align::e(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "◀"))))), )))) })))) } } impl HasContent for ClipLength { fn content (&self) -> impl Content + '_ { use ClipLengthFocus::*; let bars = ||self.bars_string(); let beats = ||self.beats_string(); let ticks = ||self.ticks_string(); match self.focus { None => row!(" ", bars(), ".", beats(), ".", ticks()), Some(Bar) => row!("[", bars(), "]", beats(), ".", ticks()), Some(Beat) => row!(" ", bars(), "[", beats(), "]", ticks()), Some(Tick) => row!(" ", bars(), ".", beats(), "[", ticks()), } } } impl> RegisterPorts for J { fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { MidiInput::new(self.jack(), name, connect) } fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { MidiOutput::new(self.jack(), name, connect) } fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { AudioInput::new(self.jack(), name, connect) } fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { AudioOutput::new(self.jack(), name, connect) } } //take!(MidiInputCommand |state: Arrangement, iter|state.selected_midi_in().as_ref() //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); //take!(MidiOutputCommand |state: Arrangement, iter|state.selected_midi_out().as_ref() //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); impl Connect { pub fn collect (exact: &[impl AsRef], re: &[impl AsRef], re_all: &[impl AsRef]) -> Vec { let mut connections = vec![]; for port in exact.iter() { connections.push(Self::exact(port)) } for port in re.iter() { connections.push(Self::regexp(port)) } for port in re_all.iter() { connections.push(Self::regexp_all(port)) } connections } /// Connect to this exact port pub fn exact (name: impl AsRef) -> Self { let info = format!("=:{}", name.as_ref()).into(); let name = Some(Exact(name.as_ref().into())); Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } } pub fn regexp (name: impl AsRef) -> Self { let info = format!("~:{}", name.as_ref()).into(); let name = Some(RegExp(name.as_ref().into())); Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } } pub fn regexp_all (name: impl AsRef) -> Self { let info = format!("+:{}", name.as_ref()).into(); let name = Some(RegExp(name.as_ref().into())); Self { name, scope: Some(All), status: Arc::new(RwLock::new(vec![])), info } } pub fn info (&self) -> Arc { format!(" ({}) {} {}", { let status = self.status.read().unwrap(); let mut ok = 0; for (_, _, state) in status.iter() { if *state == Connected { ok += 1 } } format!("{ok}/{}", status.len()) }, match self.scope { None => "x", Some(One) => " ", Some(All) => "*", }, match &self.name { None => format!("x"), Some(Exact(name)) => format!("= {name}"), Some(RegExp(name)) => format!("~ {name}"), }).into() } } impl HasJack<'static> for MidiInput { fn jack (&self) -> &Jack<'static> { &self.jack } } impl JackPort for MidiInput { type Port = MidiIn; type Pair = MidiOut; fn port_name (&self) -> &Arc { &self.name } fn port (&self) -> &Port { &self.port } fn port_mut (&mut self) -> &mut Port { &mut self.port } fn into_port (self) -> Port { self.port } fn connections (&self) -> &[Connect] { self.connections.as_slice() } fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) -> Usually where Self: Sized { let port = Self { port: Self::register(jack, name)?, jack: jack.clone(), name: name.as_ref().into(), connections: connect.to_vec(), held: Arc::new(RwLock::new([false;128])) }; port.connect_to_matching()?; Ok(port) } } impl MidiInput { pub fn parsed <'a> (&'a self, scope: &'a ProcessScope) -> impl Iterator, &'a [u8])> { parse_midi_input(self.port().iter(scope)) } } impl>> HasMidiIns for T { fn midi_ins (&self) -> &Vec { self.get() } fn midi_ins_mut (&mut self) -> &mut Vec { self.get_mut() } } impl> AddMidiIn for T { fn midi_in_add (&mut self) -> Usually<()> { let index = self.midi_ins().len(); let port = MidiInput::new(self.jack(), &format!("M/{index}"), &[])?; self.midi_ins_mut().push(port); Ok(()) } } impl HasJack<'static> for MidiOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } impl JackPort for MidiOutput { type Port = MidiOut; type Pair = MidiIn; fn port_name (&self) -> &Arc { &self.name } fn port (&self) -> &Port { &self.port } fn port_mut (&mut self) -> &mut Port { &mut self.port } fn into_port (self) -> Port { self.port } fn connections (&self) -> &[Connect] { self.connections.as_slice() } fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) -> Usually where Self: Sized { let port = Self::register(jack, name)?; let jack = jack.clone(); let name = name.as_ref().into(); let connections = connect.to_vec(); let port = Self { jack, port, name, connections, held: Arc::new([false;128].into()), note_buffer: vec![0;8], output_buffer: vec![vec![];65536], }; port.connect_to_matching()?; Ok(port) } } impl MidiOutput { /// Clear the section of the output buffer that we will be using, /// emitting "all notes off" at start of buffer if requested. pub fn buffer_clear (&mut self, scope: &ProcessScope, reset: bool) { let n_frames = (scope.n_frames() as usize).min(self.output_buffer.len()); for frame in &mut self.output_buffer[0..n_frames] { frame.clear(); } if reset { all_notes_off(&mut self.output_buffer); } } /// Write a note to the output buffer pub fn buffer_write <'a> ( &'a mut self, sample: usize, event: LiveEvent, ) { self.note_buffer.fill(0); event.write(&mut self.note_buffer).expect("failed to serialize MIDI event"); self.output_buffer[sample].push(self.note_buffer.clone()); // Update the list of currently held notes. if let LiveEvent::Midi { ref message, .. } = event { update_keys(&mut*self.held.write().unwrap(), message); } } /// Write a chunk of MIDI data from the output buffer to the output port. pub fn buffer_emit (&mut self, scope: &ProcessScope) { let samples = scope.n_frames() as usize; let mut writer = self.port.writer(scope); for (time, events) in self.output_buffer.iter().enumerate().take(samples) { for bytes in events.iter() { writer.write(&RawMidi { time: time as u32, bytes }).unwrap_or_else(|_|{ panic!("Failed to write MIDI data: {bytes:?}"); }); } } } } impl>> HasMidiOuts for T { fn midi_outs (&self) -> &Vec { self.get() } fn midi_outs_mut (&mut self) -> &mut Vec { self.get_mut() } } /// Trail for thing that may gain new MIDI ports. impl> AddMidiOut for T { fn midi_out_add (&mut self) -> Usually<()> { let index = self.midi_outs().len(); let port = MidiOutput::new(self.jack(), &format!("{index}/M"), &[])?; self.midi_outs_mut().push(port); Ok(()) } } impl HasJack<'static> for AudioInput { fn jack (&self) -> &Jack<'static> { &self.jack } } impl JackPort for AudioInput { type Port = AudioIn; type Pair = AudioOut; fn port_name (&self) -> &Arc { &self.name } fn port (&self) -> &Port { &self.port } fn port_mut (&mut self) -> &mut Port { &mut self.port } fn into_port (self) -> Port { self.port } fn connections (&self) -> &[Connect] { self.connections.as_slice() } fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) -> Usually where Self: Sized { let port = Self { port: Self::register(jack, name)?, jack: jack.clone(), name: name.as_ref().into(), connections: connect.to_vec() }; port.connect_to_matching()?; Ok(port) } } impl HasJack<'static> for AudioOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } impl JackPort for AudioOutput { type Port = AudioOut; type Pair = AudioIn; fn port_name (&self) -> &Arc { &self.name } fn port (&self) -> &Port { &self.port } fn port_mut (&mut self) -> &mut Port { &mut self.port } fn into_port (self) -> Port { self.port } fn connections (&self) -> &[Connect] { self.connections.as_slice() } fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) -> Usually where Self: Sized { let port = Self { port: Self::register(jack, name)?, jack: jack.clone(), name: name.as_ref().into(), connections: connect.to_vec() }; port.connect_to_matching()?; Ok(port) } } impl Draw for AddSampleModal { fn draw (&self, _to: &mut TuiOut) { todo!() //let area = to.area(); //to.make_dim(); //let area = center_box( //area, //64.max(area.w().saturating_sub(8)), //20.max(area.w().saturating_sub(8)), //); //to.fill_fg(area, Color::Reset); //to.fill_bg(area, Nord::bg_lo(true, true)); //to.fill_char(area, ' '); //to.blit(&format!("{}", &self.dir.to_string_lossy()), area.x()+2, area.y()+1, Some(Style::default().bold()))?; //to.blit(&"Select sample:", area.x()+2, area.y()+2, Some(Style::default().bold()))?; //for (i, (is_dir, name)) in self.subdirs.iter() //.map(|path|(true, path)) //.chain(self.files.iter().map(|path|(false, path))) //.enumerate() //.skip(self.offset) //{ //if i >= area.h() as usize - 4 { //break //} //let t = if is_dir { "" } else { "" }; //let line = format!("{t} {}", name.to_string_lossy()); //let line = &line[..line.len().min(area.w() as usize - 4)]; //to.blit(&line, area.x() + 2, area.y() + 3 + i as u16, Some(if i == self.cursor { //Style::default().green() //} else { //Style::default().white() //}))?; //} //Lozenge(Style::default()).draw(to) } } impl Sampler { pub fn view_grid (&self) -> impl Content + use<'_> { //let cells_x = 8u16; //let cells_y = 8u16; //let cell_width = 10u16; //let cell_height = 2u16; //let width = cells_x * cell_width; //let height = cells_y * cell_height; //let cols = Map::east( //cell_width, //move||0..cells_x, //move|x, _|Map::south( //cell_height, //move||0..cells_y, //move|y, _|self.view_grid_cell("........", x, y, cell_width, cell_height) //) //); //cols //Thunk::new(|to: &mut TuiOut|{ //}) "TODO" } pub fn view_grid_cell <'a> ( &'a self, name: &'a str, x: u16, y: u16, w: u16, h: u16 ) -> impl Content + use<'a> { let cursor = self.cursor(); let hi_fg = Color::Rgb(64, 64, 64); let hi_bg = if y == 0 { Color::Reset } else { Color::Rgb(64, 64, 64) /*prev*/ }; let tx_fg = if let Some((index, _)) = self.recording && index % 8 == x as usize && index / 8 == y as usize { Color::Rgb(255, 64, 0) } else { Color::Rgb(255, 255, 255) }; let tx_bg = if x as usize == cursor.0 && y as usize == cursor.1 { Color::Rgb(96, 96, 96) } else { Color::Rgb(64, 64, 64) }; let lo_fg = Color::Rgb(64, 64, 64); let lo_bg = if y == 7 { Color::Reset } else { tx_bg }; Fixed::XY(w, h, Bsp::s( Fixed::Y(1, Tui::fg_bg(hi_fg, hi_bg, Repeat::X(Phat::<()>::LO))), Bsp::n( Fixed::Y(1, Tui::fg_bg(lo_fg, lo_bg, Repeat::X(Phat::<()>::HI))), Fill::X(Fixed::Y(1, Tui::fg_bg(tx_fg, tx_bg, name))), ), )) } const _EMPTY: &[(f64, f64)] = &[(0., 0.), (1., 1.), (2., 2.), (0., 2.), (2., 0.)]; pub fn view_list <'a, T: NotePoint + NoteRange> ( &'a self, compact: bool, editor: &T ) -> impl Content + 'a { let note_lo = editor.get_note_lo(); let note_pt = editor.get_note_pos(); let note_hi = editor.get_note_hi(); Fixed::X(if compact { 4 } else { 12 }, Map::south( 1, move||(note_lo..=note_hi).rev(), move|note, _index| { //let offset = |a|Push::y(i as u16, Align::n(Fixed::Y(1, Fill::X(a)))); let mut bg = if note == note_pt { Tui::g(64) } else { Color::Reset }; let mut fg = Tui::g(160); if let Some(mapped) = self.samples.get(note) { let sample = mapped.read().unwrap(); fg = if note == note_pt { sample.color.lightest.rgb } else { Tui::g(224) }; bg = if note == note_pt { sample.color.light.rgb } else { sample.color.base.rgb }; } if let Some((index, _)) = self.recording { if note == index { bg = if note == note_pt { Color::Rgb(96,24,0) } else { Color::Rgb(64,16,0) }; fg = Color::Rgb(224,64,32) } } Tui::fg_bg(fg, bg, format!("{note:3} {}", self.view_list_item(note, compact))) })) } pub fn view_list_item (&self, note: usize, compact: bool) -> String { if compact { String::default() } else { draw_list_item(&self.samples.get(note)) } } pub fn view_sample (&self, note_pt: usize) -> impl Content + use<'_> { Outer(true, Style::default().fg(Tui::g(96))) .enclose(Fill::XY(draw_viewer(if let Some((_, Some(sample))) = &self.recording { Some(sample) } else if let Some(sample) = &self.samples.get(note_pt) { Some(sample) } else { None }))) } pub fn view_sample_info (&self, note_pt: usize) -> impl Content + use<'_> { Fill::X(Fixed::Y(1, draw_info(if let Some((_, Some(sample))) = &self.recording { Some(sample) } else if let Some(sample) = &self.samples.get(note_pt) { Some(sample) } else { None }))) } pub fn view_sample_status (&self, note_pt: usize) -> impl Content + use<'_> { Fixed::X(20, draw_info_v(if let Some((_, Some(sample))) = &self.recording { Some(sample) } else if let Some(sample) = &self.samples.get(note_pt) { Some(sample) } else { None })) } pub fn view_status (&self, index: usize) -> impl Content { draw_status(self.samples.get(index).as_ref()) } pub fn view_meters_input (&self) -> impl Content + use<'_> { draw_meters(&self.input_meters) } pub fn view_meters_output (&self) -> impl Content + use<'_> { draw_meters(&self.output_meters) } } fn draw_meters (meters: &[f32]) -> impl Content + use<'_> { Tui::bg(Black, Fixed::X(2, Map::east(1, ||meters.iter(), |value, _index|{ Fill::Y(RmsMeter(*value)) }))) } fn draw_list_item (sample: &Option>>) -> String { if let Some(sample) = sample { let sample = sample.read().unwrap(); format!("{:8}", sample.name) //format!("{:8} {:3} {:6}-{:6}/{:6}", //sample.name, //sample.gain, //sample.start, //sample.end, //sample.channels[0].len() //) } else { String::from("........") } } fn draw_viewer (sample: Option<&Arc>>) -> impl Content + use<'_> { let min_db = -64.0; Thunk::new(move|to: &mut TuiOut|{ let XYWH(x, y, width, height) = to.area(); let area = Rect { x, y, width, height }; if let Some(sample) = &sample { let sample = sample.read().unwrap(); let start = sample.start as f64; let end = sample.end as f64; let length = end - start; let step = length / width as f64; let mut t = start; let mut lines = vec![]; while t < end { let chunk = &sample.channels[0][t as usize..((t + step) as usize).min(sample.end)]; let total: f32 = chunk.iter().map(|x|x.abs()).sum(); let count = chunk.len() as f32; let meter = 10. * (total / count).log10(); let x = t as f64; let y = meter as f64; lines.push(Line::new(x, min_db, x, y, Color::Green)); t += step / 2.; } Canvas::default() .x_bounds([sample.start as f64, sample.end as f64]) .y_bounds([min_db, 0.]) .paint(|ctx| { for line in lines.iter() { ctx.draw(line); } //FIXME: proportions //let text = "press record to finish sampling"; //ctx.print( //(width - text.len() as u16) as f64 / 2.0, //height as f64 / 2.0, //text.red() //); }).render(area, &mut to.buffer); } else { Canvas::default() .x_bounds([0.0, width as f64]) .y_bounds([0.0, height as f64]) .paint(|_ctx| { //let text = "press record to begin sampling"; //ctx.print( //(width - text.len() as u16) as f64 / 2.0, //height as f64 / 2.0, //text.red() //); }) .render(area, &mut to.buffer); } }) } impl Sampler { fn sample_selected (&self) -> usize { (self.get_note_pos() as u8).into() } fn sample_selected_pitch (&self) -> u7 { (self.get_note_pos() as u8).into() } } impl AddSampleModal { fn exited (&self) -> bool { self.exited } fn exit (&mut self) { self.exited = true } } impl AddSampleModal { pub fn new ( sample: &Arc>, voices: &Arc>> ) -> Usually { let dir = std::env::current_dir()?; let (subdirs, files) = scan(&dir)?; Ok(Self { exited: false, dir, subdirs, files, cursor: 0, offset: 0, sample: sample.clone(), voices: voices.clone(), _search: None }) } fn rescan (&mut self) -> Usually<()> { scan(&self.dir).map(|(subdirs, files)|{ self.subdirs = subdirs; self.files = files; }) } fn prev (&mut self) { self.cursor = self.cursor.saturating_sub(1); } fn next (&mut self) { self.cursor = self.cursor + 1; } fn try_preview (&mut self) -> Usually<()> { if let Some(path) = self.cursor_file() { if let Ok(sample) = Sample::from_file(&path) { *self.sample.write().unwrap() = sample; self.voices.write().unwrap().push( Sample::play(&self.sample, 0, &u7::from(100u8)) ); } //load_sample(&path)?; //let src = std::fs::File::open(&path)?; //let mss = MediaSourceStream::new(Box::new(src), Default::default()); //let mut hint = Hint::new(); //if let Some(ext) = path.extension() { //hint.with_extension(&ext.to_string_lossy()); //} //let meta_opts: MetadataOptions = Default::default(); //let fmt_opts: FormatOptions = Default::default(); //if let Ok(mut probed) = symphonia::default::get_probe() //.format(&hint, mss, &fmt_opts, &meta_opts) //{ //panic!("{:?}", probed.format.metadata()); //}; } Ok(()) } fn cursor_dir (&self) -> Option { if self.cursor < self.subdirs.len() { Some(self.dir.join(&self.subdirs[self.cursor])) } else { None } } fn cursor_file (&self) -> Option { if self.cursor < self.subdirs.len() { return None } let index = self.cursor.saturating_sub(self.subdirs.len()); if index < self.files.len() { Some(self.dir.join(&self.files[index])) } else { None } } fn pick (&mut self) -> Usually { if self.cursor == 0 { if let Some(parent) = self.dir.parent() { self.dir = parent.into(); self.rescan()?; self.cursor = 0; return Ok(false) } } if let Some(dir) = self.cursor_dir() { self.dir = dir; self.rescan()?; self.cursor = 0; return Ok(false) } if let Some(path) = self.cursor_file() { let (end, channels) = read_sample_data(&path.to_string_lossy())?; let mut sample = self.sample.write().unwrap(); sample.name = path.file_name().unwrap().to_string_lossy().into(); sample.end = end; sample.channels = channels; return Ok(true) } return Ok(false) } } impl Sampler { pub fn process_audio_in (&mut self, scope: &ProcessScope) { self.reset_input_meters(); if self.recording.is_some() { self.record_into(scope); } else { self.update_input_meters(scope); } } /// Make sure that input meter count corresponds to input channel count fn reset_input_meters (&mut self) { let channels = self.audio_ins.len(); if self.input_meters.len() != channels { self.input_meters = vec![f32::MIN;channels]; } } /// Record from inputs to sample fn record_into (&mut self, scope: &ProcessScope) { if let Some(ref sample) = self.recording.as_ref().expect("no recording sample").1 { let mut sample = sample.write().unwrap(); if sample.channels.len() != self.audio_ins.len() { panic!("channel count mismatch"); } let samples_with_meters = self.audio_ins.iter() .zip(self.input_meters.iter_mut()) .zip(sample.channels.iter_mut()); let mut length = 0; for ((input, meter), channel) in samples_with_meters { let slice = input.port().as_slice(scope); length = length.max(slice.len()); *meter = to_rms(slice); channel.extend_from_slice(slice); } sample.end += length; } else { panic!("tried to record into the void") } } /// Update input meters fn update_input_meters (&mut self, scope: &ProcessScope) { for (input, meter) in self.audio_ins.iter().zip(self.input_meters.iter_mut()) { let slice = input.port().as_slice(scope); *meter = to_rms(slice); } } /// Make sure that output meter count corresponds to input channel count fn reset_output_meters (&mut self) { let channels = self.audio_outs.len(); if self.output_meters.len() != channels { self.output_meters = vec![f32::MIN;channels]; } } /// Mix all currently playing samples into the output. pub fn process_audio_out (&mut self, scope: &ProcessScope) { self.clear_output_buffer(); self.populate_output_buffer(scope.n_frames() as usize); self.write_output_buffer(scope); } /// Zero the output buffer. fn clear_output_buffer (&mut self) { for buffer in self.buffer.iter_mut() { buffer.fill(0.0); } } /// Write playing voices to output buffer fn populate_output_buffer (&mut self, frames: usize) { let Sampler { buffer, voices, output_gain, mixing_mode, .. } = self; let channel_count = buffer.len(); match mixing_mode { MixingMode::Summing => voices.write().unwrap().retain_mut(|voice|{ mix_summing(buffer.as_mut_slice(), *output_gain, frames, ||voice.next()) }), MixingMode::Average => voices.write().unwrap().retain_mut(|voice|{ mix_average(buffer.as_mut_slice(), *output_gain, frames, ||voice.next()) }), } } /// Write output buffer to output ports. fn write_output_buffer (&mut self, scope: &ProcessScope) { let Sampler { audio_outs, buffer, .. } = self; for (i, port) in audio_outs.iter_mut().enumerate() { let buffer = &buffer[i]; for (i, value) in port.port_mut().as_mut_slice(scope).iter_mut().enumerate() { *value = *buffer.get(i).unwrap_or(&0.0); } } } } impl Iterator for Voice { type Item = [f32;2]; fn next (&mut self) -> Option { if self.after > 0 { self.after -= 1; return Some([0.0, 0.0]) } let sample = self.sample.read().unwrap(); if self.position < sample.end { let position = self.position; self.position += 1; return sample.channels[0].get(position).map(|_amplitude|[ sample.channels[0][position] * self.velocity * sample.gain, sample.channels[0][position] * self.velocity * sample.gain, ]) } None } } impl Sampler { pub fn new ( jack: &Jack<'static>, name: impl AsRef, #[cfg(feature = "port")] midi_from: &[Connect], #[cfg(feature = "port")] audio_from: &[&[Connect];2], #[cfg(feature = "port")] audio_to: &[&[Connect];2], ) -> Usually { let name = name.as_ref(); Ok(Self { name: name.into(), input_meters: vec![0.0;2], output_meters: vec![0.0;2], output_gain: 1., buffer: vec![vec![0.0;16384];2], #[cfg(feature = "port")] midi_in: Some( MidiInput::new(jack, &format!("M/{name}"), midi_from)? ), #[cfg(feature = "port")] audio_ins: vec![ AudioInput::new(jack, &format!("L/{name}"), audio_from[0])?, AudioInput::new(jack, &format!("R/{name}"), audio_from[1])?, ], #[cfg(feature = "port")] audio_outs: vec![ AudioOutput::new(jack, &format!("{name}/L"), audio_to[0])?, AudioOutput::new(jack, &format!("{name}/R"), audio_to[1])?, ], ..Default::default() }) } /// Value of cursor pub fn cursor (&self) -> (usize, usize) { (self.cursor.0.load(Relaxed), self.cursor.1.load(Relaxed)) } } impl NoteRange for Sampler { fn note_lo (&self) -> &AtomicUsize { &self.note_lo } fn note_axis (&self) -> &AtomicUsize { &self.size.y } } impl NotePoint for Sampler { fn note_len (&self) -> &AtomicUsize { unreachable!(); } fn get_note_len (&self) -> usize { 0 } fn set_note_len (&self, x: usize) -> usize { 0 /*TODO?*/ } fn note_pos (&self) -> &AtomicUsize { &self.note_pt } fn get_note_pos (&self) -> usize { self.note_pt.load(Relaxed) } fn set_note_pos (&self, x: usize) -> usize { let old = self.note_pt.swap(x, Relaxed); self.cursor.0.store(x % 8, Relaxed); self.cursor.1.store(x / 8, Relaxed); old } } impl Sample { pub fn new (name: impl AsRef, start: usize, end: usize, channels: Vec>) -> Self { Self { name: name.as_ref().into(), start, end, channels, rate: None, gain: 1.0, color: ItemTheme::random(), } } pub fn play (sample: &Arc>, after: usize, velocity: &u7) -> Voice { Voice { sample: sample.clone(), after, position: sample.read().unwrap().start, velocity: velocity.as_int() as f32 / 127.0, } } } impl Sample { /// Read WAV from file pub fn read_data (src: &str) -> Usually<(usize, Vec>)> { let mut channels: Vec> = vec![]; for channel in wavers::Wav::from_path(src)?.channels() { channels.push(channel); } let mut end = 0; let mut data: Vec> = vec![]; for samples in channels.iter() { let channel = Vec::from(samples.as_ref()); end = end.max(channel.len()); data.push(channel); } Ok((end, data)) } pub fn from_file (path: &PathBuf) -> Usually { let name = path.file_name().unwrap().to_string_lossy().into(); let mut sample = Self { name, ..Default::default() }; // Use file extension if present let mut hint = Hint::new(); if let Some(ext) = path.extension() { hint.with_extension(&ext.to_string_lossy()); } let probed = symphonia::default::get_probe().format( &hint, MediaSourceStream::new( Box::new(File::open(path)?), Default::default(), ), &Default::default(), &Default::default() )?; let mut format = probed.format; let params = &format.tracks().iter() .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) .expect("no tracks found") .codec_params; let mut decoder = get_codecs().make(params, &Default::default())?; loop { match format.next_packet() { Ok(packet) => sample.decode_packet(&mut decoder, packet)?, Err(symphonia::core::errors::Error::IoError(_)) => break decoder.last_decoded(), Err(err) => return Err(err.into()), }; }; sample.end = sample.channels.iter().fold(0, |l, c|l + c.len()); Ok(sample) } fn decode_packet ( &mut self, decoder: &mut Box, packet: Packet ) -> Usually<()> { // Decode a packet let decoded = decoder .decode(&packet) .map_err(|e|Box::::from(e))?; // Determine sample rate let spec = *decoded.spec(); if let Some(rate) = self.rate { if rate != spec.rate as usize { panic!("sample rate changed"); } } else { self.rate = Some(spec.rate as usize); } // Determine channel count while self.channels.len() < spec.channels.count() { self.channels.push(vec![]); } // Load sample let mut samples = SampleBuffer::new( decoded.frames() as u64, spec ); if samples.capacity() > 0 { samples.copy_interleaved_ref(decoded); for frame in samples.samples().chunks(spec.channels.count()) { for (chan, frame) in frame.iter().enumerate() { self.channels[chan].push(*frame) } } } Ok(()) } } fn sampler_jack_process ( state: &mut Sampler, _: &Client, scope: &ProcessScope ) -> Control { if let Some(midi_in) = &state.midi_in { for midi in midi_in.port().iter(scope) { sampler_midi_in(&state.samples, &state.voices, midi) } } state.process_audio_out(scope); state.process_audio_in(scope); Control::Continue } /// Create [Voice]s from [Sample]s in response to MIDI input. fn sampler_midi_in ( samples: &SampleKit<128>, voices: &Arc>>, RawMidi { time, bytes }: RawMidi ) { if let Ok(LiveEvent::Midi { message, .. }) = LiveEvent::parse(bytes) { match message { MidiMessage::NoteOn { ref key, ref vel } => { if let Some(sample) = samples.get(key.as_int() as usize) { voices.write().unwrap().push(Sample::play(sample, time as usize, vel)); } }, MidiMessage::Controller { controller: _, value: _ } => { // TODO } _ => {} } } } impl Sample { pub fn handle_cc (&mut self, controller: u7, value: u7) { let percentage = value.as_int() as f64 / 127.; match controller.as_int() { 20 => { self.start = (percentage * self.end as f64) as usize; }, 21 => { let length = self.channels[0].len(); self.end = length.min( self.start + (percentage * (length as f64 - self.start as f64)) as usize ); }, 22 => { /*attack*/ }, 23 => { /*decay*/ }, 24 => { self.gain = percentage as f32 * 2.0; }, 26 => { /* pan */ } 25 => { /* pitch */ } _ => {} } } } impl> + Send + Sync> HasScenes for T {} impl HasSceneScroll for Arrangement { fn scene_scroll (&self) -> usize { self.scene_scroll } } impl AddScene for T {} impl Scene { fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } fn _todo_usize_stub_ (&self) -> usize { todo!() } fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } } impl> + Send + Sync> HasScene for T {} impl Scene { /// Returns the pulse length of the longest clip in the scene pub fn pulses (&self) -> usize { self.clips.iter().fold(0, |a, p|{ a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0)) }) } /// Returns true if all clips in the scene are /// currently playing on the given collection of tracks. pub fn is_playing (&self, tracks: &[Track]) -> bool { self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate() .all(|(track_index, clip)|match clip { Some(c) => tracks .get(track_index) .map(|track|{ if let Some((_, Some(clip))) = track.sequencer().play_clip() { *clip.read().unwrap() == *c.read().unwrap() } else { false } }) .unwrap_or(false), None => true }) } pub fn clip (&self, index: usize) -> Option<&Arc>> { match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None } } } impl> HasSelection for T {} #[cfg(feature = "track")] impl Selection { pub fn track (&self) -> Option { use Selection::*; if let Track(track)|TrackClip{track,..}|TrackInput{track,..}|TrackOutput{track,..}|TrackDevice{track,..} = self { Some(*track) } else { None } } pub fn select_track (&self, track_count: usize) -> Self { use Selection::*; match self { Mix => Track(0), Scene(_) => Mix, Track(t) => Track((t + 1) % track_count), TrackClip { track, .. } => Track(*track), _ => todo!(), } } pub fn select_track_next (&self, len: usize) -> Self { use Selection::*; match self { Mix => Track(0), Scene(s) => TrackClip { track: 0, scene: *s }, Track(t) => if t + 1 < len { Track(t + 1) } else { Mix }, TrackClip {track, scene} => if track + 1 < len { TrackClip { track: track + 1, scene: *scene } } else { Scene(*scene) }, _ => todo!() } } pub fn select_track_prev (&self) -> Self { use Selection::*; match self { Mix => Mix, Scene(s) => Scene(*s), Track(0) => Mix, Track(t) => Track(t - 1), TrackClip { track: 0, scene } => Scene(*scene), TrackClip { track: t, scene } => TrackClip { track: t - 1, scene: *scene }, _ => todo!() } } } #[cfg(feature = "scene")] impl Selection { pub fn scene (&self) -> Option { use Selection::*; match self { Scene(scene) | TrackClip { scene, .. } => Some(*scene), _ => None } } pub fn select_scene (&self, scene_count: usize) -> Self { use Selection::*; match self { Mix | Track(_) => Scene(0), Scene(s) => Scene((s + 1) % scene_count), TrackClip { scene, .. } => Track(*scene), _ => todo!(), } } pub fn select_scene_next (&self, len: usize) -> Self { use Selection::*; match self { Mix => Scene(0), Track(t) => TrackClip { track: *t, scene: 0 }, Scene(s) => if s + 1 < len { Scene(s + 1) } else { Mix }, TrackClip { track, scene } => if scene + 1 < len { TrackClip { track: *track, scene: scene + 1 } } else { Track(*track) }, _ => todo!() } } pub fn select_scene_prev (&self) -> Self { use Selection::*; match self { Mix | Scene(0) => Mix, Scene(s) => Scene(s - 1), Track(t) => Track(*t), TrackClip { track, scene: 0 } => Track(*track), TrackClip { track, scene } => TrackClip { track: *track, scene: scene - 1 }, _ => todo!() } } } impl Selection { pub fn describe ( &self, #[cfg(feature = "track")] tracks: &[Track], #[cfg(feature = "scene")] scenes: &[Scene], ) -> Arc { use Selection::*; format!("{}", match self { Mix => "Everything".to_string(), #[cfg(feature = "scene")] Scene(s) => scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name)).unwrap_or_else(||"S??".into()), #[cfg(feature = "track")] Track(t) => tracks.get(*t).map(|track|format!("T{t}: {}", &track.name)).unwrap_or_else(||"T??".into()), TrackClip { track, scene } => match (tracks.get(*track), scenes.get(*scene)) { (Some(_), Some(s)) => match s.clip(*track) { Some(clip) => format!("T{track} S{scene} C{}", &clip.read().unwrap().name), None => format!("T{track} S{scene}: Empty") }, _ => format!("T{track} S{scene}: Empty"), }, _ => todo!() }).into() } } impl> HasSequencer for T { fn sequencer (&self) -> &Sequencer { self.get() } fn sequencer_mut (&mut self) -> &mut Sequencer { self.get_mut() } } impl Default for Sequencer { fn default () -> Self { Self { #[cfg(feature = "clock")] clock: Clock::default(), #[cfg(feature = "clip")] play_clip: None, #[cfg(feature = "clip")] next_clip: None, #[cfg(feature = "port")] midi_ins: vec![], #[cfg(feature = "port")] midi_outs: vec![], recording: false, monitoring: true, overdub: false, notes_in: RwLock::new([false;128]).into(), notes_out: RwLock::new([false;128]).into(), note_buf: vec![0;8], midi_buf: vec![], reset: true, } } } impl Sequencer { pub fn new ( name: impl AsRef, jack: &Jack<'static>, #[cfg(feature = "clock")] clock: Option<&Clock>, #[cfg(feature = "clip")] clip: Option<&Arc>>, #[cfg(feature = "port")] midi_from: &[Connect], #[cfg(feature = "port")] midi_to: &[Connect], ) -> Usually { let _name = name.as_ref(); #[cfg(feature = "clock")] let clock = clock.cloned().unwrap_or_default(); Ok(Self { reset: true, notes_in: RwLock::new([false;128]).into(), notes_out: RwLock::new([false;128]).into(), #[cfg(feature = "port")] midi_ins: vec![MidiInput::new(jack, &format!("M/{}", name.as_ref()), midi_from)?,], #[cfg(feature = "port")] midi_outs: vec![MidiOutput::new(jack, &format!("{}/M", name.as_ref()), midi_to)?, ], #[cfg(feature = "clip")] play_clip: clip.map(|clip|(Moment::zero(&clock.timebase), Some(clip.clone()))), #[cfg(feature = "clock")] clock, ..Default::default() }) } } impl std::fmt::Debug for Sequencer { fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.debug_struct("Sequencer") .field("clock", &self.clock) .field("play_clip", &self.play_clip) .field("next_clip", &self.next_clip) .finish() } } #[cfg(feature = "clock")] has!(Clock: |self:Sequencer|self.clock); #[cfg(feature = "port")] has!(Vec: |self:Sequencer|self.midi_ins); #[cfg(feature = "port")] has!(Vec: |self:Sequencer|self.midi_outs); impl MidiMonitor for Sequencer { fn notes_in (&self) -> &Arc> { &self.notes_in } fn monitoring (&self) -> bool { self.monitoring } fn monitoring_mut (&mut self) -> &mut bool { &mut self.monitoring } } impl MidiRecord for Sequencer { fn recording (&self) -> bool { self.recording } fn recording_mut (&mut self) -> &mut bool { &mut self.recording } fn overdub (&self) -> bool { self.overdub } fn overdub_mut (&mut self) -> &mut bool { &mut self.overdub } } #[cfg(feature="clip")] impl HasPlayClip for Sequencer { fn reset (&self) -> bool { self.reset } fn reset_mut (&mut self) -> &mut bool { &mut self.reset } fn play_clip (&self) -> &Option<(Moment, Option>>)> { &self.play_clip } fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)> { &mut self.play_clip } fn next_clip (&self) -> &Option<(Moment, Option>>)> { &self.next_clip } fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)> { &mut self.next_clip } } /// JACK process callback for a sequencer's clip sequencer/recorder. impl Audio for Sequencer { fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { if self.clock().is_rolling() { self.process_rolling(scope) } else { self.process_stopped(scope) } } } impl Sequencer { fn process_rolling (&mut self, scope: &ProcessScope) -> Control { self.process_clear(scope, false); // Write chunk of clip to output, handle switchover if self.process_playback(scope) { self.process_switchover(scope); } // Monitor input to output self.process_monitoring(scope); // Record and/or monitor input self.process_recording(scope); // Emit contents of MIDI buffers to JACK MIDI output ports. self.midi_outs_emit(scope); Control::Continue } fn process_stopped (&mut self, scope: &ProcessScope) -> Control { if self.monitoring() && self.midi_ins().len() > 0 && self.midi_outs().len() > 0 { self.process_monitoring(scope) } Control::Continue } fn process_monitoring (&mut self, scope: &ProcessScope) { let notes_in = self.notes_in().clone(); // For highlighting keys and note repeat let monitoring = self.monitoring(); for input in self.midi_ins.iter() { for (sample, event, bytes) in input.parsed(scope) { if let LiveEvent::Midi { message, .. } = event { if monitoring { self.midi_buf[sample].push(bytes.to_vec()); } // FIXME: don't lock on every event! update_keys(&mut notes_in.write().unwrap(), &message); } } } } /// Clear the section of the output buffer that we will be using, /// emitting "all notes off" at start of buffer if requested. fn process_clear (&mut self, scope: &ProcessScope, reset: bool) { let n_frames = (scope.n_frames() as usize).min(self.midi_buf_mut().len()); for frame in &mut self.midi_buf_mut()[0..n_frames] { frame.clear(); } if reset { all_notes_off(self.midi_buf_mut()); } for port in self.midi_outs_mut().iter_mut() { // Clear output buffer(s) port.buffer_clear(scope, false); } } fn process_recording (&mut self, scope: &ProcessScope) { if self.monitoring() { self.monitor(scope); } if let Some((started, ref clip)) = self.play_clip.clone() { self.record_clip(scope, started, clip); } if let Some((_start_at, _clip)) = &self.next_clip() { self.record_next(); } } fn process_playback (&mut self, scope: &ProcessScope) -> bool { // If a clip is playing, write a chunk of MIDI events from it to the output buffer. // If no clip is playing, prepare for switchover immediately. if let Some((started, clip)) = &self.play_clip { // Length of clip, to repeat or stop on end. let length = clip.as_ref().map_or(0, |p|p.read().unwrap().length); // Index of first sample to populate. let offset = self.clock().get_sample_offset(scope, &started); // Write MIDI events from clip at sample offsets corresponding to pulses. for (sample, pulse) in self.clock().get_pulses(scope, offset) { // If a next clip is enqueued, and we're past the end of the current one, // break the loop here (FIXME count pulse correctly) let past_end = if clip.is_some() { pulse >= length } else { true }; // Is it time for switchover? if self.next_clip().is_some() && past_end { return true } // If there's a currently playing clip, output notes from it to buffer: if let Some(clip) = clip { // Source clip from which the MIDI events will be taken. let clip = clip.read().unwrap(); // Clip with zero length is not processed if clip.length > 0 { // Current pulse index in source clip let pulse = pulse % clip.length; // Output each MIDI event from clip at appropriate frames of output buffer: for message in clip.notes[pulse].iter() { for port in self.midi_outs.iter_mut() { port.buffer_write(sample, LiveEvent::Midi { channel: 0.into(), /* TODO */ message: *message }); } } } } } false } else { true } } /// Handle switchover from current to next playing clip. fn process_switchover (&mut self, scope: &ProcessScope) { let midi_buf = self.midi_buf_mut(); let sample0 = scope.last_frame_time() as usize; //let samples = scope.n_frames() as usize; if let Some((start_at, clip)) = &self.next_clip() { let start = start_at.sample.get() as usize; let sample = self.clock().started.read().unwrap() .as_ref().unwrap().sample.get() as usize; // If it's time to switch to the next clip: if start <= sample0.saturating_sub(sample) { // Samples elapsed since clip was supposed to start let _skipped = sample0 - start; // Switch over to enqueued clip let started = Moment::from_sample(self.clock().timebase(), start as f64); // Launch enqueued clip *self.play_clip_mut() = Some((started, clip.clone())); // Unset enqueuement (TODO: where to implement looping?) *self.next_clip_mut() = None; // Fill in remaining ticks of chunk from next clip. self.process_playback(scope); } } } } impl HasMidiBuffers for Sequencer { fn note_buf_mut (&mut self) -> &mut Vec { &mut self.note_buf } fn midi_buf_mut (&mut self) -> &mut Vec>> { &mut self.midi_buf } } impl> + Send + Sync> HasTracks for T {} impl HasTrackScroll for Arrangement { fn track_scroll (&self) -> usize { self.track_scroll } } impl> HasTrack for T { fn track (&self) -> Option<&Track> { self.get() } fn track_mut (&mut self) -> Option<&mut Track> { self.get_mut() } } has!(Clock: |self: Track|self.sequencer.clock); has!(Sequencer: |self: Track|self.sequencer); impl Track { /// Create a new track with only the default [Sequencer]. pub fn new ( name: &impl AsRef, color: Option, jack: &Jack<'static>, clock: Option<&Clock>, clip: Option<&Arc>>, midi_from: &[Connect], midi_to: &[Connect], ) -> Usually { Ok(Self { name: name.as_ref().into(), color: color.unwrap_or_default(), sequencer: Sequencer::new(format!("{}/sequencer", name.as_ref()), jack, clock, clip, midi_from, midi_to)?, ..Default::default() }) } pub fn audio_ins (&self) -> &[AudioInput] { self.devices.first().map(|x|x.audio_ins()).unwrap_or_default() } pub fn audio_outs (&self) -> &[AudioOutput] { self.devices.last().map(|x|x.audio_outs()).unwrap_or_default() } fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } fn _todo_usize_stub_ (&self) -> usize { todo!() } fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } } impl HasWidth for Track { const MIN_WIDTH: usize = 9; fn width_inc (&mut self) { self.width += 1; } fn width_dec (&mut self) { if self.width > Track::MIN_WIDTH { self.width -= 1; } } } #[cfg(feature = "sampler")] impl Track { /// Create a new track connecting the [Sequencer] to a [Sampler]. pub fn new_with_sampler ( name: &impl AsRef, color: Option, jack: &Jack<'static>, clock: Option<&Clock>, clip: Option<&Arc>>, midi_from: &[Connect], midi_to: &[Connect], audio_from: &[&[Connect];2], audio_to: &[&[Connect];2], ) -> Usually { let mut track = Self::new(name, color, jack, clock, clip, midi_from, midi_to)?; let client_name = jack.with_client(|c|c.name().to_string()); let port_name = track.sequencer.midi_outs[0].port_name(); let connect = [Connect::exact(format!("{client_name}:{}", port_name))]; track.devices.push(Device::Sampler(Sampler::new( jack, &format!("{}/sampler", name.as_ref()), &connect, audio_from, audio_to )?)); Ok(track) } pub fn sampler (&self, mut nth: usize) -> Option<&Sampler> { for device in self.devices.iter() { match device { Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; }, _ => {} } } None } pub fn sampler_mut (&mut self, mut nth: usize) -> Option<&mut Sampler> { for device in self.devices.iter_mut() { match device { Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; }, _ => {} } } None } } impl ClipsView for T {} impl Track { pub fn per <'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 { 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))))}) } } impl SampleKit { fn get (&self, index: usize) -> &Option>> { if index < self.0.len() { &self.0[index] } else { &None } } } impl Default for SampleKit { fn default () -> Self { Self([const { None }; N]) } }