diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 00000000..fc86e571 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,626 @@ +use crate::*; +pub mod bind; pub use self::bind::*; +pub mod cli; pub use self::cli::*; +pub mod config; pub use self::config::*; +pub mod view; pub use self::view::*; + +/// Total application state. +/// +/// ``` +/// use tek::{HasTracks, HasScenes, TracksView, ScenesView}; +/// let mut app = tek::App::default(); +/// let _ = app.scene_add(None, None).unwrap(); +/// let _ = app.update_clock(); +/// app.project.editor = Some(Default::default()); +/// //let _: Vec<_> = app.project.inputs_with_sizes().collect(); +/// //let _: Vec<_> = app.project.outputs_with_sizes().collect(); +/// let _: Vec<_> = app.project.tracks_with_sizes().collect(); +/// //let _: Vec<_> = app.project.scenes_with_sizes(true, 10, 10).collect(); +/// //let _: Vec<_> = app.scenes_with_colors(true, 10).collect(); +/// //let _: Vec<_> = app.scenes_with_track_colors(true, 10, 10).collect(); +/// let _ = app.project.w(); +/// //let _ = app.project.w_sidebar(); +/// //let _ = app.project.w_tracks_area(); +/// let _ = app.project.h(); +/// //let _ = app.project.h_tracks_area(); +/// //let _ = app.project.h_inputs(); +/// //let _ = app.project.h_outputs(); +/// let _ = app.project.h_scenes(); +/// ``` +#[derive(Default, Debug)] pub struct App { + /// Base color. + pub color: ItemTheme, + /// Must not be dropped for the duration of the process + pub jack: Jack<'static>, + /// Display size + pub size: Size, + /// Performance counter + pub perf: PerfModel, + /// Available view modes and input bindings + pub config: Config, + /// Currently selected mode + pub mode: Arc>>, + /// Undo history + pub history: Vec<(AppCommand, Option)>, + /// Dialog overlay + pub dialog: Dialog, + /// Contains all recently created clips. + pub pool: Pool, + /// Contains the currently edited musical arrangement + pub project: Arrangement, + /// Error, if any + pub error: Arc>>> +} + +impl App { + + /// Create a new application instance from a backend, project, config, and mode + /// + /// ``` + /// let jack = tek::tengri::Jack::new(&"test_tek").expect("failed to connect to jack"); + /// let proj = tek::Arrangement::default(); + /// let mut conf = tek::Config::default(); + /// conf.add("(mode hello)"); + /// let tek = tek::tek(&jack, proj, conf, "hello"); + /// ``` + pub fn new ( + jack: &Jack<'static>, project: Arrangement, config: Config, mode: impl AsRef + ) -> Self { + let mode: &str = mode.as_ref(); + App { + color: ItemTheme::random(), + dialog: Dialog::welcome(), + jack: jack.clone(), + mode: config.get_mode(mode).expect(&format!("failed to find mode '{mode}'")), + config, + project, + ..Default::default() + } + } + + pub fn new_shared ( + jack: &Jack<'static>, project: Arrangement, config: Config, mode: impl AsRef + ) -> Arc> { + Arc::new(RwLock::new(App::new(jack, ":menu", config, project))) + } + + pub fn confirm (&mut self) -> Perhaps { + Ok(match &self.dialog { + Dialog::Menu(index, items) => { + let callback = items.0[*index].1.clone(); + callback(self)?; + None + }, + _ => todo!(), + }) + } + + pub fn inc (&mut self, axis: &ControlAxis) -> Perhaps { + Ok(match (&self.dialog, axis) { + (Dialog::None, _) => todo!(), + (Dialog::Menu(_, _), ControlAxis::Y) => + AppCommand::SetDialog { dialog: self.dialog.menu_next() }.act(self)?, + _ => todo!() + }) + } + + pub fn dec (&mut self, axis: &ControlAxis) -> Perhaps { + Ok(match (&self.dialog, axis) { + (Dialog::None, _) => None, + (Dialog::Menu(_, _), ControlAxis::Y) => + AppCommand::SetDialog { dialog: self.dialog.menu_prev() }.act(self)?, + _ => todo!() + }) + } + + pub fn jack_process ( + &mut self, client: &Client, scope: &ProcessScope + ) -> Control { + let t0 = self.perf.get_t0(); + self.clock().update_from_scope(scope).unwrap(); + let midi_in = self.project.midi_input_collect(scope); + if let Some(editor) = &self.editor() { + let mut pitch: Option = None; + for port in midi_in.iter() { + for event in port.iter() { + if let (_, Ok(LiveEvent::Midi {message: MidiMessage::NoteOn {key, ..}, ..})) + = event + { + pitch = Some(key.clone()); + } + } + } + if let Some(pitch) = pitch { + editor.set_note_pos(pitch.as_int() as usize); + } + } + let result = self.project.process_tracks(client, scope); + self.perf.update_from_jack_scope(t0, scope); + result + } + + pub fn jack_event ( + &mut self, event: JackEvent + ) { + use JackEvent::*; + match event { + SampleRate(sr) => { self.clock().timebase.sr.set(sr as f64); }, + PortRegistration(_id, true) => { + //let port = self.jack().port_by_id(id); + //println!("\rport add: {id} {port:?}"); + //println!("\rport add: {id}"); + }, + PortRegistration(_id, false) => { + /*println!("\rport del: {id}")*/ + }, + PortsConnected(_a, _b, true) => { /*println!("\rport conn: {a} {b}")*/ }, + PortsConnected(_a, _b, false) => { /*println!("\rport disc: {a} {b}")*/ }, + ClientRegistration(_id, true) => {}, + ClientRegistration(_id, false) => {}, + ThreadInit => {}, + XRun => {}, + GraphReorder => {}, + _ => { panic!("{event:?}"); } + } + } + + /// Update memoized render of clock values. + /// ``` + /// tek::App::default().update_clock(); + /// ``` + pub fn update_clock (&self) { + ClockView::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80) + } + + /// Set modal dialog. + /// + /// ``` + /// let previous: tek::Dialog = tek::App::default().set_dialog(tek::Dialog::welcome()); + /// ``` + pub fn set_dialog (&mut self, mut dialog: Dialog) -> Dialog { + std::mem::swap(&mut self.dialog, &mut dialog); + dialog + } + + /// FIXME: generalize. Set picked device in device pick dialog. + /// + /// ``` + /// tek::App::default().device_pick(0); + /// ``` + pub fn device_pick (&mut self, index: usize) { + self.dialog = Dialog::Device(index); + } + + /// FIXME: generalize. Add device to current track. + 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. + /// + /// ``` + /// assert_eq!(tek::App::default().browser(), None); + /// ``` + pub fn browser (&self) -> Option<&Browse> { + if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None } + } + + /// Is a MIDI editor currently focused? + /// + /// ``` + /// tek::App::default().editor_focused(); + /// ``` + pub fn editor_focused (&self) -> bool { + false + } + + /// Toggle MIDI editor. + /// + /// ``` + /// tek::App::default().toggle_editor(None); + /// ``` + 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, 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()); + } + } + } +} + +/// The [Draw] implementation for [App] handles the loaded view, +/// which is defined in terms of [dizzle] DSL. +/// +/// If there is an error, the error is displayed. FIXME: overlay it +/// Then, every top-level form of the DSL description is rendered. +impl Draw for App { + fn draw (self, to: &mut Tui) -> Usually> { + let xywh = to.area().into(); + if let Some(e) = self.error.read().unwrap().as_ref() { + to.show(xywh, e.as_ref()); + } + for (index, dsl) in self.mode.view.iter().enumerate() { + if let Err(e) = self.interpret(to, dsl) { + *self.error.write().unwrap() = Some(format!("view #{index}: {e}").into()); + break; + } + } + Ok(xywh) + } +} + +impl HasClipsSize for App { + fn clips_size (&self) -> &Size { &self.project.size_inner } +} + +impl HasJack<'static> for App { + fn jack (&self) -> &Jack<'static> { &self.jack } +} + +//impl_handle!(TuiIn: |self: App, input|{ + //let commands = tek_collect_commands(self, input)?; + //let history = tek_execute_commands(self, commands)?; + //self.history.extend(history.into_iter()); + //Ok(None) +//}); + +//fn tek_collect_commands (app: &App, input: &TuiIn) -> Usually> { + //let mut commands = vec![]; + //for id in app.mode.keys.iter() { + //if let Some(event_map) = app.config.binds.clone().read().unwrap().get(id.as_ref()) + //&& let Some(bindings) = event_map.query(input.event()) { + //for binding in bindings { + //for command in binding.commands.iter() { + //if let Some(command) = app.namespace(command)? as Option { + //commands.push(command) + //} + //} + //} + //} + //} + //Ok(commands) +//} + +//fn tek_execute_commands ( + //app: &mut App, commands: Vec +//) -> Usually)>> { + //let mut history = vec![]; + //for command in commands.into_iter() { + //let result = command.act(app); + //match result { Err(err) => { history.push((command, None)); return Err(err) } + //Ok(undo) => { history.push((command, undo)); } }; + //} + //Ok(history) +//} + +def_command!(AppCommand: |app: App| { + Nop => Ok(None), + Confirm => tek_confirm(app), + Cancel => todo!(), // TODO delegate: + Inc { axis: ControlAxis } => tek_inc(app, axis), + Dec { axis: ControlAxis } => tek_dec(app, axis), + SetDialog { dialog: Dialog } => { + swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog }) + }, +}); + +impl_default!(AppCommand: Self::Nop); + +impl_has!(Clock: |self: App|self.project.clock); +impl_has!(Vec: |self: App|self.project.midi_ins); +impl_has!(Vec: |self: App|self.project.midi_outs); +impl_has!(Dialog: |self: App|self.dialog); +impl_has!(Jack<'static>: |self: App|self.jack); +impl_has!(Size: |self: App|self.size); +impl_has!(Pool: |self: App|self.pool); +impl_has!(Selection: |self: App|self.project.selection); + +impl_as_ref!(Vec: |self: App|self.project.as_ref()); +impl_as_mut!(Vec: |self: App|self.project.as_mut()); + +impl_as_ref_opt!(MidiEditor: |self: App|self.project.as_ref_opt()); +impl_as_mut_opt!(MidiEditor: |self: App|self.project.as_mut_opt()); + +impl_has_clips!( |self: App|self.pool.clips); + +impl_audio!(App: tek_jack_process, tek_jack_event); + +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.size.h() 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), }; }); + +namespace!(App: bool { symbol = |app| { // Provide boolean values. + ":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), +}; }); + +namespace!(App: ItemTheme {}); // TODO: provide colors here + // +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 + } + }; +}); + +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 Interpret for App { + fn interpret_expr <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Usually<()> { + app_interpret_expr(self, to, lang) + } + fn interpret_word <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Usually<()> { + app_interpret_word(self, to, lang) + } +} + +fn app_interpret_expr (state: &App, to: &mut Tui, lang: &impl Expression) -> Usually<()> { + if eval_view(state, to, lang)? || eval_view_tui(state, to, lang)? { + Ok(()) + } else { + Err(format!("App::interpret_expr: unexpected: {lang:?}").into()) + } +} + +fn app_interpret_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usually<()> { + let mut frags = dsl.src()?.unwrap().split("/"); + match frags.next() { + + Some(":logo") => view_logo().draw(to), + + Some(":status") => h_exact(1, "TODO: Status Bar").draw(to), + + Some(":meters") => match frags.next() { + Some("input") => bg(Rgb(30, 30, 30), h_full(origin_s("Input Meters"))).draw(to), + Some("output") => bg(Rgb(30, 30, 30), h_full(origin_s("Output Meters"))).draw(to), + _ => panic!() + }, + + Some(":tracks") => match frags.next() { + None => "TODO tracks".draw(to), + Some("names") => state.project.view_track_names(state.color.clone()).draw(to),//bg(Rgb(40, 40, 40), w_full(origin_w("Track Names")))), + Some("inputs") => bg(Rgb(40, 40, 40), w_full(origin_w("Track Inputs"))).draw(to), + Some("devices") => bg(Rgb(40, 40, 40), w_full(origin_w("Track Devices"))).draw(to), + Some("outputs") => bg(Rgb(40, 40, 40), w_full(origin_w("Track Outputs"))).draw(to), + _ => panic!() + }, + + Some(":scenes") => match frags.next() { + None => "TODO scenes".draw(to), + Some(":scenes/names") => "TODO Scene Names".draw(to), + _ => panic!() + }, + + Some(":editor") => "TODO Editor".draw(to), + + Some(":dialog") => match frags.next() { + Some("menu") => if let Dialog::Menu(selected, items) = &state.dialog { + let items = items.clone(); + let selected = selected; + Some(wh_full(thunk(move|to: &mut Tui|{ + for (index, MenuItem(item, _)) in items.0.iter().enumerate() { + to.place(&y_push((2 * index) as u16, + 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) }, + h_exact(2, origin_n(w_full(item))) + ))); + } + }))) + } else { + None + }.draw(to), + _ => unimplemented!("App::interpret_word: {dsl:?} ({frags:?})"), + }, + + Some(":templates") => { + let modes = state.config.modes.clone(); + let height = (modes.read().unwrap().len() * 2) as u16; + h_exact(height, w_min(Some(30), thunk(move |to: &mut Tui|{ + 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 = w_full(origin_w(fg(fg1, name))); + let field_id = w_full(origin_e(fg(fg2, id))); + let field_info = w_full(origin_w(info)); + y_push((2 * index) as u16, + h_exact(2, w_full(bg(bg, south( + above(field_name, field_id), field_info))))).draw(to); + } + }))) + }.draw(to), + + Some(":sessions") => h_exact(Some(6), w_min(30, thunk(|to: &mut Tui|{ + 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) }; + y_push((2 * index) as u16, + &h_exact(2, w_full(bg(bg, origin_w(fg(fg, name)))))).draw(to); + } + }))).draw(to), + + Some(":browse/title") => w_full(origin_w(field_v(ItemColor::default(), + match state.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:", + }, w_shrink(3, h_exact(1, fg(g(96), x_repeat("🭻"))))))).draw(to), + + Some(":device") => { + let selected = state.dialog.device_kind().unwrap(); + south(bold(true, "Add device"), iter_south( + move||device_kinds().iter(), + move|_label: &&'static str, i|{ + let b = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }; + let l = if i == selected { "[ " } else { " " }; + let r = if i == selected { " ]" } else { " " }; + w_full(bg(b, east(l, west(r, "FIXME device name")))) })).draw(to) + }, + + Some(":debug") => h_exact(1, format!("[{:?}]", to.area())).draw(to), + + Some(_) => { + let views = state.config.views.read().unwrap(); + if let Some(dsl) = views.get(dsl.src()?.unwrap()) { + let dsl = dsl.clone(); + std::mem::drop(views); + state.interpret(to, &dsl)? + } else { + unimplemented!("{dsl:?}"); + } + }, + + _ => unreachable!() + } + Ok(()) +} + +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 ScenesView for App { + fn w_mid (&self) -> u16 { + (self.size.w() as u16).saturating_sub(self.w_side()) + } + fn w_side (&self) -> u16 { + 20 + } + fn h_scenes (&self) -> u16 { + (self.size.h() as u16).saturating_sub(20) + } +} diff --git a/src/bind.rs b/src/app/bind.rs similarity index 98% rename from src/bind.rs rename to src/app/bind.rs index a8eaae7d..be5a5705 100644 --- a/src/bind.rs +++ b/src/app/bind.rs @@ -61,7 +61,7 @@ impl Bind> { // TODO Ok(()) } else if let Ok(Some(_word)) = item.expr().head().word() { - if let Some(key) = TuiEvent::from_dsl(item.expr()?.head()?)? { + if let Some(key) = TuiEvent::named(item.expr()?.head()?)? { map.add(key, Binding { commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(), condition: None, diff --git a/src/cli.rs b/src/app/cli.rs similarity index 63% rename from src/cli.rs rename to src/app/cli.rs index 4373c13f..99e0b53e 100644 --- a/src/cli.rs +++ b/src/app/cli.rs @@ -1,4 +1,4 @@ -use crate::{*, arrange::*, clock::*, config::*, device::*}; +use crate::*; /// The command-line interface descriptor. /// @@ -61,7 +61,7 @@ use crate::{*, arrange::*, clock::*, config::*, device::*}; #[arg(short='L', long)] left_to: Vec, /// Audio ins to connect from right output #[arg(short='R', long)] right_to: Vec, - /// Tracks to create + /// Tracks to creat #[arg(short='t', long)] tracks: Option, /// Scenes to create #[arg(short='s', long)] scenes: Option, @@ -75,96 +75,103 @@ use crate::{*, arrange::*, clock::*, config::*, device::*}; } /// Command-line configuration. -#[cfg(feature = "cli")] impl Cli { +#[cfg(feature = "cli")] +impl Cli { pub fn run (&self) -> Usually<()> { - if let Action::Version = self.action { - return Ok(tek_show_version()) - } + self.action.run(&Config::init_new(None)?) + } +} - 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)?; - - // 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![], - Connect::collect(&midi_from, &[] as &[&str], &midi_from_re).iter().enumerate() - .map(|(index, connect)|jack.midi_in(&format!("M/{index}"), &[connect.clone()])) - .collect::, _>>()?, - Connect::collect(&midi_to, &[] as &[&str], &midi_to_re).iter().enumerate() - .map(|(index, connect)|jack.midi_out(&format!("{index}/M"), &[connect.clone()])) - .collect::, _>>()? - ); - 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(()) +#[cfg(feature = "cli")] +impl Action { + fn run (&self, config: &Config) -> Usually<()> { + use Action::*; + match self { + Version => tek_show_version(), + Config => tek_print_config(&config), + List => todo!("list sessions"), + Resume => todo!("resume session"), + New { + name, bpm, tracks, scenes, sync_lead, sync_follow, + midi_from: mf, midi_from_re: mfr, midi_to: mt, midi_to_re: mtr, + left_from: lf, right_from: rf, left_to: lt, right_to: rt, .. + } => { + let name = name.as_ref().map_or("tek", |x|x.as_str()); + let jack = Jack::new(&name)?; + let proj = tek_project_new( + &jack, Clock::new(&jack, *bpm)?, &lf, <, &rf, &rt, &mf, &mt, &mfr, &mtr + )?; + proj.tracks_add(tracks.unwrap_or(0), None, &[], &[])?; + proj.scenes_add(scenes.unwrap_or(0))?; + //if matches!(self, Action::Status) { + //// Show status and exit + //tek_print_status(&proj); + //return Ok(()) + //} + // Initialize the app state + let app = tek(&jack, proj, config, ":menu"); + //if matches!(self, Action::Headless) { + //// TODO: Headless mode (daemon + client over IPC, then over network...) + //println!("todo headless"); + //return Ok(()) + //} + // Run the [Tui] and [Jack] threads with the [App] state. + Tui::new(Box::new(std::io::stdout()))?.run(true, &jack.run(move|jack|{ + // Between jack init and app's first cycle: + 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)?; + // FIXME: They don't work properly. + Ok(app) + })?)?; } - - // 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"); - return Ok(()) - } - - // Run the [Tui] and [Jack] threads with the [App] state. - Tui::new(Box::new(std::io::stdout()))?.run(true, &jack.run(move|jack|{ - - // Between jack init and app's first cycle: - - 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)?; - - // FIXME: They don't work properly. - - Ok(app) - - })?)?; } Ok(()) } } +pub fn tek_project_new ( + jack: &Jack, + clock: Clock, + left_from: &[impl AsRef], + left_to: &[impl AsRef], + right_from: &[impl AsRef], + right_to: &[impl AsRef], + midi_from: &[impl AsRef], + midi_to: &[impl AsRef], + midi_from_re: &[impl AsRef], + midi_to_re: &[impl AsRef], +) -> Usually { + // 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 mut project = Arrangement::new( + jack, + None, + clock, + vec![], + vec![], + Connect::collect(&midi_from, &[] as &[&str], &midi_from_re).iter().enumerate() + .map(|(index, connect)|jack.midi_in(&format!("M/{index}"), &[connect.clone()])) + .collect::, _>>()?, + Connect::collect(&midi_to, &[] as &[&str], &midi_to_re).iter().enumerate() + .map(|(index, connect)|jack.midi_out(&format!("{index}/M"), &[connect.clone()])) + .collect::, _>>()? + ); + Ok(project) +} + pub fn tek_show_version () { println!("todo version"); } diff --git a/src/app/config.rs b/src/app/config.rs new file mode 100644 index 00000000..cceefff8 --- /dev/null +++ b/src/app/config.rs @@ -0,0 +1,192 @@ +use crate::{*, bind::*, view::*}; + +/// Configuration: mode, view, and bind definitions. +/// +/// ``` +/// let config = tek::Config::default(); +/// ``` +/// +/// ``` +/// // Some dizzle. +/// // What indentation to use here lol? +/// let source = stringify!((mode :menu (name Menu) +/// (info Mode selector.) (keys :axis/y :confirm) +/// (view (bg (g 0) (bsp/s :ports/out +/// (bsp/n :ports/in +/// (bg (g 30) (bsp/s (fixed/y 7 :logo) +/// (fill :dialog/menu))))))))); +/// // Add this definition to the config and try to load it. +/// // A "mode" is basically a state machine +/// // with associated input and output definitions. +/// tek::Config::default().add(&source).unwrap().get_mode(":menu").unwrap(); +/// ``` +#[derive(Default, Debug)] pub struct Config { + /// XDG base directories of running user. + pub dirs: BaseDirectories, + /// Active collection of interaction modes. + pub modes: Modes, + /// Active collection of event bindings. + pub binds: Binds, + /// Active collection of view definitions. + pub views: Views, +} + +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"); + pub fn init_new (dirs: Option) -> Usually { + let mut config = Self::new(None); + config.init()?; + Ok(config) + } + /// 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() + } +} + +pub(crate) fn load_mode (modes: &Modes, name: &impl AsRef, body: &impl Language) -> Usually<()> { + let mut mode = Mode::default(); + body.each(|item|mode.add(item))?; + modes.write().unwrap().insert(name.as_ref().into(), Arc::new(mode)); + Ok(()) +} + +/// Collection of interaction modes. +pub type Modes = Arc, Arc>>>>>; + +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()); + })) + } +} + +/// Group of view and keys definitions. +/// +/// ``` +/// let mode = tek::Mode::>::default(); +/// ``` +#[derive(Default, Debug)] pub struct Mode { + pub path: PathBuf, + pub name: Vec, + pub info: Vec, + pub view: Vec, + pub keys: Vec, + pub modes: Modes, +} diff --git a/src/app/view.rs b/src/app/view.rs new file mode 100644 index 00000000..f5e2852e --- /dev/null +++ b/src/app/view.rs @@ -0,0 +1,590 @@ +use crate::*; + +/// Collection of custom view definitions. +pub type Views = Arc, Arc>>>; + +/// Load custom view definition. +pub(crate) fn load_view (views: &Views, name: &impl AsRef, body: &impl Language) -> Usually<()> { + views.write().unwrap().insert(name.as_ref().into(), body.src()?.unwrap_or_default().into()); + Ok(()) +} + +/// ``` +/// let _ = tek::view_logo(); +/// ``` +pub fn view_logo () -> impl Draw { + wh_exact(Some(32), Some(7), bold(true, fg(Rgb(240, 200, 180), south!{ + h_exact(1, ""), + h_exact(1, ""), + h_exact(1, "~~ ╓─β•₯─╖ ╓──╖ β•₯ β•– ~~~~~~~~~~~~"), + h_exact(1, east("~~~~ β•‘ ~ β•Ÿβ”€β•Œ ~β•Ÿβ”€< ~~ ", east(fg(Rgb(230,100,40), "v0.3.0"), " ~~"))), + h_exact(1, "~~~~ ╨ ~ β•™β”€β”€β•œ ╨ β•œ ~~~~~~~~~~~~"), + }))) +} + +/// ``` +/// let x = std::sync::Arc::>::default(); +/// let _ = tek::view_transport(true, x.clone(), x.clone(), x.clone()); +/// let _ = tek::view_transport(false, x.clone(), x.clone(), x.clone()); +/// ``` +pub fn view_transport (play: bool, bpm: &str, beat: &str, time: &str) -> impl Draw { + let theme = ItemTheme::G[96]; + bg(Black, east!(above( + wh_full(origin_w(button_play_pause(play))), + wh_full(origin_e(east!( + field_h(theme, "BPM", bpm), + field_h(theme, "Beat", beat), + field_h(theme, "Time", time), + ))) + ))) +} + +/// ``` +/// let x = std::sync::Arc::>::default(); +/// let _ = tek::view_status(None, x.clone(), x.clone(), x.clone()); +/// let _ = tek::view_status(Some("".into()), x.clone(), x.clone(), x.clone()); +/// ``` +pub fn view_status (sel: Option<&str>, sr: &str, buf: &str, lat: &str) -> impl Draw { + let theme = ItemTheme::G[96]; + let sr = field_h(theme, "SR", sr); + let buf = field_h(theme, "Buf", buf); + let lat = field_h(theme, "Lat", lat); + bg(Black, east!(above( + wh_full(origin_w(sel.map(|sel|field_h(theme, "Selected", sel)))), + wh_full(origin_e(east!(sr, buf, lat))), + ))) +} + +/// ``` +/// let _ = tek::button_play_pause(true); +/// ``` +pub fn button_play_pause (playing: bool) -> impl Draw { + let compact = true;//self.is_editing(); + bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, + either(compact, + thunk(move|to: &mut Tui|w_exact(9, either(playing, + fg(Rgb(0, 255, 0), " PLAYING "), + fg(Rgb(255, 128, 0), " STOPPED ")) + ).draw(to)), + thunk(move|to: &mut Tui|w_exact(5, either(playing, + fg(Rgb(0, 255, 0), south(" πŸ­πŸ­‘πŸ¬½ ", " 🭞🭜🭘 ",)), + fg(Rgb(255, 128, 0), south(" β–—β–„β–– ", " β–β–€β–˜ ",))) + ).draw(to)), + ) + ) +} + +#[cfg(feature = "track")] pub fn view_track_row_section ( + _theme: ItemTheme, + button: impl Draw, + button_add: impl Draw, + content: impl Draw, +) -> impl Draw { + west(h_full(w_exact(4, origin_nw(button_add))), + east(w_exact(20, h_full(origin_nw(button))), wh_full(origin_c(content)))) +} + +/// ``` +/// let bg = tengri::ratatui::style::Color::Red; +/// let fg = tengri::ratatui::style::Color::Green; +/// let _ = tek::view_wrap(bg, fg, "and then blue, too!"); +/// ``` +pub fn view_wrap (bg: Color, fg: Color, content: impl Draw) -> impl Draw { + let left = fg_bg(bg, Reset, w_exact(1, y_repeat("▐"))); + let right = fg_bg(bg, Reset, w_exact(1, y_repeat("β–Œ"))); + east(left, west(right, fg_bg(fg, bg, content))) +} + +/// ``` +/// let _ = tek::view_meter("", 0.0); +/// let _ = tek::view_meters(&[0.0, 0.0]); +/// ``` +pub fn view_meter <'a> (label: &'a str, value: f32) -> impl Draw + 'a { + let f = field_h(ItemTheme::G[128], label, format!("{:>+9.3}", value)); + let w = if value >= 0.0 { 13 } + else if value >= -1.0 { 12 } + else if value >= -2.0 { 11 } + else if value >= -3.0 { 10 } + else if value >= -4.0 { 9 } + else if value >= -6.0 { 8 } + else if value >= -9.0 { 7 } + else if value >= -12.0 { 6 } + else if value >= -15.0 { 5 } + else if value >= -20.0 { 4 } + else if value >= -25.0 { 3 } + else if value >= -30.0 { 2 } + else if value >= -40.0 { 1 } + else { 0 }; + let c = if value >= 0.0 { Red } + else if value >= -3.0 { Yellow } + else { Green }; + south!(f, wh_exact(Some(w), Some(1), bg(c, ()))) +} + +pub fn view_meters (values: &[f32;2]) -> impl Draw + use<'_> { + let left = format!("L/{:>+9.3}", values[0]); + let right = format!("R/{:>+9.3}", values[1]); + south(left, right) +} + +pub fn view_sample_info (sample: Option<&Arc>>) -> impl Draw + use<'_> { + when(sample.is_some(), thunk(move|to: &mut Tui|{ + let sample = sample.unwrap().read().unwrap(); + let theme = sample.color; + east!( + field_h(theme, "Name", format!("{:<10}", sample.name.clone())), + field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())), + field_h(theme, "Start", format!("{:<8}", sample.start)), + field_h(theme, "End", format!("{:<8}", sample.end)), + field_h(theme, "Trans", "0"), + field_h(theme, "Gain", format!("{}", sample.gain)), + ).draw(to) + })) +} + +pub fn view_sample_info_v (sample: Option<&Arc>>) -> impl Draw + use<'_> { + let a = thunk(move|to: &mut Tui|{ + let sample = sample.unwrap().read().unwrap(); + let theme = sample.color; + w_exact(20, south!( + w_full(origin_w(field_h(theme, "Name ", format!("{:<10}", sample.name.clone())))), + w_full(origin_w(field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())))), + w_full(origin_w(field_h(theme, "Start ", format!("{:<8}", sample.start)))), + w_full(origin_w(field_h(theme, "End ", format!("{:<8}", sample.end)))), + w_full(origin_w(field_h(theme, "Trans ", "0"))), + w_full(origin_w(field_h(theme, "Gain ", format!("{}", sample.gain)))), + )).draw(to) + }); + + let b = thunk(|to: &mut Tui|fg(Red, south!( + bold(true, "Γ— No sample."), + "[r] record", + "[Shift-F9] import", + )).draw(to)); + + either(sample.is_some(), a, b) +} + +pub fn view_sample_status (sample: Option<&Arc>>) -> impl Draw { + bold(true, fg(g(224), sample + .map(|sample|{ + let sample = sample.read().unwrap(); + format!("Sample {}-{}", sample.start, sample.end) + }) + .unwrap_or_else(||"No sample".to_string()))) +} + +pub fn view_track_header (theme: ItemTheme, content: impl Draw) -> impl Draw { + w_exact(12, bg(theme.darker.term, w_full(origin_e(content)))) +} + +pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, ports: &'a [T]) + -> impl Draw + use<'a, T> +{ + let ins = ports.len() as u16; + let frame = Outer(true, Style::default().fg(g(96))); + let iter = move||ports.iter(); + let names = iter_south(iter, move|port, index|h_full(origin_w(format!(" {index} {}", port.port_name())))); + let field = field_v(theme, title, names); + wh_exact(Some(20), Some(1 + ins), border(true, frame, wh_exact(Some(20), Some(1 + ins), field))) +} + +pub fn view_io_ports <'a, T: PortsSizes<'a>> ( + fg: Color, bg: Color, items: impl Fn()->T + Send + Sync + 'a +) -> impl Draw + 'a { + type Item<'a> = (usize, &'a Arc, &'a [Connect], usize, usize); + iter(items, move|(_index, name, connections, y, y2): Item<'a>, _| { + y_push(y as u16, h_exact((y2-y) as u16, + south(h_full(bold(true, fg_bg(fg, bg, origin_w(east(" σ°£² ", name))))), + iter(||connections.iter(), move|connect: &'a Connect, index|y_push( + index as u16, h_exact(1, origin_w(bold(false, fg_bg(fg, bg, &connect.info)))) + ))))) + }) +} + +pub fn view_scenes_clips ( + scenes: impl ScenesSizes<'_>, + tracks: impl TracksSizes<'_>, + select: &Selection, + editor: Option<&MidiEditor>, + size: &Size, + is_editing: bool, +) -> impl Draw { + size.of(wh_full(above(wh_full(origin_se(fg(Green, format!("{}x{}", size.w(), size.h())))), + thunk(|to: &mut Tui|{ + for (index, track, _, _) in tracks { + let clips = view_track_clips(scenes, select, editor, index, track, is_editing); + w_exact(track.width as u16, h_full(clips)).draw(to); + } + Ok(XYWH(0, 0, 0, 0)) + })))) +} + +pub fn view_track_clips ( + scenes: impl ScenesSizes<'_>, + select: &Selection, + editor: &Option, + index: usize, + track: &Track, + is_editing: bool, +) -> impl Draw { + thunk(move|to: &mut Tui|{ + + for (scene_index, scene, ..) in scenes { + + let ( + name, theme + ): (Arc, ItemTheme) = if let Some(Some(clip)) = &scene.clips.get(index) { + let clip = clip.read().unwrap(); + (format!(" ⏹ {}", &clip.name).into(), clip.color) + } else { + (" ⏹ -- ".into(), ItemTheme::G[32]) + }; + + let fg = theme.lightest.term; + + let mut outline = theme.base.term; + + let bg = if select.track() == Some(index) && select.scene() == Some(scene_index) { + outline = theme.lighter.term; + theme.light.term + } else if select.track() == Some(index) || select.scene() == Some(scene_index) { + outline = theme.darkest.term; + theme.base.term + } else { + theme.dark.term + }; + + let w = if select.track() == Some(index) && let Some(editor) = editor { + (editor.size.w() as usize).max(24).max(track.width) + } else { + track.width + } as u16; + + let y = if select.scene() == Some(scene_index) && let Some(editor) = editor { + (editor.size.h() as usize).max(12) + } else { + Self::H_SCENE as usize + } as u16; + + let is_selected = is_editing && select.track() == Some(index) && select.scene() == Some(scene_index); + + wh_exact(Some(w), Some(y), below( + wh_full(Outer(true, Style::default().fg(outline))), + wh_full(below( + below( + fg_bg(outline, bg, wh_full("")), + wh_full(origin_nw(fg_bg(fg, bg, bold(true, &name)))), + ), + wh_full(when(is_selected, editor.map(|e|e.view())))))) + ).draw(to); + + } + + Ok(XYWH(0, 0, 0, 0)) + }) +} + +pub fn view_track_names ( + theme: ItemTheme, + tracks: impl TracksSizes<'_>, + track_count: usize, + scene_count: usize, + selected: &Selection, +) -> impl Draw { + let button = south( + button_3("t", "rack ", format!("{}{track_count}", selected.track() + .map(|track|format!("{track}/")).unwrap_or_default()), false), + button_3("s", "cene ", format!("{}{scene_count}", selected.scene() + .map(|scene|format!("{scene}/")).unwrap_or_default()), false)); + let button_2 = south( + button_2("T", "+", false), + button_2("S", "+", false)); + view_track_row_section(theme, button, button_2, bg(theme.darker.term, + h_exact(2, thunk(|to: &mut Tui|{ + for (index, track, x1, _x2) in tracks { + x_push(x1 as u16, w_exact(track_width(index, track), + bg(if selected.track() == Some(index) { + track.color.light.term + } else { + track.color.base.term + }, south(w_full(origin_nw(east( + format!("Β·t{index:02} "), + fg(Rgb(255, 255, 255), bold(true, &track.name)) + ))), ""))) ).draw(to); + } + Ok(XYWH(0, 0, 0, 0)) + })))) +} + +pub fn view_track_outputs ( + theme: ItemTheme, + tracks: impl TracksSizes<'_>, + midi_outs: impl Iterator, +) -> impl Draw { + view_track_row_section(theme, + south(w_full(origin_w(button_2("o", "utput", false))), + thunk(|to: &mut Tui|{ + for port in midi_outs { + w_full(origin_w(port.port_name())).draw(to); + } + Ok(XYWH(0, 0, 0, 0)) + })), + button_2("O", "+", false), + bg(theme.darker.term, origin_w(thunk(|to: &mut Tui|{ + for (index, track, _x1, _x2) in tracks { + let iter = ||track.sequencer.midi_outs.iter(); + let draw = |port: &MidiOutput, _|fg(Rgb(255, 255, 255), + h_exact(1, bg(track.color.dark.term, w_full(origin_w( + format!("Β·o{index:02} {}", port.port_name())))))); + w_exact(track_width(index, track), + origin_nw(h_full(iter_south(iter, draw)))).draw(to); + } + Ok(XYWH(0, 0, 0, 0)) + })))) +} + +pub fn view_track_inputs ( + theme: ItemTheme, + tracks: impl TracksSizes<'_>, + h: u16, +) -> impl Draw { + view_track_row_section(theme, button_2("i", "nput", false), button_2("I", "+", false), + bg(theme.darker.term, origin_w(thunk(move|to: &mut Tui|{ + for (index, track, _x1, _x2) in tracks { + wh_exact(Some(track_width(index, track)), Some(h + 1), + origin_nw(south( + bg(track.color.base.term, + w_full(origin_w(east!( + either(track.sequencer.monitoring, fg(Green, "●mon "), "Β·mon "), + either(track.sequencer.recording, fg(Red, "●rec "), "Β·rec "), + either(track.sequencer.overdub, fg(Yellow, "●dub "), "Β·dub "), + )))), + iter_south(||track.sequencer.midi_ins.iter(), + |port, _|fg_bg(Rgb(255, 255, 255), track.color.dark.term, + w_full(origin_w(format!("Β·i{index:02} {}", port.port_name())))))))) + .draw(to); + } + Ok(XYWH(0, 0, 0, 0)) + })))) +} + +pub fn view_scenes_names ( + scenes: &impl ScenesSizes<'_> +) -> impl Draw { + w_exact(20, thunk(|to: &mut Tui|{ + for (index, scene, ..) in scenes { + view_scene_name(index, scene).draw(to); + } + Ok(XYWH(1, 1, 1, 1)) + })) +} + +pub fn view_scene_name ( + select: Option<&Selection>, + editor: Option<&MidiEditor>, + index: usize, + scene: &Scene, + editing: bool +) -> impl Draw { + let h = if select.scene() == Some(index) && let Some(_editor) = editor { + 7 + } else { + Self::H_SCENE as u16 + }; + let a = w_full(origin_w(east(format!("Β·s{index:02} "), + fg(g(255), bold(true, &scene.name))))); + let b = when(select.scene() == Some(index) && editing, + wh_full(origin_nw(south( + editor.as_ref().map(|e|e.clip_status()), + editor.as_ref().map(|e|e.edit_status()))))); + let c = if select.scene() == Some(index) { + scene.color.light.term + } else { + scene.color.base.term + }; + wh_exact(Some(20), Some(h), bg(c, origin_nw(south(a, b)))) +} + +pub fn view_midi_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { + track.map(move|track|view_ports_status(theme, "MIDI ins: ", &track.sequencer.midi_ins)) +} + +pub fn view_midi_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { + track.map(move|track|view_ports_status(theme, "MIDI outs: ", &track.sequencer.midi_outs)) +} + +pub fn view_audio_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { + track.map(move|track|view_ports_status(theme, "Audio ins: ", &track.audio_ins())) +} + +pub fn view_audio_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { + track.map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs())) +} + +pub fn view_track_per ( + tracks: impl TracksSizes<'_> +) -> impl Draw { + iter_east(tracks, move|(index, track, x1, x2): (usize, &Track, usize, usize), _|{ + w_exact((x2 - x1) as u16, fg_bg( + track.color.lightest.term, + track.color.base.term, + callback(index, track))) + }) +} + +pub fn view_per_track () -> impl Draw {} + +pub fn view_per_track_top () -> impl Draw {} + +pub fn view_inputs ( + tracks: impl TracksSizes<'_>, + midi_ins: &[MidiIn], +) -> impl Draw { + let header = h_exact(1, view_inputs_header(tracks)); + south(header, thunk(|to: &mut Tui|{ + for (index, port) in midi_ins.iter().enumerate() { + x_push(index as u16 * 10, h_exact(1, view_inputs_row(tracks, port))).draw(to); + } + todo!() + })) +} + +pub fn view_inputs_header ( + tracks: impl TracksSizes<'_>, + midi_ins: &[MidiIn], +) -> impl Draw { + east(w_exact(20, origin_w(button_3("i", "nput ", format!("{}", midi_ins.len()), false))), + west(w_exact(4, button_2("I", "+", false)), thunk(move|to: &mut Tui|{ + for (_index, track, x1, _x2) in tracks { + #[cfg(feature = "track")] + x_push(x1 as u16, bg(track.color.dark.term, origin_w(w_exact(track.width as u16, east!( + either(track.sequencer.monitoring, fg(Green, "mon "), "mon "), + either(track.sequencer.recording, fg(Red, "rec "), "rec "), + either(track.sequencer.overdub, fg(Yellow, "dub "), "dub "), + ))))).draw(to); + } + todo!() + }))) +} + +pub fn view_inputs_row ( + tracks: impl TracksSizes<'_>, + port: () +) -> impl Draw { + east(w_exact(20, origin_w(east(" ● ", bold(true, fg(Rgb(255,255,255), port.port_name()))))), + west(w_exact(4, ()), thunk(move|to: &mut Tui|{ + for (_index, track, _x1, _x2) in tracks { + #[cfg(feature = "track")] + bg(track.color.darker.term, origin_w(w_exact(track.width as u16, east!( + either(track.sequencer.monitoring, fg(Green, " ● "), " Β· "), + either(track.sequencer.recording, fg(Red, " ● "), " Β· "), + either(track.sequencer.overdub, fg(Yellow, " ● "), " Β· "), + )))).draw(to); + } + todo!() + }))) +} + +pub fn view_outputs ( + theme: ItemTheme, + tracks: impl TracksSizes<'_>, + midi_outs: &[MidiOutput], + height: u16, +) -> impl Draw { + + let list = south( + h_exact(1, w_full(origin_w(button_3( + "o", "utput", format!("{}", midi_outs.len()), false + )))), + h_exact(height - 1, wh_full(origin_nw(thunk(|to: &mut Tui|{ + for (_index, port) in midi_outs.iter().enumerate() { + h_exact(1,w_full(east( + origin_w(east(" ● ", fg(Rgb(255,255,255), bold(true, port.port_name())))), + w_full(origin_e(format!("{}/{} ", + port.port().get_connections().len(), + port.connections.len())))))).draw(to); + for (index, conn) in port.connections.iter().enumerate() { + h_exact(1, w_full(origin_w(format!(" c{index:02}{}", conn.info())))) + .draw(to); + } + } + todo!(); + })))) + ); + + h_exact(height, view_track_row_section(theme, list, button_2("O", "+", false), + bg(theme.darker.term, origin_w(w_full( + thunk(|to: &mut Tui|{ + for (index, track, _x1, _x2) in tracks { + w_exact(track_width(index, track), + thunk(|to: &mut Tui|{ + h_exact(1, origin_w(east( + either(true, fg(Green, "play "), "play "), + either(false, fg(Yellow, "solo "), "solo "), + ))).draw(to); + for (_index, port) in midi_outs.iter().enumerate() { + h_exact(1, origin_w(east( + either(true, fg(Green, " ● "), " Β· "), + either(false, fg(Yellow, " ● "), " Β· "), + ))).draw(to); + for (_index, _conn) in port.connections.iter().enumerate() { + h_exact(1, w_full("")).draw(to); + } + } + todo!() + }) + ).draw(to); + } + todo!() + }) + ))) + )) +} + +pub fn view_track_devices ( + theme: ItemTheme, + tracks: &impl TracksSizes<'_>, + track: Option<&Track>, + h: u16, +) -> impl Draw { + view_track_row_section(theme, + button_3("d", "evice", format!("{}", track.map(|t|t.devices.len()).unwrap_or(0)), false), + button_2("D", "+", false), + iter(tracks, move|(_, track, _x1, _x2), index| wh_exact( + Some(track_width(index, track)), + Some(h + 1), + bg(track.color.dark.term, origin_nw(iter_south(move||0..h, + |_, _index|wh_exact(Some(track.width as u16), Some(2), + fg_bg( + ItemTheme::G[32].lightest.term, + ItemTheme::G[32].dark.term, + origin_nw(format!(" Β· {}", "--")) + ) + ))))))) +} + +pub fn per_track <'a, T: Draw + 'a, U: TracksSizes<'a>> ( + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a +) -> impl Draw + 'a { + per_track_top(tracks, move|index, track|h_full(origin_y(callback(index, track)))) +} + +pub fn per_track_top <'a, T: Draw + 'a, U: TracksSizes<'a>> ( + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a +) -> impl Draw + 'a { + origin_x(bg(Reset, iter_east(tracks, + move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ + w_exact((x2 - x1) as u16, fg_bg( + track.color.lightest.term, + track.color.base.term, + callback(index, track))) }))) +} + +pub fn field_h (theme: ItemTheme, head: impl Draw, body: impl Draw) -> impl Draw { +} + +pub fn field_v (theme: ItemTheme, head: impl Draw, body: impl Draw) -> impl Draw { +} diff --git a/src/arrange.rs b/src/arrange.rs deleted file mode 100644 index 4880e9ec..00000000 --- a/src/arrange.rs +++ /dev/null @@ -1,904 +0,0 @@ -use ::std::sync::{Arc, RwLock}; -use ::tengri::{space::east, color::ItemTheme}; -use ::tengri::{draw::*, term::*}; -use crate::{*, device::*, editor::*, sequence::*, clock::*, select::*, sample::*}; - -/// Arranger. -/// -/// ``` -/// let arranger = tek::Arrangement::default(); -/// ``` -#[derive(Default, Debug)] pub struct Arrangement { - /// Project name. - pub name: Arc, - /// Base color. - pub color: ItemTheme, - /// JACK client handle. - pub jack: Jack<'static>, - /// FIXME a render of the project arrangement, redrawn on update. - /// TODO rename to "render_cache" or smth - pub arranger: Arc>, - /// Display size - pub size: Size, - /// Display size of clips area - pub size_inner: Size, - /// Source of time - #[cfg(feature = "clock")] pub clock: Clock, - /// Allows one MIDI clip to be edited - #[cfg(feature = "editor")] pub editor: Option, - /// List of global midi inputs - #[cfg(feature = "port")] pub midi_ins: Vec, - /// List of global midi outputs - #[cfg(feature = "port")] pub midi_outs: Vec, - /// List of global audio inputs - #[cfg(feature = "port")] pub audio_ins: Vec, - /// List of global audio outputs - #[cfg(feature = "port")] pub audio_outs: Vec, - /// Selected UI element - #[cfg(feature = "select")] pub selection: Selection, - /// Last track number (to avoid duplicate port names) - #[cfg(feature = "track")] pub track_last: usize, - /// List of tracks - #[cfg(feature = "track")] pub tracks: Vec, - /// Scroll offset of tracks - #[cfg(feature = "track")] pub track_scroll: usize, - /// List of scenes - #[cfg(feature = "scene")] pub scenes: Vec, - /// Scroll offset of scenes - #[cfg(feature = "scene")] pub scene_scroll: usize, -} - -/// A track consists of a sequencer and zero or more devices chained after it. -/// -/// ``` -/// let track: tek::Track = Default::default(); -/// ``` -#[derive(Debug, Default)] pub struct Track { - /// Name of track - pub name: Arc, - /// Identifying color of track - pub color: ItemTheme, - /// Preferred width of track column - pub width: usize, - /// MIDI sequencer state - pub sequencer: Sequencer, - /// Device chain - pub devices: Vec, -} - -/// A scene consists of a set of clips to play together. -/// -/// ``` -/// let scene: tek::Scene = Default::default(); -/// let _ = scene.pulses(); -/// let _ = scene.is_playing(&[]); -/// ``` -#[derive(Debug, Default)] pub struct Scene { - /// Name of scene - pub name: Arc, - /// Identifying color of scene - pub color: ItemTheme, - /// Clips in scene, one per track - pub clips: Vec>>>, -} - -impl_has!(Jack<'static>: |self: Arrangement| self.jack); -impl HasJack<'static> for Arrangement { - fn jack (&self) -> &Jack<'static> { &self.jack } -} - -impl_has!(Size: |self: Arrangement| self.size); -impl_has!(Vec: |self: Arrangement| self.tracks); -impl_has!(Vec: |self: Arrangement| self.scenes); -impl_has!(Vec: |self: Arrangement| self.midi_ins); -impl_has!(Vec: |self: Arrangement| self.midi_outs); -impl_has!(Clock: |self: Arrangement| self.clock); -impl_has!(Selection: |self: Arrangement| self.selection); -impl_as_ref_opt!(MidiEditor: |self: Arrangement| self.editor.as_ref()); -impl_as_mut_opt!(MidiEditor: |self: Arrangement| self.editor.as_mut()); -impl_as_ref_opt!(Track: |self: Arrangement| self.selected_track()); -impl_as_mut_opt!(Track: |self: Arrangement| self.selected_track_mut()); - -impl >+AsMut>> HasScenes for T {} -impl >+AsMut>> HasTracks for T {} -impl +AsMutOpt+Send+Sync> HasScene for T {} -impl +AsMutOpt+Send+Sync> HasTrack for T {} -impl TracksView for T {} -impl ClipsView for T {} - -pub trait ClipsView: TracksView + ScenesView { - - fn view_scenes_clips <'a> (&'a self) - -> impl Draw + 'a - { - self.clips_size().of(wh_full(above( - wh_full(origin_se(fg(Green, format!("{}x{}", self.clips_size().w(), self.clips_size().h())))), - thunk(|to: &mut Tui|{ - for (track_index, track, _, _) in self.tracks_with_sizes() { - let w = track.width as u16; - let v = self.view_track_clips(track_index, track); - w_exact(w, h_full(v)).draw(to); - } - Ok(XYWH(0, 0, 0, 0)) - })))) - } - - fn view_track_clips <'a> (&'a self, track_index: usize, track: &'a Track) -> impl Draw + 'a { - thunk(move|to: &mut Tui|{ - for (scene_index, scene, ..) in self.scenes_with_sizes() { - let (name, theme): (Arc, ItemTheme) = if let Some(Some(clip)) = &scene.clips.get(track_index) { - let clip = clip.read().unwrap(); - (format!(" ⏹ {}", &clip.name).into(), clip.color) - } else { - (" ⏹ -- ".into(), ItemTheme::G[32]) - }; - let fg = theme.lightest.term; - let mut outline = theme.base.term; - let bg = if self.selection().track() == Some(track_index) - && self.selection().scene() == Some(scene_index) - { - outline = theme.lighter.term; - theme.light.term - } else if self.selection().track() == Some(track_index) - || self.selection().scene() == Some(scene_index) - { - outline = theme.darkest.term; - theme.base.term - } else { - theme.dark.term - }; - let w = if self.selection().track() == Some(track_index) - && let Some(editor) = self.editor () - { - (editor.size.w() as usize).max(24).max(track.width) - } else { - track.width - } as u16; - let y = if self.selection().scene() == Some(scene_index) - && let Some(editor) = self.editor () - { - (editor.size.h() as usize).max(12) - } else { - Self::H_SCENE as usize - } as u16; - - let is_selected = - self.selection().track() == Some(track_index) && - self.selection().scene() == Some(scene_index) && - self.is_editing(); - - wh_exact(Some(w), Some(y), below( - wh_full(Outer(true, Style::default().fg(outline))), - wh_full(below( - below( - fg_bg(outline, bg, wh_full("")), - wh_full(origin_nw(fg_bg(fg, bg, bold(true, &name)))), - ), - wh_full(when(is_selected, self.editor().map(|e|e.view()))))))).draw(to); - } - Ok(XYWH(0, 0, 0, 0)) - }) - } - -} - -pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll { - - //fn tracks_width_available (&self) -> u16 { - //(self.size.width() as u16).saturating_sub(40) - //} - - /// Iterate over tracks with their corresponding sizes. - fn tracks_with_sizes (&self) -> impl TracksSizes<'_> { - let _editor_width = self.editor().map(|e|e.size.w()); - let _active_track = self.selection().track(); - let mut x = 0; - self.tracks().iter().enumerate().map_while(move |(index, track)|{ - let width = track.width.max(8); - if x + width < self.clips_size().w() as usize { - let data = (index, track, x, x + width); - x += width + Self::TRACK_SPACING; - Some(data) - } else { - None - } - }) - } - - fn view_track_names (&self, theme: ItemTheme) -> impl Draw { - let track_count = self.tracks().len(); - let scene_count = self.scenes().len(); - let selected = self.selection(); - let button = south( - button_3("t", "rack ", format!("{}{track_count}", selected.track() - .map(|track|format!("{track}/")).unwrap_or_default()), false), - button_3("s", "cene ", format!("{}{scene_count}", selected.scene() - .map(|scene|format!("{scene}/")).unwrap_or_default()), false)); - let button_2 = south( - button_2("T", "+", false), - button_2("S", "+", false)); - view_track_row_section(theme, button, button_2, bg(theme.darker.term, - h_exact(2, thunk(|to: &mut Tui|{ - for (index, track, x1, _x2) in self.tracks_with_sizes() { - x_push(x1 as u16, w_exact(track_width(index, track), - bg(if selected.track() == Some(index) { - track.color.light.term - } else { - track.color.base.term - }, south(w_full(origin_nw(east( - format!("Β·t{index:02} "), - fg(Rgb(255, 255, 255), bold(true, &track.name)) - ))), ""))) ).draw(to); - } - Ok(XYWH(0, 0, 0, 0)) - })))) - } - - fn view_track_outputs <'a> (&'a self, theme: ItemTheme, _h: u16) -> impl Draw { - view_track_row_section(theme, - south(w_full(origin_w(button_2("o", "utput", false))), - thunk(|to: &mut Tui|{ - for port in self.midi_outs().iter() { - w_full(origin_w(port.port_name())).draw(to); - } - Ok(XYWH(0, 0, 0, 0)) - })), - button_2("O", "+", false), - bg(theme.darker.term, origin_w(thunk(|to: &mut Tui|{ - for (index, track, _x1, _x2) in self.tracks_with_sizes() { - let iter = ||track.sequencer.midi_outs.iter(); - let draw = |port: &MidiOutput, _|fg(Rgb(255, 255, 255), - h_exact(1, bg(track.color.dark.term, w_full(origin_w( - format!("Β·o{index:02} {}", port.port_name())))))); - w_exact(track_width(index, track), - origin_nw(h_full(iter_south(iter, draw)))).draw(to); - } - Ok(XYWH(0, 0, 0, 0)) - })))) - } - - fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Draw { - let mut h = 0u16; - for track in self.tracks().iter() { - h = h.max(track.sequencer.midi_ins.len() as u16); - } - let content = thunk(move|to: &mut Tui|{ - for (index, track, _x1, _x2) in self.tracks_with_sizes() { - wh_exact(Some(track_width(index, track)), Some(h + 1), - origin_nw(south( - bg(track.color.base.term, - w_full(origin_w(east!( - either(track.sequencer.monitoring, fg(Green, "●mon "), "Β·mon "), - either(track.sequencer.recording, fg(Red, "●rec "), "Β·rec "), - either(track.sequencer.overdub, fg(Yellow, "●dub "), "Β·dub "), - )))), - iter_south(||track.sequencer.midi_ins.iter(), - |port, _|fg_bg(Rgb(255, 255, 255), track.color.dark.term, - w_full(origin_w(format!("Β·i{index:02} {}", port.port_name())))))))) - .draw(to); - } - Ok(XYWH(0, 0, 0, 0)) - }); - view_track_row_section(theme, button_2("i", "nput", false), button_2("I", "+", false), - bg(theme.darker.term, origin_w(content))) - } - -} - -pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + Send + Sync { - /// Default scene height. - const H_SCENE: usize = 2; - /// Default editor height. - const H_EDITOR: usize = 15; - fn h_scenes (&self) -> u16; - fn w_side (&self) -> u16; - fn w_mid (&self) -> u16; - fn scenes_with_sizes (&self) -> impl ScenesSizes<'_> { - let mut y = 0; - self.scenes().iter().enumerate().skip(self.scene_scroll()).map_while(move|(s, scene)|{ - let height = if self.selection().scene() == Some(s) && self.editor().is_some() { - 8 - } else { - Self::H_SCENE - }; - if y + height <= self.clips_size().h() as usize { - let data = (s, scene, y, y + height); - y += height; - Some(data) - } else { - None - } - }) - } - - fn view_scenes_names (&self) -> impl Draw { - w_exact(20, thunk(|to: &mut Tui|{ - for (index, scene, ..) in self.scenes_with_sizes() { - self.view_scene_name(index, scene).draw(to); - } - Ok(XYWH(1, 1, 1, 1)) - })) - } - - fn view_scene_name <'a> (&'a self, index: usize, scene: &'a Scene) -> impl Draw + 'a { - let h = if self.selection().scene() == Some(index) && let Some(_editor) = self.editor() { - 7 - } else { - Self::H_SCENE as u16 - }; - let a = w_full(origin_w(east(format!("Β·s{index:02} "), - fg(g(255), bold(true, &scene.name))))); - let b = when(self.selection().scene() == Some(index) && self.is_editing(), - wh_full(origin_nw(south( - self.editor().as_ref().map(|e|e.clip_status()), - self.editor().as_ref().map(|e|e.edit_status()))))); - let c = if self.selection().scene() == Some(index) { - scene.color.light.term - } else { - scene.color.base.term - }; - wh_exact(Some(20), Some(h), bg(c, origin_nw(south(a, b)))) - } - -} -pub trait HasSceneScroll: HasScenes { fn scene_scroll (&self) -> usize; } -pub trait HasTrackScroll: HasTracks { fn track_scroll (&self) -> usize; } -pub trait HasScene: AsRefOpt + AsMutOpt { - fn scene_mut (&mut self) -> Option<&mut Scene> { self.as_mut_opt() } - fn scene (&self) -> Option<&Scene> { self.as_ref_opt() } -} -pub trait HasScenes: AsRef> + AsMut> { - fn scenes (&self) -> &Vec { self.as_ref() } - fn scenes_mut (&mut self) -> &mut Vec { self.as_mut() } - /// Generate the default name for a new scene - fn scene_default_name (&self) -> Arc { format!("s{:3>}", self.scenes().len() + 1).into() } - fn scene_longest_name (&self) -> usize { self.scenes().iter().map(|s|s.name.len()).fold(0, usize::max) } - /// Add multiple scenes - fn scenes_add (&mut self, n: usize) -> Usually<()> where Self: HasTracks { - let scene_color_1 = ItemColor::random(); - let scene_color_2 = ItemColor::random(); - for i in 0..n { - let _ = self.scene_add(None, Some( - scene_color_1.mix(scene_color_2, i as f32 / n as f32).into() - ))?; - } - Ok(()) - } - /// Add a scene - fn scene_add (&mut self, name: Option<&str>, color: Option) - -> Usually<(usize, &mut Scene)> where Self: HasTracks - { - let scene = Scene { - name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()), - clips: vec![None;self.tracks().len()], - color: color.unwrap_or_else(ItemTheme::random), - }; - self.scenes_mut().push(scene); - let index = self.scenes().len() - 1; - Ok((index, &mut self.scenes_mut()[index])) - } -} -pub trait HasTracks: AsRef> + AsMut> { - fn tracks (&self) -> &Vec { self.as_ref() } - fn tracks_mut (&mut self) -> &mut Vec { self.as_mut() } - /// Run audio callbacks for every track and every device - fn process_tracks (&mut self, client: &Client, scope: &ProcessScope) -> Control { - for track in self.tracks_mut().iter_mut() { - if Control::Quit == Audio::process(&mut track.sequencer, client, scope) { - return Control::Quit - } - for device in track.devices.iter_mut() { - if Control::Quit == DeviceAudio(device).process(client, scope) { - return Control::Quit - } - } - } - Control::Continue - } - fn track_longest_name (&self) -> usize { self.tracks().iter().map(|s|s.name.len()).fold(0, usize::max) } - /// Stop all playing clips - fn tracks_stop_all (&mut self) { for track in self.tracks_mut().iter_mut() { track.sequencer.enqueue_next(None); } } - /// Stop all playing clips - fn tracks_launch (&mut self, clips: Option>>>>) { - if let Some(clips) = clips { - for (clip, track) in clips.iter().zip(self.tracks_mut()) { track.sequencer.enqueue_next(clip.as_ref()); } - } else { - for track in self.tracks_mut().iter_mut() { track.sequencer.enqueue_next(None); } - } - } - /// Spacing between tracks. - const TRACK_SPACING: usize = 0; -} -pub trait HasTrack: AsRefOpt + AsMutOpt { - fn track (&self) -> Option<&Track> { self.as_ref_opt() } - fn track_mut (&mut self) -> Option<&mut Track> { self.as_mut_opt() } - #[cfg(feature = "port")] fn view_midi_ins_status <'a> (&'a self, theme: ItemTheme) -> impl Draw + 'a { - self.track().map(move|track|view_ports_status(theme, "MIDI ins: ", &track.sequencer.midi_ins)) - } - #[cfg(feature = "port")] fn view_midi_outs_status (&self, theme: ItemTheme) -> impl Draw + '_ { - self.track().map(move|track|view_ports_status(theme, "MIDI outs: ", &track.sequencer.midi_outs)) - } - #[cfg(feature = "port")] fn view_audio_ins_status (&self, theme: ItemTheme) -> impl Draw { - self.track().map(move|track|view_ports_status(theme, "Audio ins: ", &track.audio_ins())) - } - #[cfg(feature = "port")] fn view_audio_outs_status (&self, theme: ItemTheme) -> impl Draw { - self.track().map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs())) - } -} - impl_as_ref!(Vec: |self: App| self.project.as_ref()); - impl_as_mut!(Vec: |self: App| self.project.as_mut()); - #[cfg(feature = "select")] impl_as_ref_opt!(Track: |self: App| self.project.as_ref_opt()); - #[cfg(feature = "select")] impl_as_mut_opt!(Track: |self: App| self.project.as_mut_opt()); - impl HasTrackScroll for App { fn track_scroll (&self) -> usize { self.project.track_scroll() } } - impl HasTrackScroll for Arrangement { fn track_scroll (&self) -> usize { self.track_scroll } } - - 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; } } - } - - 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!() } - pub fn per <'a, T: Draw + 'a, U: TracksSizes<'a>> ( - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a - ) -> impl Draw + 'a { - iter_east(tracks, - move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ - w_exact((x2 - x1) as u16, fg_bg( - track.color.lightest.term, - track.color.base.term, - callback(index, track)))}) - } - /// Create a new track connecting the [Sequencer] to a [Sampler]. - #[cfg(feature = "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) - } - #[cfg(feature = "sampler")] 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 - } - #[cfg(feature = "sampler")] 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 - } - } - - pub fn per_track <'a, T: Draw + 'a, U: TracksSizes<'a>> ( - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a - ) -> impl Draw + 'a { - per_track_top(tracks, move|index, track|h_full(origin_y(callback(index, track)))) - } - - pub fn per_track_top <'a, T: Draw + 'a, U: TracksSizes<'a>> ( - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a - ) -> impl Draw + 'a { - origin_x(bg(Reset, iter_east(tracks, - move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ - w_exact((x2 - x1) as u16, fg_bg( - track.color.lightest.term, - track.color.base.term, - callback(index, track))) }))) - } - #[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: App| self.project.as_ref_opt()); - #[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: App| self.project.as_mut_opt()); - #[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: Arrangement| self.selected_scene()); - #[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: Arrangement| self.selected_scene_mut()); - impl HasSceneScroll for App { fn scene_scroll (&self) -> usize { self.project.scene_scroll() } } - impl HasSceneScroll for Arrangement { fn scene_scroll (&self) -> usize { self.scene_scroll } } - impl ScenesView for App { - fn w_mid (&self) -> u16 { (self.size.w() as u16).saturating_sub(self.w_side()) } - fn w_side (&self) -> u16 { 20 } - fn h_scenes (&self) -> u16 { (self.size.h() as u16).saturating_sub(20) } - } - 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 } - } - 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 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) - } - - /// Add multiple tracks - #[cfg(feature = "track")] 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 - #[cfg(feature = "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])) - } - - #[cfg(feature = "track")] pub fn view_inputs (&self, _theme: ItemTheme) -> impl Draw + '_ { - south( - h_exact(1, self.view_inputs_header()), - thunk(|to: &mut Tui|{ - for (index, port) in self.midi_ins().iter().enumerate() { - x_push(index as u16 * 10, h_exact(1, self.view_inputs_row(port))).draw(to); - } - todo!() - }) - ) - } - - #[cfg(feature = "track")] fn view_inputs_header (&self) -> impl Draw + '_ { - east(w_exact(20, origin_w(button_3("i", "nput ", format!("{}", self.midi_ins.len()), false))), - west(w_exact(4, button_2("I", "+", false)), thunk(move|to: &mut Tui|{ - for (_index, track, x1, _x2) in self.tracks_with_sizes() { - #[cfg(feature = "track")] - x_push(x1 as u16, bg(track.color.dark.term, origin_w(w_exact(track.width as u16, east!( - either(track.sequencer.monitoring, fg(Green, "mon "), "mon "), - either(track.sequencer.recording, fg(Red, "rec "), "rec "), - either(track.sequencer.overdub, fg(Yellow, "dub "), "dub "), - ))))).draw(to); - } - todo!() - }))) - } - - #[cfg(feature = "track")] fn view_inputs_row (&self, port: &MidiInput) -> impl Draw { - east(w_exact(20, origin_w(east(" ● ", bold(true, fg(Rgb(255,255,255), port.port_name()))))), - west(w_exact(4, ()), thunk(move|to: &mut Tui|{ - for (_index, track, _x1, _x2) in self.tracks_with_sizes() { - #[cfg(feature = "track")] - bg(track.color.darker.term, origin_w(w_exact(track.width as u16, east!( - either(track.sequencer.monitoring, fg(Green, " ● "), " Β· "), - either(track.sequencer.recording, fg(Red, " ● "), " Β· "), - either(track.sequencer.overdub, fg(Yellow, " ● "), " Β· "), - )))).draw(to); - } - todo!() - }))) - } - - #[cfg(feature = "track")] pub fn view_outputs (&self, theme: ItemTheme) -> impl Draw { - let mut h = 1; - - for output in self.midi_outs().iter() { - h += 1 + output.connections.len(); - } - - let h = h as u16; - - let list = south( - h_exact(1, w_full(origin_w(button_3( - "o", "utput", format!("{}", self.midi_outs.len()), false - )))), - h_exact(h - 1, wh_full(origin_nw(thunk(|to: &mut Tui|{ - for (_index, port) in self.midi_outs().iter().enumerate() { - h_exact(1,w_full(east( - origin_w(east(" ● ", fg(Rgb(255,255,255), bold(true, port.port_name())))), - w_full(origin_e(format!("{}/{} ", - port.port().get_connections().len(), - port.connections.len())))))).draw(to); - for (index, conn) in port.connections.iter().enumerate() { - h_exact(1, w_full(origin_w(format!(" c{index:02}{}", conn.info())))) - .draw(to); - } - } - todo!(); - })))) - ); - - h_exact(h, view_track_row_section(theme, list, button_2("O", "+", false), - bg(theme.darker.term, origin_w(w_full( - thunk(|to: &mut Tui|{ - for (index, track, _x1, _x2) in self.tracks_with_sizes() { - w_exact(track_width(index, track), - thunk(|to: &mut Tui|{ - h_exact(1, origin_w(east( - either(true, fg(Green, "play "), "play "), - either(false, fg(Yellow, "solo "), "solo "), - ))).draw(to); - for (_index, port) in self.midi_outs().iter().enumerate() { - h_exact(1, origin_w(east( - either(true, fg(Green, " ● "), " Β· "), - either(false, fg(Yellow, " ● "), " Β· "), - ))).draw(to); - for (_index, _conn) in port.connections.iter().enumerate() { - h_exact(1, w_full("")).draw(to); - } - } - todo!() - }) - ).draw(to); - } - todo!() - }) - ))) - )) - } - #[cfg(feature = "track")] pub fn view_track_devices (&self, theme: ItemTheme) -> impl Draw { - let mut h = 2u16; - for track in self.tracks().iter() { - h = h.max(track.devices.len() as u16 * 2); - } - let tracks = ||self.tracks_with_sizes(); - let track = move|(_, track, _x1, _x2), index| wh_exact( - Some(track_width(index, track)), - Some(h + 1), - bg(track.color.dark.term, origin_nw(iter_south(move||0..h, - |_, _index|wh_exact(Some(track.width as u16), Some(2), - fg_bg( - ItemTheme::G[32].lightest.term, - ItemTheme::G[32].dark.term, - origin_nw(format!(" Β· {}", "--")) - ) - ))))); - 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), - iter(tracks, track) - ) - } - - /// Put a clip in a slot - #[cfg(feature = "clip")] 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 - #[cfg(feature = "clip")] 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 - #[cfg(feature = "clip")] pub fn toggle_loop (&mut self) { - if let Some(clip) = self.selected_clip() { - clip.write().unwrap().toggle_loop() - } - } - - /// Get the first sampler of the active track - #[cfg(feature = "sampler")] pub fn sampler (&self) -> Option<&Sampler> { - self.selected_track()?.sampler(0) - } - - /// Get the first sampler of the active track - #[cfg(feature = "sampler")] pub fn sampler_mut (&mut self) -> Option<&mut Sampler> { - self.selected_track_mut()?.sampler_mut(0) - } - -} -impl ScenesView for Arrangement { - fn h_scenes (&self) -> u16 { - (self.size.h() as u16).saturating_sub(20) - } - fn w_side (&self) -> u16 { - (self.size.w() as u16 * 2 / 10).max(20) - } - fn w_mid (&self) -> u16 { - (self.size.w() as u16).saturating_sub(2 * self.w_side()).max(40) - } -} -impl HasClipsSize for Arrangement { - fn clips_size (&self) -> &Size { &self.size_inner } -} - -pub type SceneWith<'a, T> = - (usize, &'a Scene, usize, usize, T); - -def_command!(SceneCommand: |scene: Scene| { - SetSize { size: usize } => { todo!() }, - SetZoom { size: usize } => { todo!() }, - SetName { name: Arc } => - swap_value(&mut scene.name, name, |name|Self::SetName{name}), - SetColor { color: ItemTheme } => - swap_value(&mut scene.color, color, |color|Self::SetColor{color}), -}); - -def_command!(TrackCommand: |track: Track| { - Stop => { track.sequencer.enqueue_next(None); Ok(None) }, - SetMute { mute: Option } => todo!(), - SetSolo { solo: Option } => todo!(), - SetSize { size: usize } => todo!(), - SetZoom { zoom: usize } => todo!(), - SetName { name: Arc } => - swap_value(&mut track.name, name, |name|Self::SetName { name }), - SetColor { color: ItemTheme } => - swap_value(&mut track.color, color, |color|Self::SetColor { color }), - SetRec { rec: Option } => - toggle_bool(&mut track.sequencer.recording, rec, |rec|Self::SetRec { rec }), - SetMon { mon: Option } => - toggle_bool(&mut track.sequencer.monitoring, mon, |mon|Self::SetMon { mon }), -}); - -def_command!(ClipCommand: |clip: MidiClip| { - SetColor { color: Option } => { - //(SetColor [t: usize, s: usize, c: ItemTheme] - //clip.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o))))); - //("color" [a: usize, b: usize] Some(Self::SetColor(a.unwrap(), b.unwrap(), ItemTheme::random()))) - todo!() - }, - SetLoop { looping: Option } => { - //(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}")) - //("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap()))) - todo!() - } -}); diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index 415f99b8..00000000 --- a/src/config.rs +++ /dev/null @@ -1,92 +0,0 @@ -use crate::{*, bind::*, mode::*, view::*}; - -/// Configuration: mode, view, and bind definitions. -/// -/// ``` -/// let config = tek::Config::default(); -/// ``` -/// -/// ``` -/// // Some dizzle. -/// // What indentation to use here lol? -/// let source = stringify!((mode :menu (name Menu) -/// (info Mode selector.) (keys :axis/y :confirm) -/// (view (bg (g 0) (bsp/s :ports/out -/// (bsp/n :ports/in -/// (bg (g 30) (bsp/s (fixed/y 7 :logo) -/// (fill :dialog/menu))))))))); -/// // Add this definition to the config and try to load it. -/// // A "mode" is basically a state machine -/// // with associated input and output definitions. -/// tek::Config::default().add(&source).unwrap().get_mode(":menu").unwrap(); -/// ``` -#[derive(Default, Debug)] pub struct Config { - /// XDG base directories of running user. - pub dirs: BaseDirectories, - /// Active collection of interaction modes. - pub modes: Modes, - /// Active collection of event bindings. - pub binds: Binds, - /// Active collection of view definitions. - pub views: Views, -} -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()) - } - } -} - diff --git a/src/device.rs b/src/device.rs index c3ea587d..83cc1ad3 100644 --- a/src/device.rs +++ b/src/device.rs @@ -1,30 +1,7 @@ use crate::*; -use ConnectName::*; -use ConnectScope::*; -use ConnectStatus::*; def_command!(DeviceCommand: |device: Device| {}); -def_command!(MidiInputCommand: |port: MidiInput| { - Close => todo!(), - Connect { midi_out: Arc } => todo!(), -}); - -def_command!(MidiOutputCommand: |port: MidiOutput| { - Close => todo!(), - Connect { midi_in: Arc } => todo!(), -}); - -def_command!(AudioInputCommand: |port: AudioInput| { - Close => todo!(), - Connect { audio_out: Arc } => todo!(), -}); - -def_command!(AudioOutputCommand: |port: AudioOutput| { - Close => todo!(), - Connect { audio_in: Arc } => todo!(), -}); - impl Device { pub fn name (&self) -> &str { match self { @@ -84,528 +61,6 @@ impl Device { /// Some sort of wrapper? pub struct DeviceAudio<'a>(pub &'a mut Device); -/// Audio input port. -#[derive(Debug)] pub struct AudioInput { - /// Handle to JACK client, for receiving reconnect events. - pub jack: Jack<'static>, - /// Port name - pub name: Arc, - /// Port handle. - pub port: Port, - /// List of ports to connect to. - pub connections: Vec, -} - -/// Audio output port. -#[derive(Debug)] pub struct AudioOutput { - /// Handle to JACK client, for receiving reconnect events. - pub jack: Jack<'static>, - /// Port name - pub name: Arc, - /// Port handle. - pub port: Port, - /// List of ports to connect to. - pub connections: Vec, -} - -/// MIDI input port. -#[derive(Debug)] pub struct MidiInput { - /// Handle to JACK client, for receiving reconnect events. - pub jack: Jack<'static>, - /// Port name - pub name: Arc, - /// Port handle. - pub port: Port, - /// List of currently held notes. - pub held: Arc>, - /// List of ports to connect to. - pub connections: Vec, -} - -/// MIDI output port. -#[derive(Debug)] pub struct MidiOutput { - /// Handle to JACK client, for receiving reconnect events. - pub jack: Jack<'static>, - /// Port name - pub name: Arc, - /// Port handle. - pub port: Port, - /// List of ports to connect to. - pub connections: Vec, - /// List of currently held notes. - pub held: Arc>, - /// Buffer - pub note_buffer: Vec, - /// Buffer - pub output_buffer: Vec>>, -} - -#[derive(Clone, Debug, PartialEq)] pub enum ConnectName { - /** Exact match */ - Exact(Arc), - /** Match regular expression */ - RegExp(Arc), -} - -#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectScope { - One, - All -} - -#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectStatus { - Missing, - Disconnected, - Connected, - Mismatch, -} - -/// Port connection manager. -/// -/// ``` -/// let connect = tek::Connect::default(); -/// ``` -#[derive(Clone, Debug, Default)] pub struct Connect { - pub name: Option, - pub scope: Option, - pub status: Arc, Arc, ConnectStatus)>>>, - pub info: Arc, -} - -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 HasJack<'static> for MidiOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } -impl HasJack<'static> for AudioInput { fn jack (&self) -> &Jack<'static> { &self.jack } } -impl HasJack<'static> for AudioOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } -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) - } -} - -/// May create new MIDI input ports. -pub trait AddMidiIn { - fn midi_in_add (&mut self) -> Usually<()>; -} - -/// May create new MIDI output ports. -pub trait AddMidiOut { - fn midi_out_add (&mut self) -> Usually<()>; -} - -pub trait RegisterPorts: HasJack<'static> { - /// Register a MIDI input port. - fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; - /// Register a MIDI output port. - fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; - /// Register an audio input port. - fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; - /// Register an audio output port. - fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; -} - -pub trait JackPort: HasJack<'static> { - - type Port: PortSpec + Default; - - type Pair: PortSpec + Default; - - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized; - - fn register (jack: &Jack<'static>, name: &impl AsRef) -> Usually> { - jack.with_client(|c|c.register_port::(name.as_ref(), Default::default())) - .map_err(|e|e.into()) - } - - fn port_name (&self) -> &Arc; - - fn connections (&self) -> &[Connect]; - - fn port (&self) -> &Port; - - fn port_mut (&mut self) -> &mut Port; - - fn into_port (self) -> Port where Self: Sized; - - fn close (self) -> Usually<()> where Self: Sized { - let jack = self.jack().clone(); - Ok(jack.with_client(|c|c.unregister_port(self.into_port()))?) - } - - fn ports (&self, re_name: Option<&str>, re_type: Option<&str>, flags: PortFlags) -> Vec { - self.with_client(|c|c.ports(re_name, re_type, flags)) - } - - fn port_by_id (&self, id: u32) -> Option> { - self.with_client(|c|c.port_by_id(id)) - } - - fn port_by_name (&self, name: impl AsRef) -> Option> { - self.with_client(|c|c.port_by_name(name.as_ref())) - } - - fn connect_to_matching <'k> (&'k self) -> Usually<()> { - for connect in self.connections().iter() { - match &connect.name { - Some(Exact(name)) => { - *connect.status.write().unwrap() = self.connect_exact(name)?; - }, - Some(RegExp(re)) => { - *connect.status.write().unwrap() = self.connect_regexp(re, connect.scope)?; - }, - _ => {}, - }; - } - Ok(()) - } - - fn connect_exact <'k> (&'k self, name: &str) -> - Usually, Arc, ConnectStatus)>> - { - self.with_client(move|c|{ - let mut status = vec![]; - for port in c.ports(None, None, PortFlags::empty()).iter() { - if port.as_str() == &*name { - if let Some(port) = c.port_by_name(port.as_str()) { - let port_status = self.connect_to_unowned(&port)?; - let name = port.name()?.into(); - status.push((port, name, port_status)); - if port_status == Connected { - break - } - } - } - } - Ok(status) - }) - } - - fn connect_regexp <'k> ( - &'k self, re: &str, scope: Option - ) -> Usually, Arc, ConnectStatus)>> { - self.with_client(move|c|{ - let mut status = vec![]; - let ports = c.ports(Some(&re), None, PortFlags::empty()); - for port in ports.iter() { - if let Some(port) = c.port_by_name(port.as_str()) { - let port_status = self.connect_to_unowned(&port)?; - let name = port.name()?.into(); - status.push((port, name, port_status)); - if port_status == Connected && scope == Some(One) { - break - } - } - } - Ok(status) - }) - } - - /** Connect to a matching port by name. */ - fn connect_to_name (&self, name: impl AsRef) -> Usually { - self.with_client(|c|if let Some(ref port) = c.port_by_name(name.as_ref()) { - self.connect_to_unowned(port) - } else { - Ok(Missing) - }) - } - - /** Connect to a matching port by reference. */ - fn connect_to_unowned (&self, port: &Port) -> Usually { - self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { - Connected - } else if let Ok(_) = c.connect_ports(port, self.port()) { - Connected - } else { - Mismatch - })) - } - - /** Connect to an owned matching port by reference. */ - fn connect_to_owned (&self, port: &Port) -> Usually { - self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { - Connected - } else if let Ok(_) = c.connect_ports(port, self.port()) { - Connected - } else { - Mismatch - })) - } -} - -impl 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 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 View for MidiInput { - fn view (&self) -> impl Draw { - "TODO: MIDI IN" - } -} - -impl View for MidiOutput { - fn view (&self) -> impl Draw { - "TODO: MIDI OUT" - } -} - -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 MidiInput { - pub fn parsed <'a> (&'a self, scope: &'a ProcessScope) -> impl Iterator, &'a [u8])> { - parse_midi_input(self.port().iter(scope)) - } -} -impl> + AsMut>> HasMidiIns for T { - fn midi_ins (&self) -> &Vec { self.as_ref() } - fn midi_ins_mut (&mut self) -> &mut Vec { self.as_mut() } -} -impl> + AsMut>> HasMidiOuts for T { - fn midi_outs (&self) -> &Vec { self.as_ref() } - fn midi_outs_mut (&mut self) -> &mut Vec { self.as_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(()) - } -} -/// 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 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 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_audio!(|self: DeviceAudio<'a>, client, scope|{ use Device::*; match self.0 { @@ -635,48 +90,26 @@ impl> + AsMut>> HasDevices for T { self.as_mut() } } -/// Trait for thing that may receive MIDI. -pub trait HasMidiIns { - fn midi_ins (&self) -> &Vec; - fn midi_ins_mut (&mut self) -> &mut Vec; - /// Collect MIDI input from app ports (TODO preallocate large buffers) - fn midi_input_collect <'a> (&'a self, scope: &'a ProcessScope) -> CollectedMidiInput<'a> { - self.midi_ins().iter() - .map(|port|port.port().iter(scope) - .map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes))) - .collect::>()) - .collect::>() + +pub trait HasDevices: AsRef> + AsMut> { + fn devices (&self) -> &Vec { + self.as_ref() } - fn midi_ins_with_sizes <'a> (&'a self) -> - impl Iterator, &'a [Connect], usize, usize)> + Send + Sync + 'a - { - let mut y = 0; - self.midi_ins().iter().enumerate().map(move|(i, input)|{ - let height = 1 + input.connections().len(); - let data = (i, input.port_name(), input.connections(), y, y + height); - y += height; - data - }) - } -} -/// Trait for thing that may output MIDI. -pub trait HasMidiOuts { - fn midi_outs (&self) -> &Vec; - fn midi_outs_mut (&mut self) -> &mut Vec; - fn midi_outs_with_sizes <'a> (&'a self) -> - impl Iterator, &'a [Connect], usize, usize)> + Send + Sync + 'a - { - let mut y = 0; - self.midi_outs().iter().enumerate().map(move|(i, output)|{ - let height = 1 + output.connections().len(); - let data = (i, output.port_name(), output.connections(), y, y + height); - y += height; - data - }) - } - fn midi_outs_emit (&mut self, scope: &ProcessScope) { - for port in self.midi_outs_mut().iter_mut() { - port.buffer_emit(scope) - } + fn devices_mut (&mut self) -> &mut Vec { + self.as_mut() } } + +pub mod arrange; pub use self::arrange::*; +pub mod browse; pub use self::browse::*; +pub mod clock; pub use self::clock::*; +pub mod dialog; pub use self::dialog::*; +pub mod editor; pub use self::editor::*; +pub mod menu; pub use self::menu::*; +pub mod mix; pub use self::mix::*; +pub mod port; pub use self::port::*; +pub mod sample; pub use self::sample::*; +pub mod sequence; pub use self::sequence::*; + +#[cfg(feature = "plugin")] pub mod plugin; +#[cfg(feature = "plugin")] pub use self::plugin::*; diff --git a/src/device/arrange.rs b/src/device/arrange.rs new file mode 100644 index 00000000..8c3f51d8 --- /dev/null +++ b/src/device/arrange.rs @@ -0,0 +1,123 @@ +use crate::*; + +/// Arranger. +/// +/// ``` +/// let arranger = tek::Arrangement::default(); +/// ``` +#[derive(Default, Debug)] pub struct Arrangement { + /// Project name. + pub name: Arc, + /// Base color. + pub color: ItemTheme, + /// JACK client handle. + pub jack: Jack<'static>, + /// FIXME a render of the project arrangement, redrawn on update. + /// TODO rename to "render_cache" or smth + pub arranger: Arc>, + /// Display size + pub size: Size, + /// Display size of clips area + pub size_inner: Size, + /// Source of time + #[cfg(feature = "clock")] pub clock: Clock, + /// Allows one MIDI clip to be edited + #[cfg(feature = "editor")] pub editor: Option, + /// List of global midi inputs + #[cfg(feature = "port")] pub midi_ins: Vec, + /// List of global midi outputs + #[cfg(feature = "port")] pub midi_outs: Vec, + /// List of global audio inputs + #[cfg(feature = "port")] pub audio_ins: Vec, + /// List of global audio outputs + #[cfg(feature = "port")] pub audio_outs: Vec, + /// Selected UI element + #[cfg(feature = "select")] pub selection: Selection, + /// Last track number (to avoid duplicate port names) + #[cfg(feature = "track")] pub track_last: usize, + /// List of tracks + #[cfg(feature = "track")] pub tracks: Vec, + /// Scroll offset of tracks + #[cfg(feature = "track")] pub track_scroll: usize, + /// List of scenes + #[cfg(feature = "scene")] pub scenes: Vec, + /// Scroll offset of scenes + #[cfg(feature = "scene")] pub scene_scroll: usize, +} + +impl HasJack<'static> for Arrangement { fn jack (&self) -> &Jack<'static> { &self.jack } } +impl_has!(Jack<'static>: |self: Arrangement| self.jack); +impl_has!(Size: |self: Arrangement| self.size); +impl_has!(Vec: |self: Arrangement| self.midi_ins); +impl_has!(Vec: |self: Arrangement| self.midi_outs); +impl_has!(Clock: |self: Arrangement| self.clock); +impl_has!(Selection: |self: Arrangement| self.selection); +impl_as_ref_opt!(MidiEditor: |self: Arrangement| self.editor.as_ref()); +impl_as_mut_opt!(MidiEditor: |self: Arrangement| self.editor.as_mut()); + +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) + } + + /// Get the first sampler of the active track + #[cfg(feature = "sampler")] + pub fn sampler (&self) -> Option<&Sampler> { + self.selected_track()?.sampler(0) + } + + /// Get the first sampler of the active track + #[cfg(feature = "sampler")] + pub fn sampler_mut (&mut self) -> Option<&mut Sampler> { + self.selected_track_mut()?.sampler_mut(0) + } + +} + +#[cfg(feature = "clip")] mod clip; #[cfg(feature = "clip")] pub use self::clip::*; +#[cfg(feature = "scene")] mod scene; #[cfg(feature = "scene")] pub use self::scene::*; +#[cfg(feature = "track")] mod track; #[cfg(feature = "track")] pub use self::track::*; +#[cfg(feature = "select")] mod select; #[cfg(feature = "select")] pub use self::select::*; diff --git a/src/device/arrange/clip.rs b/src/device/arrange/clip.rs new file mode 100644 index 00000000..c2b4cad0 --- /dev/null +++ b/src/device/arrange/clip.rs @@ -0,0 +1,90 @@ +use crate::*; + +/// TODO: Preserve the generic passthru syntax; +/// remove this macro (only used twice) and potentially the trait. +#[macro_export] macro_rules! impl_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() + } + } + } +} + +#[macro_export] macro_rules! has_clip { + (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? HasMidiClip for $Struct $(<$($L),*$($T),*>)? { + fn clip (&$self) -> Option>> { $cb } + } + } +} + +pub trait ClipsView: TracksView + ScenesView { + /// Draw clips per scene + fn view_scenes_clips <'a> (&'a self) -> impl Draw + 'a { + crate::view::view_scenes_clips( + self.tracks_with_sizes(), self.clips_size() + ) + } + /// Draw clips for tracks + fn view_track_clips <'a> (&'a self, track_index: usize, track: &'a Track) -> impl Draw + 'a { + crate::view::view_track_clips( + self.scenes_with_sizes(), self.selection(), self.editor(), track_index, track + ) + } +} + +impl HasClipsSize for Arrangement { + fn clips_size (&self) -> &Size { &self.size_inner } +} + +def_command!(ClipCommand: |clip: MidiClip| { + SetColor { color: Option } => { + //(SetColor [t: usize, s: usize, c: ItemTheme] + //clip.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o))))); + //("color" [a: usize, b: usize] Some(Self::SetColor(a.unwrap(), b.unwrap(), ItemTheme::random()))) + todo!() + }, + SetLoop { looping: Option } => { + //(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}")) + //("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap()))) + todo!() + } +}); + +impl Arrangement { + /// 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.selected_clip() { + clip.write().unwrap().toggle_loop() + } + } +} + +pub trait HasClipsSize { fn clips_size (&self) -> &Size; } diff --git a/src/device/arrange/scene.rs b/src/device/arrange/scene.rs new file mode 100644 index 00000000..be16abfb --- /dev/null +++ b/src/device/arrange/scene.rs @@ -0,0 +1,167 @@ +use crate::*; + +/// A scene consists of a set of clips to play together. +/// +/// ``` +/// let scene: tek::Scene = Default::default(); +/// let _ = scene.pulses(); +/// let _ = scene.is_playing(&[]); +/// ``` +#[derive(Debug, Default)] pub struct Scene { + /// Name of scene + pub name: Arc, + /// Identifying color of scene + pub color: ItemTheme, + /// Clips in scene, one per track + pub clips: Vec>>>, +} + +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 } + } + 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!() } +} + +pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + Send + Sync { + /// Default scene height. + const H_SCENE: usize = 2; + /// Default editor height. + const H_EDITOR: usize = 15; + fn h_scenes (&self) -> u16; + fn w_side (&self) -> u16; + fn w_mid (&self) -> u16; + + fn view_scenes_names (&self) -> impl Draw { + crate::view::view_scenes_names( + self.scenes_with_sizes() + ) + } + fn view_scene_name <'a> (&'a self, index: usize, scene: &'a Scene) -> impl Draw + 'a { + crate::view::view_scene_name( + self.selected(), self.editor(), index, scene + ) + } + fn scenes_with_sizes (&self) -> impl ScenesSizes<'_> { + let mut y = 0; + self.scenes().iter().enumerate().skip(self.scene_scroll()).map_while(move|(s, scene)|{ + let height = if self.selection().scene() == Some(s) && self.editor().is_some() { + 8 + } else { + Self::H_SCENE + }; + if y + height <= self.clips_size().h() as usize { + let data = (s, scene, y, y + height); + y += height; + Some(data) + } else { + None + } + }) + } +} + +pub trait HasSceneScroll: HasScenes { + fn scene_scroll (&self) -> usize; +} + +pub trait HasScene: AsRefOpt + AsMutOpt { + fn scene_mut (&mut self) -> Option<&mut Scene> { self.as_mut_opt() } + fn scene (&self) -> Option<&Scene> { self.as_ref_opt() } +} + +pub trait HasScenes: AsRef> + AsMut> { + fn scenes (&self) -> &Vec { self.as_ref() } + fn scenes_mut (&mut self) -> &mut Vec { self.as_mut() } + /// Generate the default name for a new scene + fn scene_default_name (&self) -> Arc { format!("s{:3>}", self.scenes().len() + 1).into() } + fn scene_longest_name (&self) -> usize { self.scenes().iter().map(|s|s.name.len()).fold(0, usize::max) } + /// Add multiple scenes + fn scenes_add (&mut self, n: usize) -> Usually<()> where Self: HasTracks { + let scene_color_1 = ItemColor::random(); + let scene_color_2 = ItemColor::random(); + for i in 0..n { + let _ = self.scene_add(None, Some( + scene_color_1.mix(scene_color_2, i as f32 / n as f32).into() + ))?; + } + Ok(()) + } + /// Add a scene + fn scene_add (&mut self, name: Option<&str>, color: Option) + -> Usually<(usize, &mut Scene)> where Self: HasTracks + { + let scene = Scene { + name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()), + clips: vec![None;self.tracks().len()], + color: color.unwrap_or_else(ItemTheme::random), + }; + self.scenes_mut().push(scene); + let index = self.scenes().len() - 1; + Ok((index, &mut self.scenes_mut()[index])) + } +} + +impl HasSceneScroll for Arrangement { + fn scene_scroll (&self) -> usize { self.scene_scroll } +} + +impl ScenesView for Arrangement { + fn h_scenes (&self) -> u16 { + (self.size.h() as u16).saturating_sub(20) + } + fn w_side (&self) -> u16 { + (self.size.w() as u16 * 2 / 10).max(20) + } + fn w_mid (&self) -> u16 { + (self.size.w() as u16).saturating_sub(2 * self.w_side()).max(40) + } +} + +pub type SceneWith<'a, T> = + (usize, &'a Scene, usize, usize, T); + +def_command!(SceneCommand: |scene: Scene| { + SetSize { size: usize } => { todo!() }, + SetZoom { size: usize } => { todo!() }, + SetName { name: Arc } => + swap_value(&mut scene.name, name, |name|Self::SetName{name}), + SetColor { color: ItemTheme } => + swap_value(&mut scene.color, color, |color|Self::SetColor{color}), +}); + +#[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: App| self.project.as_ref_opt()); +#[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: App| self.project.as_mut_opt()); +#[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: Arrangement| self.selected_scene()); +#[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: Arrangement| self.selected_scene_mut()); +impl ClipsView for T {} +impl_has!(Vec: |self: Arrangement| self.scenes); +impl>+AsMut>> HasScenes for T {} +impl+AsMutOpt+Send+Sync> HasScene for T {} diff --git a/src/select.rs b/src/device/arrange/select.rs similarity index 100% rename from src/select.rs rename to src/device/arrange/select.rs diff --git a/src/device/arrange/track.rs b/src/device/arrange/track.rs new file mode 100644 index 00000000..e80a64f7 --- /dev/null +++ b/src/device/arrange/track.rs @@ -0,0 +1,339 @@ +use crate::*; + +/// A track consists of a sequencer and zero or more devices chained after it. +/// +/// ``` +/// let track: tek::Track = Default::default(); +/// ``` +#[derive(Debug, Default)] pub struct Track { + /// Name of track + pub name: Arc, + /// Identifying color of track + pub color: ItemTheme, + /// Preferred width of track column + pub width: usize, + /// MIDI sequencer state + pub sequencer: Sequencer, + /// Device chain + pub devices: Vec, +} + +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!() } + + pub fn per <'a, T: Draw + 'a, U: TracksSizes<'a>> ( + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a + ) -> impl Draw + 'a { + per(tracks, callback) + } + + /// Create a new track connecting the [Sequencer] to a [Sampler]. + #[cfg(feature = "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) + } + + #[cfg(feature = "sampler")] 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 + } + + #[cfg(feature = "sampler")] 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 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; } } +} + +pub trait HasTracks: AsRef> + AsMut> { + fn tracks (&self) -> &Vec { self.as_ref() } + fn tracks_mut (&mut self) -> &mut Vec { self.as_mut() } + /// Run audio callbacks for every track and every device + fn process_tracks (&mut self, client: &Client, scope: &ProcessScope) -> Control { + for track in self.tracks_mut().iter_mut() { + if Control::Quit == Audio::process(&mut track.sequencer, client, scope) { + return Control::Quit + } + for device in track.devices.iter_mut() { + if Control::Quit == DeviceAudio(device).process(client, scope) { + return Control::Quit + } + } + } + Control::Continue + } + fn track_longest_name (&self) -> usize { + self.tracks().iter().map(|s|s.name.len()).fold(0, usize::max) + } + /// Stop all playing clips + fn tracks_stop_all (&mut self) { + for track in self.tracks_mut().iter_mut() { + track.sequencer.enqueue_next(None); + } + } + /// Stop all playing clips + fn tracks_launch (&mut self, clips: Option>>>>) { + if let Some(clips) = clips { + for (clip, track) in clips.iter().zip(self.tracks_mut()) { + track.sequencer.enqueue_next(clip.as_ref()); + } + } else { + for track in self.tracks_mut().iter_mut() { + track.sequencer.enqueue_next(None); + } + } + } + /// Spacing between tracks. + const TRACK_SPACING: usize = 0; +} + +pub trait HasTrack: AsRefOpt + AsMutOpt { + fn track (&self) -> Option<&Track> { self.as_ref_opt() } + fn track_mut (&mut self) -> Option<&mut Track> { self.as_mut_opt() } + + #[cfg(feature = "port")] + fn view_midi_ins_status <'a> (&'a self, theme: ItemTheme) -> impl Draw + 'a { + crate::view::view_midi_ins_status(theme, self.track()) + } + + #[cfg(feature = "port")] + fn view_midi_outs_status (&self, theme: ItemTheme) -> impl Draw + '_ { + crate::view::view_midi_outs_status(theme, self.track()) + } + + #[cfg(feature = "port")] + fn view_audio_ins_status (&self, theme: ItemTheme) -> impl Draw { + crate::view::view_audio_ins_status(theme, self.track()) + } + + #[cfg(feature = "port")] + fn view_audio_outs_status (&self, theme: ItemTheme) -> impl Draw { + crate::view::view_audio_outs_status(theme, self.track()) + } +} + +pub trait HasTrackScroll: HasTracks { + fn track_scroll (&self) -> usize; +} + +pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll { + /// Draw name of each track + fn view_track_names (&self, theme: ItemTheme) -> impl Draw { + crate::view::view_track_names( + self.tracks_with_sizes(), self.tracks().len(), self.scenes().len(), self.selection() + ) + } + /// Draw outputs per track + fn view_track_outputs <'a> (&'a self, theme: ItemTheme, _h: u16) -> impl Draw { + crate::view::view_track_outputs( + theme, self.tracks_with_sizes(), self.midi_outs().iter() + ) + } + /// Draw inputs per track + fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Draw { + let mut h = 0u16; + for track in self.tracks().iter() { + h = h.max(track.sequencer.midi_ins.len() as u16); + } + crate::view::view_track_inputs(theme, self.tracks_with_sizes(), h) + } + /// Iterate over tracks with their corresponding sizes. + fn tracks_with_sizes (&self) -> impl TracksSizes<'_> { + let _editor_width = self.editor().map(|e|e.size.w()); + let _active_track = self.selection().track(); + let mut x = 0; + let w = self.clips_size().w() as usize; + self.tracks().iter().enumerate().map_while(move |(index, track)|{ + let width = track.width.max(8); + if x + width < w { + let data = (index, track, x, x + width); + x += width + Self::TRACK_SPACING; + Some(data) + } else { + None + } + }) + } + +} + +impl HasTrackScroll for Arrangement { + fn track_scroll (&self) -> usize { self.track_scroll } +} + +def_command!(TrackCommand: |track: Track| { + Stop => { track.sequencer.enqueue_next(None); Ok(None) }, + SetMute { mute: Option } => todo!(), + SetSolo { solo: Option } => todo!(), + SetSize { size: usize } => todo!(), + SetZoom { zoom: usize } => todo!(), + SetName { name: Arc } => + swap_value(&mut track.name, name, |name|Self::SetName { name }), + SetColor { color: ItemTheme } => + swap_value(&mut track.color, color, |color|Self::SetColor { color }), + SetRec { rec: Option } => + toggle_bool(&mut track.sequencer.recording, rec, |rec|Self::SetRec { rec }), + SetMon { mon: Option } => + toggle_bool(&mut track.sequencer.monitoring, mon, |mon|Self::SetMon { mon }), +}); + +impl>+AsMut>> HasTracks for T {} +impl+AsMutOpt+Send+Sync> HasTrack for T {} +impl TracksView for T {} +impl_as_ref!(Vec: |self: App| self.project.as_ref()); +impl_as_mut!(Vec: |self: App| self.project.as_mut()); +#[cfg(feature = "select")] impl_as_ref_opt!(Track: |self: App| self.project.as_ref_opt()); +#[cfg(feature = "select")] impl_as_mut_opt!(Track: |self: App| self.project.as_mut_opt()); +impl_has!(Vec: |self: Arrangement| self.tracks); +impl_as_ref_opt!(Track: |self: Arrangement| self.selected_track()); +impl_as_mut_opt!(Track: |self: Arrangement| self.selected_track_mut()); + +impl Arrangement { + #[cfg(feature = "track")] + pub fn view_inputs (&self, _theme: ItemTheme) -> impl Draw + '_ { + crate::view::view_inputs() + } + + #[cfg(feature = "track")] + pub fn view_inputs_header (&self) -> impl Draw + '_ { + crate::view::view_inputs_header(self.tracks_with_sizes()) + } + + #[cfg(feature = "track")] + pub fn view_inputs_row (&self, port: &MidiInput) -> impl Draw { + crate::view::view_inputs_row(port) + } + + #[cfg(feature = "track")] + pub fn view_outputs (&self, theme: ItemTheme) -> impl Draw { + let mut h = 1; + for output in self.midi_outs().iter() { + h += 1 + output.connections.len(); + } + let h = h as u16; + crate::view::view_outputs(self.tracks_with_sizes()) + } + + #[cfg(feature = "track")] + pub fn view_track_devices (&self, theme: ItemTheme) -> impl Draw { + let mut h = 2u16; + for track in self.tracks().iter() { + h = h.max(track.devices.len() as u16 * 2); + } + crate::view::view_track_devices(||self.tracks_with_sizes(), self.track(), h) + } + + /// Add multiple tracks + #[cfg(feature = "track")] 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 + #[cfg(feature = "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])) + } +} diff --git a/src/device/audio.rs b/src/device/audio.rs new file mode 100644 index 00000000..e69de29b diff --git a/src/browse.rs b/src/device/browse.rs similarity index 94% rename from src/browse.rs rename to src/device/browse.rs index 773bfd86..a10f486f 100644 --- a/src/browse.rs +++ b/src/device/browse.rs @@ -24,11 +24,12 @@ def_command!(FileBrowserCommand: |sampler: Sampler|{ pub size: Size, } -pub(crate) struct EntriesIterator<'a> { +pub(crate) struct EntriesIterator<'a, S: Screen> { pub browser: &'a Browse, pub offset: usize, pub length: usize, pub index: usize, + _screen: std::marker::PhantomData } #[derive(Clone, Debug)] pub enum BrowseTarget { @@ -318,12 +319,13 @@ impl Browse { index: 0, length: self.dirs.len() + self.files.len(), browser: self, + _screen: Default::default(), }; iter_south_fixed(1, iterate, item) } } -impl<'a> Iterator for EntriesIterator<'a> { - type Item = (); +impl<'a> Iterator for EntriesIterator<'a, Tui> { + type Item = impl Draw; fn next (&mut self) -> Option { let dirs = self.browser.dirs.len(); let files = self.browser.files.len(); @@ -360,24 +362,36 @@ def_command!(BrowseCommand: |browse: Browse| { def_command!(PoolCommand: |pool: Pool| { // Toggle visibility of pool - Show { visible: bool } => { pool.visible = *visible; Ok(Some(Self::Show { visible: !visible })) }, + Show { visible: bool } => { + pool.visible = *visible; + Ok(Some(Self::Show { visible: !visible })) + }, // Select a clip from the clip pool - Select { index: usize } => { pool.set_clip_index(*index); Ok(None) }, + Select { index: usize } => { + pool.set_clip_index(*index); + Ok(None) + }, // Update the contents of the clip pool - Clip { command: PoolClipCommand } => Ok(command.execute(pool)?.map(|command|Self::Clip{command})), + Clip { command: PoolClipCommand } => Ok( + command.act(pool)?.map(|command|Self::Clip{command}) + ), // Rename a clip - Rename { command: RenameCommand } => Ok(command.delegate(pool, |command|Self::Rename{command})?), + Rename { command: RenameCommand } => Ok( + command.act(pool)?.map(|command|Self::Rename{command}) + ), // Change the length of a clip - Length { command: CropCommand } => Ok(command.delegate(pool, |command|Self::Length{command})?), + Length { command: CropCommand } => Ok( + command.act(pool)?.map(|command|Self::Length{command}) + ), // Import from file Import { command: BrowseCommand } => Ok(if let Some(browse) = pool.browse.as_mut() { - command.delegate(browse, |command|Self::Import{command})? + command.act(browse)?.map(|command|Self::Import{command}) } else { None }), // Export to file Export { command: BrowseCommand } => Ok(if let Some(browse) = pool.browse.as_mut() { - command.delegate(browse, |command|Self::Export{command})? + command.act(browse)?.map(|command|Self::Export{command}) } else { None }), @@ -429,7 +443,7 @@ def_command!(PoolClipCommand: |pool: Pool| { for event in events.iter() { clip.notes[event.0 as usize].push(event.2); } - Ok(Self::Add { index, clip }.execute(pool)?) + Ok(Self::Add { index, clip }.act(pool)?) }, SetName { index: usize, name: Arc } => { let index = *index; diff --git a/src/clock.rs b/src/device/clock.rs similarity index 100% rename from src/clock.rs rename to src/device/clock.rs diff --git a/src/dialog.rs b/src/device/dialog.rs similarity index 99% rename from src/dialog.rs rename to src/device/dialog.rs index 56ae8043..1d861d6d 100644 --- a/src/dialog.rs +++ b/src/device/dialog.rs @@ -103,4 +103,3 @@ impl Dialog { /// FIXME: implement pub fn browser_target (&self) -> Option<&BrowseTarget> { todo!() } } - diff --git a/src/editor.rs b/src/device/editor.rs similarity index 99% rename from src/editor.rs rename to src/device/editor.rs index 4720a409..d9aa1dda 100644 --- a/src/editor.rs +++ b/src/device/editor.rs @@ -188,7 +188,7 @@ pub trait MidiViewer: MidiRange + MidiPoint + Debug + Send + Sync { } let next_time_area = time_axis * next_time_zoom; if next_time_area >= time_len { - self.time_zoom().set(next_time_zoom); + self.time_zoom().store(next_time_zoom, Relaxed); } else { break } @@ -199,7 +199,7 @@ pub trait MidiViewer: MidiRange + MidiPoint + Debug + Send + Sync { } let prev_time_area = time_axis * prev_time_zoom; if prev_time_area <= time_len { - self.time_zoom().set(prev_time_zoom); + self.time_zoom().store(prev_time_zoom, Relaxed); } else { break } @@ -746,7 +746,7 @@ impl MidiViewer for PianoHorizontal { 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); + self.time_len().store(clip.length, Relaxed); 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 diff --git a/src/menu.rs b/src/device/menu.rs similarity index 100% rename from src/menu.rs rename to src/device/menu.rs diff --git a/src/device/midi.rs b/src/device/midi.rs new file mode 100644 index 00000000..e69de29b diff --git a/src/mix.rs b/src/device/mix.rs similarity index 99% rename from src/mix.rs rename to src/device/mix.rs index 74db8388..860a7845 100644 --- a/src/mix.rs +++ b/src/device/mix.rs @@ -1,4 +1,5 @@ use crate::*; + #[derive(Debug, Default)] pub enum MeteringMode { #[default] Rms, Log10, diff --git a/src/plugin.rs b/src/device/plugin.rs similarity index 100% rename from src/plugin.rs rename to src/device/plugin.rs diff --git a/src/device/port.rs b/src/device/port.rs new file mode 100644 index 00000000..1375822f --- /dev/null +++ b/src/device/port.rs @@ -0,0 +1,163 @@ +use crate::*; +use ConnectName::*; +use ConnectScope::*; +use ConnectStatus::*; + +pub mod audio; pub use self::audio::*; +pub mod midi; pub use self::midi::*; +pub mod connect; pub use self::connect::*; + +pub trait JackPort: HasJack<'static> { + + type Port: PortSpec + Default; + + type Pair: PortSpec + Default; + + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized; + + fn register (jack: &Jack<'static>, name: &impl AsRef) -> Usually> { + jack.with_client(|c|c.register_port::(name.as_ref(), Default::default())) + .map_err(|e|e.into()) + } + + fn port_name (&self) -> &Arc; + + fn connections (&self) -> &[Connect]; + + fn port (&self) -> &Port; + + fn port_mut (&mut self) -> &mut Port; + + fn into_port (self) -> Port where Self: Sized; + + fn close (self) -> Usually<()> where Self: Sized { + let jack = self.jack().clone(); + Ok(jack.with_client(|c|c.unregister_port(self.into_port()))?) + } + + fn ports (&self, re_name: Option<&str>, re_type: Option<&str>, flags: PortFlags) -> Vec { + self.with_client(|c|c.ports(re_name, re_type, flags)) + } + + fn port_by_id (&self, id: u32) -> Option> { + self.with_client(|c|c.port_by_id(id)) + } + + fn port_by_name (&self, name: impl AsRef) -> Option> { + self.with_client(|c|c.port_by_name(name.as_ref())) + } + + fn connect_to_matching <'k> (&'k self) -> Usually<()> { + for connect in self.connections().iter() { + match &connect.name { + Some(Exact(name)) => { + *connect.status.write().unwrap() = self.connect_exact(name)?; + }, + Some(RegExp(re)) => { + *connect.status.write().unwrap() = self.connect_regexp(re, connect.scope)?; + }, + _ => {}, + }; + } + Ok(()) + } + + fn connect_exact <'k> (&'k self, name: &str) -> + Usually, Arc, ConnectStatus)>> + { + self.with_client(move|c|{ + let mut status = vec![]; + for port in c.ports(None, None, PortFlags::empty()).iter() { + if port.as_str() == &*name { + if let Some(port) = c.port_by_name(port.as_str()) { + let port_status = self.connect_to_unowned(&port)?; + let name = port.name()?.into(); + status.push((port, name, port_status)); + if port_status == Connected { + break + } + } + } + } + Ok(status) + }) + } + + fn connect_regexp <'k> ( + &'k self, re: &str, scope: Option + ) -> Usually, Arc, ConnectStatus)>> { + self.with_client(move|c|{ + let mut status = vec![]; + let ports = c.ports(Some(&re), None, PortFlags::empty()); + for port in ports.iter() { + if let Some(port) = c.port_by_name(port.as_str()) { + let port_status = self.connect_to_unowned(&port)?; + let name = port.name()?.into(); + status.push((port, name, port_status)); + if port_status == Connected && scope == Some(One) { + break + } + } + } + Ok(status) + }) + } + + /** Connect to a matching port by name. */ + fn connect_to_name (&self, name: impl AsRef) -> Usually { + self.with_client(|c|if let Some(ref port) = c.port_by_name(name.as_ref()) { + self.connect_to_unowned(port) + } else { + Ok(Missing) + }) + } + + /** Connect to a matching port by reference. */ + fn connect_to_unowned (&self, port: &Port) -> Usually { + self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { + Connected + } else if let Ok(_) = c.connect_ports(port, self.port()) { + Connected + } else { + Mismatch + })) + } + + /** Connect to an owned matching port by reference. */ + fn connect_to_owned (&self, port: &Port) -> Usually { + self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { + Connected + } else if let Ok(_) = c.connect_ports(port, self.port()) { + Connected + } else { + Mismatch + })) + } +} + +pub trait RegisterPorts: HasJack<'static> { + /// Register a MIDI input port. + fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; + /// Register a MIDI output port. + fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; + /// Register an audio input port. + fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; + /// Register an audio output port. + fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; +} + +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) + } +} diff --git a/src/device/port/audio.rs b/src/device/port/audio.rs new file mode 100644 index 00000000..bb1db676 --- /dev/null +++ b/src/device/port/audio.rs @@ -0,0 +1,103 @@ +use crate::*; + +def_command!(AudioInputCommand: |port: AudioInput| { + Close => todo!(), + Connect { audio_out: Arc } => todo!(), +}); + +def_command!(AudioOutputCommand: |port: AudioOutput| { + Close => todo!(), + Connect { audio_in: Arc } => todo!(), +}); + +impl HasJack<'static> for AudioInput { fn jack (&self) -> &Jack<'static> { &self.jack } } +impl HasJack<'static> for AudioOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } + +/// Audio input port. +#[derive(Debug)] pub struct AudioInput { + /// Handle to JACK client, for receiving reconnect events. + pub jack: Jack<'static>, + /// Port name + pub name: Arc, + /// Port handle. + pub port: Port, + /// List of ports to connect to. + pub connections: Vec, +} + +/// Audio output port. +#[derive(Debug)] pub struct AudioOutput { + /// Handle to JACK client, for receiving reconnect events. + pub jack: Jack<'static>, + /// Port name + pub name: Arc, + /// Port handle. + pub port: Port, + /// List of ports to connect to. + pub connections: Vec, +} + + +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 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) + } +} diff --git a/src/device/port/connect.rs b/src/device/port/connect.rs new file mode 100644 index 00000000..8571fa2e --- /dev/null +++ b/src/device/port/connect.rs @@ -0,0 +1,83 @@ +use crate::*; +use ConnectName::*; +use ConnectScope::*; +use ConnectStatus::*; + +#[derive(Clone, Debug, PartialEq)] pub enum ConnectName { + /** Exact match */ + Exact(Arc), + /** Match regular expression */ + RegExp(Arc), +} + +#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectScope { + One, + All +} + +#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectStatus { + Missing, + Disconnected, + Connected, + Mismatch, +} + +/// Port connection manager. +/// +/// ``` +/// let connect = tek::Connect::default(); +/// ``` +#[derive(Clone, Debug, Default)] pub struct Connect { + pub name: Option, + pub scope: Option, + pub status: Arc, Arc, ConnectStatus)>>>, + pub info: Arc, +} + +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() + } +} diff --git a/src/device/port/midi.rs b/src/device/port/midi.rs new file mode 100644 index 00000000..eb9160cc --- /dev/null +++ b/src/device/port/midi.rs @@ -0,0 +1,256 @@ +use crate::*; + +impl HasJack<'static> for MidiInput { fn jack (&self) -> &Jack<'static> { &self.jack } } + +impl HasJack<'static> for MidiOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } + +/// MIDI input port. +#[derive(Debug)] pub struct MidiInput { + /// Handle to JACK client, for receiving reconnect events. + pub jack: Jack<'static>, + /// Port name + pub name: Arc, + /// Port handle. + pub port: Port, + /// List of currently held notes. + pub held: Arc>, + /// List of ports to connect to. + pub connections: Vec, +} + +/// MIDI output port. +#[derive(Debug)] pub struct MidiOutput { + /// Handle to JACK client, for receiving reconnect events. + pub jack: Jack<'static>, + /// Port name + pub name: Arc, + /// Port handle. + pub port: Port, + /// List of ports to connect to. + pub connections: Vec, + /// List of currently held notes. + pub held: Arc>, + /// Buffer + pub note_buffer: Vec, + /// Buffer + pub output_buffer: Vec>>, +} + +def_command!(MidiInputCommand: |port: MidiInput| { + Close => todo!(), + Connect { midi_out: Arc } => todo!(), +}); + +def_command!(MidiOutputCommand: |port: MidiOutput| { + Close => todo!(), + Connect { midi_in: Arc } => todo!(), +}); +/// Trait for thing that may receive MIDI. +pub trait HasMidiIns { + fn midi_ins (&self) -> &Vec; + fn midi_ins_mut (&mut self) -> &mut Vec; + /// Collect MIDI input from app ports (TODO preallocate large buffers) + fn midi_input_collect <'a> (&'a self, scope: &'a ProcessScope) -> CollectedMidiInput<'a> { + self.midi_ins().iter() + .map(|port|port.port().iter(scope) + .map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes))) + .collect::>()) + .collect::>() + } + fn midi_ins_with_sizes <'a> (&'a self) -> + impl Iterator, &'a [Connect], usize, usize)> + Send + Sync + 'a + { + let mut y = 0; + self.midi_ins().iter().enumerate().map(move|(i, input)|{ + let height = 1 + input.connections().len(); + let data = (i, input.port_name(), input.connections(), y, y + height); + y += height; + data + }) + } +} +/// Trait for thing that may output MIDI. +pub trait HasMidiOuts { + fn midi_outs (&self) -> &Vec; + fn midi_outs_mut (&mut self) -> &mut Vec; + fn midi_outs_with_sizes <'a> (&'a self) -> + impl Iterator, &'a [Connect], usize, usize)> + Send + Sync + 'a + { + let mut y = 0; + self.midi_outs().iter().enumerate().map(move|(i, output)|{ + let height = 1 + output.connections().len(); + let data = (i, output.port_name(), output.connections(), y, y + height); + y += height; + data + }) + } + fn midi_outs_emit (&mut self, scope: &ProcessScope) { + for port in self.midi_outs_mut().iter_mut() { + port.buffer_emit(scope) + } + } +} + +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 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 View for MidiInput { + fn view (&self) -> impl Draw { + "TODO: MIDI IN" + } +} + +impl View for MidiOutput { + fn view (&self) -> impl Draw { + "TODO: MIDI OUT" + } +} + +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 MidiInput { + pub fn parsed <'a> (&'a self, scope: &'a ProcessScope) -> impl Iterator, &'a [u8])> { + parse_midi_input(self.port().iter(scope)) + } +} +impl> + AsMut>> HasMidiIns for T { + fn midi_ins (&self) -> &Vec { self.as_ref() } + fn midi_ins_mut (&mut self) -> &mut Vec { self.as_mut() } +} +impl> + AsMut>> HasMidiOuts for T { + fn midi_outs (&self) -> &Vec { self.as_ref() } + fn midi_outs_mut (&mut self) -> &mut Vec { self.as_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(()) + } +} +/// 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(()) + } +} + +/// May create new MIDI input ports. +pub trait AddMidiIn { + fn midi_in_add (&mut self) -> Usually<()>; +} + +/// May create new MIDI output ports. +pub trait AddMidiOut { + fn midi_out_add (&mut self) -> Usually<()>; +} diff --git a/src/sample.rs b/src/device/sample.rs similarity index 99% rename from src/sample.rs rename to src/device/sample.rs index 6b6fa4a9..b9581142 100644 --- a/src/sample.rs +++ b/src/device/sample.rs @@ -4,10 +4,10 @@ def_command!(SamplerCommand: |sampler: Sampler| { RecordToggle { slot: usize } => { let slot = *slot; let recording = sampler.recording.as_ref().map(|x|x.0); - let _ = Self::RecordFinish.execute(sampler)?; + let _ = Self::RecordFinish.act(sampler)?; // autoslice: continue recording at next slot if recording != Some(slot) { - Self::RecordBegin { slot }.execute(sampler) + Self::RecordBegin { slot }.act(sampler) } else { Ok(None) } diff --git a/src/sequence.rs b/src/device/sequence.rs similarity index 100% rename from src/sequence.rs rename to src/device/sequence.rs diff --git a/src/mode.rs b/src/mode.rs deleted file mode 100644 index 8577d80b..00000000 --- a/src/mode.rs +++ /dev/null @@ -1,99 +0,0 @@ -use crate::{*, config::*}; - -impl Config { - pub fn get_mode (&self, mode: impl AsRef) -> Option>>> { - self.modes.clone().read().unwrap().get(mode.as_ref()).cloned() - } -} - -pub(crate) fn load_mode (modes: &Modes, name: &impl AsRef, body: &impl Language) -> Usually<()> { - let mut mode = Mode::default(); - body.each(|item|mode.add(item))?; - modes.write().unwrap().insert(name.as_ref().into(), Arc::new(mode)); - Ok(()) -} - -/// Collection of interaction modes. -pub type Modes = Arc, Arc>>>>>; - -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()); - })) - } -} -/// Group of view and keys definitions. -/// -/// ``` -/// let mode = tek::Mode::>::default(); -/// ``` -#[derive(Default, Debug)] pub struct Mode { - pub path: PathBuf, - pub name: Vec, - pub info: Vec, - pub view: Vec, - pub keys: Vec, - pub modes: Modes, -} - diff --git a/src/tek.rs b/src/tek.rs index f2f63594..d5060d73 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -1,9 +1,37 @@ #![allow(clippy::unit_arg)] #![feature( - adt_const_params, associated_type_defaults, closure_lifetime_binder, + adt_const_params, associated_type_defaults, anonymous_lifetime_in_impl_trait, closure_lifetime_binder, impl_trait_in_assoc_type, trait_alias, type_alias_impl_trait, type_changing_struct_update )] +/// CLI banner. +pub(crate) const HEADER: &'static str = r#" + +~ β–ˆβ–€β–ˆβ–€β–ˆ β–ˆβ–€β–€β–ˆ β–ˆ β–ˆ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~ + β–ˆ β–ˆβ–€ β–ˆβ–€β–€β–„ ~ v0.4.0, 2026 winter (or is it) ~ + ~ β–€ β–ˆβ–€β–€β–ˆ β–€ β–€ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ "#; + +#[cfg(feature = "cli")] use clap::{self, Parser, Subcommand}; + +pub extern crate atomic_float; + +pub extern crate xdg; +pub(crate) use ::xdg::BaseDirectories; + +pub extern crate midly; +pub(crate) use ::midly::{Smf, TrackEventKind, MidiMessage, Error as MidiError, num::*, live::*}; + +pub extern crate tengri; +pub(crate) use tengri::{ + *, lang::*, exit::*, eval::*, keys::*, sing::*, time::*, draw::*, term::*, color::*, + crossterm::event::{Event, KeyEvent}, + ratatui::{ + self, + prelude::{Rect, Style, Stylize, Buffer, Color::{self, *}}, + widgets::{Widget, canvas::{Canvas, Line}}, + }, +}; + /// 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}) => { @@ -25,76 +53,6 @@ } } -/// TODO: Preserve the generic passthru syntax; -/// remove this macro (only used twice) and potentially the trait. -#[macro_export] macro_rules! impl_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() - } - } - } -} - -pub mod arrange; -pub mod bind; -pub mod browse; -pub mod cli; -pub mod clock; -pub mod config; -pub mod device; -pub mod dialog; -pub mod editor; -pub mod menu; -pub mod mix; -pub mod mode; -pub mod sample; -pub mod sequence; -pub mod select; -pub mod view; - -#[cfg(feature = "plugin")] pub mod plugin; - -use clap::{self, Parser, Subcommand}; -use self::{ - config::*, mode::*, view::*, bind::*, - dialog::*, browse::*, menu::*, - clock::*, sequence::*, editor::*, - arrange::*, select::*, device::*, sample::*, -}; - -extern crate xdg; -pub(crate) use ::xdg::BaseDirectories; -pub extern crate atomic_float; -//pub(crate) use atomic_float::AtomicF64; -//pub extern crate jack; -//pub(crate) use ::jack::{*, contrib::{*, ClosureProcessHandler}}; -pub extern crate midly; -pub(crate) use ::midly::{Smf, TrackEventKind, MidiMessage, Error as MidiError, num::*, live::*}; -pub extern crate tengri; -pub(crate) use tengri::{ - *, - lang::*, - exit::*, - eval::*, - keys::*, - sing::*, - time::*, - draw::*, - term::*, - color::*, - space::*, - crossterm::event::{Event, KeyEvent}, - ratatui::{ - self, - prelude::{Rect, Style, Stylize, Buffer, Color::{self, *}}, - widgets::{Widget, canvas::{Canvas, Line}}, - }, -}; #[cfg(feature = "sampler")] pub(crate) use symphonia::{ default::get_codecs, core::{//errors::Error as SymphoniaError, @@ -102,6 +60,7 @@ pub(crate) use tengri::{ codecs::{Decoder, CODEC_TYPE_NULL}, }, }; + #[cfg(feature = "lv2_gui")] use ::winit::{ application::ApplicationHandler, event::WindowEvent, @@ -109,6 +68,7 @@ pub(crate) use tengri::{ window::{Window, WindowId}, platform::x11::EventLoopBuilderExtX11 }; + #[allow(unused)] pub(crate) use ::{ std::{ cmp::Ord, @@ -126,163 +86,23 @@ pub(crate) use tengri::{ }; /// Command-line entrypoint. -#[cfg(feature = "cli")] pub fn main () -> Usually<()> { +#[cfg(feature = "cli")] +pub fn main () -> Usually<()> { Config::watch(|config|{ Exit::enter(|exit|{ Jack::connect("tek", |jack|{ - let state = Arc::new(RwLock::new(App { - color: ItemTheme::random(), - config: Config::init(), - dialog: Dialog::welcome(), - jack: jack.clone(), - mode: ":menu", - project: Arrangement::new(&jack, &Clock::new(&jack, 51)), - ..Default::default() - })); - // TODO: Sync these timings with main clock, so that things - // "accidentally" fall on the beat in overload conditions. - let keyboard = tui_keyboard(&exit, &state, Duration::from_millis(100))?; + let project = Arrangement::new(&jack, &Clock::new(&jack, 51)); + let state = App::new_shared(jack, config, project, ":menu"); + let keyboard = tui_input(&exit, &state, Duration::from_millis(100))?; let terminal = tui_output(&exit, &state, Duration::from_millis(10))?; (keyboard, terminal) + // TODO: Sync I/O timings with main clock, so that things + // "accidentally" fall on the beat in overload conditions. }) }) }) } -/// Create a new application from a backend, project, config, and mode -/// -/// ``` -/// let jack = tek::tengri::Jack::new(&"test_tek").expect("failed to connect to jack"); -/// let proj = tek::Arrangement::default(); -/// let mut conf = tek::Config::default(); -/// conf.add("(mode hello)"); -/// let tek = tek::tek(&jack, proj, conf, "hello"); -/// ``` -pub fn tek ( - jack: &Jack<'static>, project: Arrangement, config: Config, mode: impl AsRef -) -> App { - let mode: &str = mode.as_ref(); - App { - color: ItemTheme::random(), - dialog: Dialog::welcome(), - jack: jack.clone(), - mode: config.get_mode(mode).expect(&format!("failed to find mode '{mode}'")), - config, - project, - ..Default::default() - } -} - -fn tek_confirm (state: &mut App) -> Perhaps { - Ok(match &state.dialog { - Dialog::Menu(index, items) => { - let callback = items.0[*index].1.clone(); - callback(state)?; - None - }, - _ => todo!(), - }) -} - -fn tek_inc (state: &mut App, axis: &ControlAxis) -> Perhaps { - Ok(match (&state.dialog, axis) { - (Dialog::None, _) => todo!(), - (Dialog::Menu(_, _), ControlAxis::Y) => AppCommand::SetDialog { dialog: state.dialog.menu_next() } - .act(state)?, - _ => todo!() - }) -} - -fn tek_dec (state: &mut App, axis: &ControlAxis) -> Perhaps { - Ok(match (&state.dialog, axis) { - (Dialog::None, _) => None, - (Dialog::Menu(_, _), ControlAxis::Y) => AppCommand::SetDialog { dialog: state.dialog.menu_prev() } - .act(state)?, - _ => todo!() - }) -} - -//impl_handle!(TuiIn: |self: App, input|{ - //let commands = tek_collect_commands(self, input)?; - //let history = tek_execute_commands(self, commands)?; - //self.history.extend(history.into_iter()); - //Ok(None) -//}); -//fn tek_collect_commands (app: &App, input: &TuiIn) -> Usually> { - //let mut commands = vec![]; - //for id in app.mode.keys.iter() { - //if let Some(event_map) = app.config.binds.clone().read().unwrap().get(id.as_ref()) - //&& let Some(bindings) = event_map.query(input.event()) { - //for binding in bindings { - //for command in binding.commands.iter() { - //if let Some(command) = app.namespace(command)? as Option { - //commands.push(command) - //} - //} - //} - //} - //} - //Ok(commands) -//} -//fn tek_execute_commands ( - //app: &mut App, commands: Vec -//) -> Usually)>> { - //let mut history = vec![]; - //for command in commands.into_iter() { - //let result = command.act(app); - //match result { Err(err) => { history.push((command, None)); return Err(err) } - //Ok(undo) => { history.push((command, undo)); } }; - //} - //Ok(history) -//} - -pub fn tek_jack_process (app: &mut App, client: &Client, scope: &ProcessScope) -> Control { - let t0 = app.perf.get_t0(); - app.clock().update_from_scope(scope).unwrap(); - let midi_in = app.project.midi_input_collect(scope); - if let Some(editor) = &app.editor() { - let mut pitch: Option = None; - for port in midi_in.iter() { - for event in port.iter() { - if let (_, Ok(LiveEvent::Midi {message: MidiMessage::NoteOn {key, ..}, ..})) - = event - { - pitch = Some(key.clone()); - } - } - } - if let Some(pitch) = pitch { - editor.set_note_pos(pitch.as_int() as usize); - } - } - let result = app.project.process_tracks(client, scope); - app.perf.update_from_jack_scope(t0, scope); - result -} - -pub fn tek_jack_event (app: &mut App, event: JackEvent) { - use JackEvent::*; - match event { - SampleRate(sr) => { app.clock().timebase.sr.set(sr as f64); }, - PortRegistration(_id, true) => { - //let port = app.jack().port_by_id(id); - //println!("\rport add: {id} {port:?}"); - //println!("\rport add: {id}"); - }, - PortRegistration(_id, false) => { - /*println!("\rport del: {id}")*/ - }, - PortsConnected(_a, _b, true) => { /*println!("\rport conn: {a} {b}")*/ }, - PortsConnected(_a, _b, false) => { /*println!("\rport disc: {a} {b}")*/ }, - ClientRegistration(_id, true) => {}, - ClientRegistration(_id, false) => {}, - ThreadInit => {}, - XRun => {}, - GraphReorder => {}, - _ => { panic!("{event:?}"); } - } -} - pub fn swap_value ( target: &mut T, value: &T, returned: impl Fn(T)->U ) -> Perhaps { @@ -307,18 +127,9 @@ pub fn toggle_bool ( } } - //take!(DeviceCommand|state: Arrangement, iter|state.selected_device().as_ref() //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); -#[macro_export] macro_rules! has_clip { - (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? HasMidiClip for $Struct $(<$($L),*$($T),*>)? { - fn clip (&$self) -> Option>> { $cb } - } - } -} - fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { let (mut subdirs, mut files) = std::fs::read_dir(dir)? .fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{ @@ -340,17 +151,6 @@ pub(crate) fn track_width (_index: usize, track: &Track) -> u16 { track.width as u16 } -def_command!(AppCommand: |app: App| { - Nop => Ok(None), - Confirm => tek_confirm(app), - Cancel => todo!(), // TODO delegate: - Inc { axis: ControlAxis } => tek_inc(app, axis), - Dec { axis: ControlAxis } => tek_dec(app, axis), - SetDialog { dialog: Dialog } => { - swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog }) - }, -}); - /// Define a type alias for iterators of sized items (columns). macro_rules! def_sizes_iter { ($Type:ident => $($Item:ty),+) => { @@ -358,341 +158,11 @@ macro_rules! def_sizes_iter { Iterator + Send + Sync + 'a; } } -def_sizes_iter!(InputsSizes => MidiInput); -def_sizes_iter!(OutputsSizes => MidiOutput); -def_sizes_iter!(PortsSizes => Arc, [Connect]); -def_sizes_iter!(ScenesSizes => Scene); -def_sizes_iter!(TracksSizes => Track); -/// ``` -/// let _ = tek::view_logo(); -/// ``` -pub fn view_logo () -> impl Draw { - wh_exact(Some(32), Some(7), bold(true, fg(Rgb(240, 200, 180), south!{ - h_exact(1, ""), - h_exact(1, ""), - h_exact(1, "~~ ╓─β•₯─╖ ╓──╖ β•₯ β•– ~~~~~~~~~~~~"), - h_exact(1, east("~~~~ β•‘ ~ β•Ÿβ”€β•Œ ~β•Ÿβ”€< ~~ ", east(fg(Rgb(230,100,40), "v0.3.0"), " ~~"))), - h_exact(1, "~~~~ ╨ ~ β•™β”€β”€β•œ ╨ β•œ ~~~~~~~~~~~~"), - }))) -} - -/// ``` -/// let x = std::sync::Arc::>::default(); -/// let _ = tek::view_transport(true, x.clone(), x.clone(), x.clone()); -/// let _ = tek::view_transport(false, x.clone(), x.clone(), x.clone()); -/// ``` -pub fn view_transport (play: bool, bpm: &str, beat: &str, time: &str) -> impl Draw { - let theme = ItemTheme::G[96]; - bg(Black, east!(above( - wh_full(origin_w(button_play_pause(play))), - wh_full(origin_e(east!( - field_h(theme, "BPM", bpm), - field_h(theme, "Beat", beat), - field_h(theme, "Time", time), - ))) - ))) -} - -/// ``` -/// let x = std::sync::Arc::>::default(); -/// let _ = tek::view_status(None, x.clone(), x.clone(), x.clone()); -/// let _ = tek::view_status(Some("".into()), x.clone(), x.clone(), x.clone()); -/// ``` -pub fn view_status (sel: Option<&str>, sr: &str, buf: &str, lat: &str) -> impl Draw { - let theme = ItemTheme::G[96]; - let sr = field_h(theme, "SR", sr); - let buf = field_h(theme, "Buf", buf); - let lat = field_h(theme, "Lat", lat); - bg(Black, east!(above( - wh_full(origin_w(sel.map(|sel|field_h(theme, "Selected", sel)))), - wh_full(origin_e(east!(sr, buf, lat))), - ))) -} - -/// ``` -/// let _ = tek::button_play_pause(true); -/// ``` -pub fn button_play_pause (playing: bool) -> impl Draw { - let compact = true;//self.is_editing(); - bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, - either(compact, - thunk(move|to: &mut Tui|w_exact(9, either(playing, - fg(Rgb(0, 255, 0), " PLAYING "), - fg(Rgb(255, 128, 0), " STOPPED ")) - ).draw(to)), - thunk(move|to: &mut Tui|w_exact(5, either(playing, - fg(Rgb(0, 255, 0), south(" πŸ­πŸ­‘πŸ¬½ ", " 🭞🭜🭘 ",)), - fg(Rgb(255, 128, 0), south(" β–—β–„β–– ", " β–β–€β–˜ ",))) - ).draw(to)), - ) - ) -} - -#[cfg(feature = "track")] pub fn view_track_row_section ( - _theme: ItemTheme, - button: impl Draw, - button_add: impl Draw, - content: impl Draw, -) -> impl Draw { - west(h_full(w_exact(4, origin_nw(button_add))), - east(w_exact(20, h_full(origin_nw(button))), wh_full(origin_c(content)))) -} - -/// ``` -/// let bg = tengri::ratatui::style::Color::Red; -/// let fg = tengri::ratatui::style::Color::Green; -/// let _ = tek::view_wrap(bg, fg, "and then blue, too!"); -/// ``` -pub fn view_wrap (bg: Color, fg: Color, content: impl Draw) -> impl Draw { - let left = fg_bg(bg, Reset, w_exact(1, y_repeat("▐"))); - let right = fg_bg(bg, Reset, w_exact(1, y_repeat("β–Œ"))); - east(left, west(right, fg_bg(fg, bg, content))) -} - -/// ``` -/// let _ = tek::view_meter("", 0.0); -/// let _ = tek::view_meters(&[0.0, 0.0]); -/// ``` -pub fn view_meter <'a> (label: &'a str, value: f32) -> impl Draw + 'a { - let f = field_h(ItemTheme::G[128], label, format!("{:>+9.3}", value)); - let w = if value >= 0.0 { 13 } - else if value >= -1.0 { 12 } - else if value >= -2.0 { 11 } - else if value >= -3.0 { 10 } - else if value >= -4.0 { 9 } - else if value >= -6.0 { 8 } - else if value >= -9.0 { 7 } - else if value >= -12.0 { 6 } - else if value >= -15.0 { 5 } - else if value >= -20.0 { 4 } - else if value >= -25.0 { 3 } - else if value >= -30.0 { 2 } - else if value >= -40.0 { 1 } - else { 0 }; - let c = if value >= 0.0 { Red } - else if value >= -3.0 { Yellow } - else { Green }; - south!(f, wh_exact(Some(w), Some(1), bg(c, ()))) -} - -pub fn view_meters (values: &[f32;2]) -> impl Draw + use<'_> { - let left = format!("L/{:>+9.3}", values[0]); - let right = format!("R/{:>+9.3}", values[1]); - south(left, right) -} - -pub fn draw_info (sample: Option<&Arc>>) -> impl Draw + use<'_> { - when(sample.is_some(), thunk(move|to: &mut Tui|{ - let sample = sample.unwrap().read().unwrap(); - let theme = sample.color; - east!( - field_h(theme, "Name", format!("{:<10}", sample.name.clone())), - field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())), - field_h(theme, "Start", format!("{:<8}", sample.start)), - field_h(theme, "End", format!("{:<8}", sample.end)), - field_h(theme, "Trans", "0"), - field_h(theme, "Gain", format!("{}", sample.gain)), - ).draw(to) - })) -} - -pub fn draw_info_v (sample: Option<&Arc>>) -> impl Draw + use<'_> { - let a = thunk(move|to: &mut Tui|{ - let sample = sample.unwrap().read().unwrap(); - let theme = sample.color; - w_exact(20, south!( - w_full(origin_w(field_h(theme, "Name ", format!("{:<10}", sample.name.clone())))), - w_full(origin_w(field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())))), - w_full(origin_w(field_h(theme, "Start ", format!("{:<8}", sample.start)))), - w_full(origin_w(field_h(theme, "End ", format!("{:<8}", sample.end)))), - w_full(origin_w(field_h(theme, "Trans ", "0"))), - w_full(origin_w(field_h(theme, "Gain ", format!("{}", sample.gain)))), - )).draw(to) - }); - - let b = thunk(|to: &mut Tui|fg(Red, south!( - bold(true, "Γ— No sample."), - "[r] record", - "[Shift-F9] import", - )).draw(to)); - - either(sample.is_some(), a, b) -} - -pub fn draw_status (sample: Option<&Arc>>) -> impl Draw { - bold(true, fg(g(224), sample - .map(|sample|{ - let sample = sample.read().unwrap(); - format!("Sample {}-{}", sample.start, sample.end) - }) - .unwrap_or_else(||"No sample".to_string()))) -} - -pub fn view_track_header (theme: ItemTheme, content: impl Draw) -> impl Draw { - w_exact(12, bg(theme.darker.term, w_full(origin_e(content)))) -} - -pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, ports: &'a [T]) - -> impl Draw + use<'a, T> -{ - let ins = ports.len() as u16; - let frame = Outer(true, Style::default().fg(g(96))); - let iter = move||ports.iter(); - let names = iter_south(iter, move|port, index|h_full(origin_w(format!(" {index} {}", port.port_name())))); - let field = field_v(theme, title, names); - wh_exact(Some(20), Some(1 + ins), border(true, frame, wh_exact(Some(20), Some(1 + ins), field))) -} - -pub fn io_ports <'a, T: PortsSizes<'a>> ( - fg: Color, bg: Color, items: impl Fn()->T + Send + Sync + 'a -) -> impl Draw + 'a { - type Item<'a> = (usize, &'a Arc, &'a [Connect], usize, usize); - iter(items, move|(_index, name, connections, y, y2): Item<'a>, _| { - y_push(y as u16, h_exact((y2-y) as u16, - south(h_full(bold(true, fg_bg(fg, bg, origin_w(east(" σ°£² ", name))))), - iter(||connections.iter(), move|connect: &'a Connect, index|y_push( - index as u16, h_exact(1, origin_w(bold(false, fg_bg(fg, bg, &connect.info)))) - ))))) - }) -} - -/// CLI banner. -pub(crate) const HEADER: &'static str = r#" - -~ β–ˆβ–€β–ˆβ–€β–ˆ β–ˆβ–€β–€β–ˆ β–ˆ β–ˆ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~ - β–ˆ β–ˆβ–€ β–ˆβ–€β–€β–„ ~ v0.4.0, 2026 winter (or is it) ~ - ~ β–€ β–ˆβ–€β–€β–ˆ β–€ β–€ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ "#; - -/// Total state -/// -/// ``` -/// use tek::{HasTracks, HasScenes, TracksView, ScenesView}; -/// let mut app = tek::App::default(); -/// let _ = app.scene_add(None, None).unwrap(); -/// let _ = app.update_clock(); -/// app.project.editor = Some(Default::default()); -/// //let _: Vec<_> = app.project.inputs_with_sizes().collect(); -/// //let _: Vec<_> = app.project.outputs_with_sizes().collect(); -/// let _: Vec<_> = app.project.tracks_with_sizes().collect(); -/// //let _: Vec<_> = app.project.scenes_with_sizes(true, 10, 10).collect(); -/// //let _: Vec<_> = app.scenes_with_colors(true, 10).collect(); -/// //let _: Vec<_> = app.scenes_with_track_colors(true, 10, 10).collect(); -/// let _ = app.project.w(); -/// //let _ = app.project.w_sidebar(); -/// //let _ = app.project.w_tracks_area(); -/// let _ = app.project.h(); -/// //let _ = app.project.h_tracks_area(); -/// //let _ = app.project.h_inputs(); -/// //let _ = app.project.h_outputs(); -/// let _ = app.project.h_scenes(); -/// ``` -#[derive(Default, Debug)] pub struct App { - /// Base color. - pub color: ItemTheme, - /// Must not be dropped for the duration of the process - pub jack: Jack<'static>, - /// Display size - pub size: Size, - /// Performance counter - pub perf: PerfModel, - /// Available view modes and input bindings - pub config: Config, - /// Currently selected mode - pub mode: Arc>>, - /// Undo history - pub history: Vec<(AppCommand, Option)>, - /// Dialog overlay - pub dialog: Dialog, - /// Contains all recently created clips. - pub pool: Pool, - /// Contains the currently edited musical arrangement - pub project: Arrangement, - /// Error, if any - pub error: Arc>>> -} -impl_has!(Clock: |self: App|self.project.clock); -impl_has!(Vec: |self: App|self.project.midi_ins); -impl_has!(Vec: |self: App|self.project.midi_outs); -impl_has!(Dialog: |self: App|self.dialog); -impl_has!(Jack<'static>: |self: App|self.jack); -impl_has!(Size: |self: App|self.size); -impl_has!(Pool: |self: App|self.pool); -impl_has!(Selection: |self: App|self.project.selection); -impl_as_ref!(Vec: |self: App|self.project.as_ref()); -impl_as_mut!(Vec: |self: App|self.project.as_mut()); -impl_as_ref_opt!(MidiEditor: |self: App|self.project.as_ref_opt()); -impl_as_mut_opt!(MidiEditor: |self: App|self.project.as_mut_opt()); -impl_has_clips!( |self: App|self.pool.clips); -impl_audio!(App: tek_jack_process, tek_jack_event); -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.size.h() 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), }; }); -namespace!(App: bool { symbol = |app| { // Provide boolean values. - ":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), -}; }); -namespace!(App: ItemTheme {}); // TODO: provide colors here -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 - } - }; -}); - -pub trait HasClipsSize { fn clips_size (&self) -> &Size; } - -pub trait HasDevices: AsRef> + AsMut> { - fn devices (&self) -> &Vec { self.as_ref() } - fn devices_mut (&mut self) -> &mut Vec { self.as_mut() } -} +primitive!(u8: try_to_u8); +primitive!(u16: try_to_u16); +primitive!(usize: try_to_usize); +primitive!(isize: try_to_isize); pub trait HasWidth { const MIN_WIDTH: usize; /// Increment track width. @@ -701,301 +171,11 @@ pub trait HasWidth { fn width_dec (&mut self); } -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 Interpret for App { - fn interpret_expr <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Usually<()> { - app_interpret_expr(self, to, lang) - } - fn interpret_word <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Usually<()> { - app_interpret_word(self, to, lang) - } -} -fn app_interpret_expr (state: &App, to: &mut Tui, lang: &impl Expression) -> Usually<()> { - if eval_view(state, to, lang)? || eval_view_tui(state, to, lang)? { - Ok(()) - } else { - Err(format!("App::interpret_expr: unexpected: {lang:?}").into()) - } -} -fn app_interpret_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usually<()> { - let mut frags = dsl.src()?.unwrap().split("/"); - match frags.next() { +pub mod device; pub use self::device::*; +pub mod app; pub use self::app::*; - Some(":logo") => view_logo().draw(to), - - Some(":status") => h_exact(1, "TODO: Status Bar").draw(to), - - Some(":meters") => match frags.next() { - Some("input") => bg(Rgb(30, 30, 30), h_full(origin_s("Input Meters"))).draw(to), - Some("output") => bg(Rgb(30, 30, 30), h_full(origin_s("Output Meters"))).draw(to), - _ => panic!() - }, - - Some(":tracks") => match frags.next() { - None => "TODO tracks".draw(to), - Some("names") => state.project.view_track_names(state.color.clone()).draw(to),//bg(Rgb(40, 40, 40), w_full(origin_w("Track Names")))), - Some("inputs") => bg(Rgb(40, 40, 40), w_full(origin_w("Track Inputs"))).draw(to), - Some("devices") => bg(Rgb(40, 40, 40), w_full(origin_w("Track Devices"))).draw(to), - Some("outputs") => bg(Rgb(40, 40, 40), w_full(origin_w("Track Outputs"))).draw(to), - _ => panic!() - }, - - Some(":scenes") => match frags.next() { - None => "TODO scenes".draw(to), - Some(":scenes/names") => "TODO Scene Names".draw(to), - _ => panic!() - }, - - Some(":editor") => "TODO Editor".draw(to), - - Some(":dialog") => match frags.next() { - Some("menu") => if let Dialog::Menu(selected, items) = &state.dialog { - let items = items.clone(); - let selected = selected; - Some(wh_full(thunk(move|to: &mut Tui|{ - for (index, MenuItem(item, _)) in items.0.iter().enumerate() { - to.place(&y_push((2 * index) as u16, - 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) }, - h_exact(2, origin_n(w_full(item))) - ))); - } - }))) - } else { - None - }.draw(to), - _ => unimplemented!("App::interpret_word: {dsl:?} ({frags:?})"), - }, - - Some(":templates") => { - let modes = state.config.modes.clone(); - let height = (modes.read().unwrap().len() * 2) as u16; - h_exact(height, w_min(Some(30), thunk(move |to: &mut Tui|{ - 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 = w_full(origin_w(fg(fg1, name))); - let field_id = w_full(origin_e(fg(fg2, id))); - let field_info = w_full(origin_w(info)); - y_push((2 * index) as u16, - h_exact(2, w_full(bg(bg, south( - above(field_name, field_id), field_info))))).draw(to); - } - }))) - }.draw(to), - - Some(":sessions") => h_exact(Some(6), w_min(30, thunk(|to: &mut Tui|{ - 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) }; - y_push((2 * index) as u16, - &h_exact(2, w_full(bg(bg, origin_w(fg(fg, name)))))).draw(to); - } - }))).draw(to), - - Some(":browse/title") => w_full(origin_w(field_v(ItemColor::default(), - match state.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:", - }, w_shrink(3, h_exact(1, fg(g(96), x_repeat("🭻"))))))).draw(to), - - Some(":device") => { - let selected = state.dialog.device_kind().unwrap(); - south(bold(true, "Add device"), iter_south( - move||device_kinds().iter(), - move|_label: &&'static str, i|{ - let b = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }; - let l = if i == selected { "[ " } else { " " }; - let r = if i == selected { " ]" } else { " " }; - w_full(bg(b, east(l, west(r, "FIXME device name")))) })).draw(to) - }, - - Some(":debug") => h_exact(1, format!("[{:?}]", to.area())).draw(to), - - Some(_) => { - let views = state.config.views.read().unwrap(); - if let Some(dsl) = views.get(dsl.src()?.unwrap()) { - let dsl = dsl.clone(); - std::mem::drop(views); - state.interpret(to, &dsl)? - } else { - unimplemented!("{dsl:?}"); - } - }, - - _ => unreachable!() - } - Ok(()) -} -impl App { - /// Update memoized render of clock values. - /// ``` - /// tek::App::default().update_clock(); - /// ``` - pub fn update_clock (&self) { - ClockView::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80) - } - - /// Set modal dialog. - /// - /// ``` - /// let previous: tek::Dialog = tek::App::default().set_dialog(tek::Dialog::welcome()); - /// ``` - pub fn set_dialog (&mut self, mut dialog: Dialog) -> Dialog { - std::mem::swap(&mut self.dialog, &mut dialog); - dialog - } - - /// FIXME: generalize. Set picked device in device pick dialog. - /// - /// ``` - /// tek::App::default().device_pick(0); - /// ``` - pub fn device_pick (&mut self, index: usize) { - self.dialog = Dialog::Device(index); - } - - /// FIXME: generalize. Add device to current track. - 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. - /// - /// ``` - /// assert_eq!(tek::App::default().browser(), None); - /// ``` - pub fn browser (&self) -> Option<&Browse> { - if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None } - } - - /// Is a MIDI editor currently focused? - /// - /// ``` - /// tek::App::default().editor_focused(); - /// ``` - pub fn editor_focused (&self) -> bool { - false - } - - /// Toggle MIDI editor. - /// - /// ``` - /// tek::App::default().toggle_editor(None); - /// ``` - 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, 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()); - } - } - } -} - -/// The [Draw] implementation for [App] handles the loaded view, -/// which is defined in terms of [dizzle] DSL. -/// -/// If there is an error, the error is displayed. FIXME: overlay it -/// Then, every top-level form of the DSL description is rendered. -impl Draw for App { - fn draw (self, to: &mut Tui) -> Usually> { - let xywh = to.area().into(); - if let Some(e) = self.error.read().unwrap().as_ref() { - to.show(xywh, e.as_ref()); - } - for (index, dsl) in self.mode.view.iter().enumerate() { - if let Err(e) = self.interpret(to, dsl) { - *self.error.write().unwrap() = Some(format!("view #{index}: {e}").into()); - break; - } - } - Ok(xywh) - } -} - -impl HasClipsSize for App { fn clips_size (&self) -> &Size { &self.project.size_inner } } -impl HasJack<'static> for App { fn jack (&self) -> &Jack<'static> { &self.jack } } -impl_default!(AppCommand: Self::Nop); -primitive!(u8: try_to_u8); -primitive!(u16: try_to_u16); -primitive!(usize: try_to_usize); -primitive!(isize: try_to_isize); - -fn field_h (theme: ItemTheme, head: impl Draw, body: impl Draw) -> impl Draw { -} - -fn field_v (theme: ItemTheme, head: impl Draw, body: impl Draw) -> impl Draw { -} +def_sizes_iter!(InputsSizes => MidiInput); +def_sizes_iter!(OutputsSizes => MidiOutput); +def_sizes_iter!(PortsSizes => Arc, [Connect]); +def_sizes_iter!(ScenesSizes => Scene); +def_sizes_iter!(TracksSizes => Track); diff --git a/src/view.rs b/src/view.rs deleted file mode 100644 index 018515fb..00000000 --- a/src/view.rs +++ /dev/null @@ -1,10 +0,0 @@ -use crate::*; - -/// Collection of view definitions. -pub type Views = Arc, Arc>>>; - -pub(crate) fn load_view (views: &Views, name: &impl AsRef, body: &impl Language) -> Usually<()> { - views.write().unwrap().insert(name.as_ref().into(), body.src()?.unwrap_or_default().into()); - Ok(()) -} -