diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..7b08ef33 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,3 @@ +root = true +[*] +max_line_length = 132 diff --git a/Cargo.lock b/Cargo.lock index 4959d05a..34c84187 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2395,6 +2395,7 @@ dependencies = [ "proptest-derive", "rand 0.8.5", "symphonia", + "tek_device", "tengri", "toml", "uuid", @@ -2403,6 +2404,43 @@ dependencies = [ "xdg", ] +[[package]] +name = "tek_device" +version = "0.3.0" +dependencies = [ + "atomic_float", + "backtrace", + "clap", + "jack", + "konst", + "livi", + "midly", + "palette", + "proptest", + "proptest-derive", + "rand 0.8.5", + "symphonia", + "tek_engine", + "tengri", + "toml", + "uuid", + "wavers", + "winit", + "xdg", +] + +[[package]] +name = "tek_engine" +version = "0.3.0" +dependencies = [ + "atomic_float", + "jack", + "midly", + "proptest", + "proptest-derive", + "tengri", +] + [[package]] name = "tempfile" version = "3.21.0" diff --git a/Cargo.toml b/Cargo.toml index 396c69f5..9f379dc6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = [ "./tek" ] +members = [ "./app", "./engine", "./device" ] exclude = [ "./deps/tengri" ] [workspace.package] diff --git a/tek/Cargo.toml b/app/Cargo.toml similarity index 97% rename from tek/Cargo.toml rename to app/Cargo.toml index 656b182b..d6c3afa9 100644 --- a/tek/Cargo.toml +++ b/app/Cargo.toml @@ -14,6 +14,8 @@ path = "tek_cli.rs" rustflags = ["-C", "link-arg=-fuse-ld=mold"] [dependencies] +tek_device = { path = "../device" } + atomic_float = { workspace = true } backtrace = { workspace = true } clap = { workspace = true, optional = true } diff --git a/tek/tek.edn b/app/tek.edn similarity index 91% rename from tek/tek.edn rename to app/tek.edn index 225def70..17b02a6c 100644 --- a/tek/tek.edn +++ b/app/tek.edn @@ -30,57 +30,82 @@ (@closebrace w2/inc)) (mode :menu (keys :axis/y :confirm) :menu) + (keys :confirm (@enter confirm)) + (view :menu (bg (g 0) (bsp/s - :ports/out - (bsp/n :ports/in (bg (g 30) (bsp/s (fixed/y 7 :logo) (fill :dialog/menu))))))) + :ports/out + (bsp/n :ports/in (bg (g 30) (bsp/s (fixed/y 7 :logo) (fill :dialog/menu))))))) + (view :menu (bsp/s - (push/y 4 (fixed/xy 20 2 (bg (g 0) :debug))) - (fixed 20 2 (bg (g 20) (push/x 2 :debug))))) + (push/y 4 (fixed/xy 20 2 (bg (g 0) :debug))) + (fixed 20 2 (bg (g 20) (push/x 2 :debug))))) + +(view :menu (bsp/s (fixed/y 4 :debug) :debug)) + (view :ports/out (fill/x (fixed/y 3 (bsp/a - (fill/x (align/w (text L-AUDIO-OUT))) - (bsp/a (text MIDI-OUT) (fill/x (align/e (text AUDIO-OUT-R)))))))) + (fill/x (align/w (text L-AUDIO-OUT))) + (bsp/a (text MIDI-OUT) (fill/x (align/e (text AUDIO-OUT-R)))))))) + (view :ports/in (fill/x (fixed/y 3 (bsp/a - (fill/x (align/w (text L-AUDIO-IN))) - (bsp/a (text MIDI-IN) (fill/x (align/e (text AUDIO-IN-R)))))))) + (fill/x (align/w (text L-AUDIO-IN))) + (bsp/a (text MIDI-IN) (fill/x (align/e (text AUDIO-IN-R)))))))) + (view :browse (bsp/s - (padding 3 1 :browse-title) - (enclose (fg (g 96)) browser))) + (padding 3 1 :browse-title) + (enclose (fg (g 96)) browser))) + (keys :help (@f1 dialog :help)) + (keys :back (@escape back)) + (keys :page (@pgup page/up) (@pgdn page/down)) + (keys :delete (@delete delete) (@backspace delete/back)) + (keys :input (see :axis/x :delete) (:char input)) + (keys :list (see :axis/y :confirm)) + (keys :length (see :axis/x :axis/y :confirm)) + (keys :browse (see :list :input :focus)) + (keys :history (@u undo 1) (@r redo 1)) + (keys :clock (@space clock/toggle 0) (@shift/space clock/toggle 0)) + (keys :color (@c color)) + (keys :launch (@q launch)) + (keys :saveload (@f6 dialog :save) (@f9 dialog :load)) + (keys :global (see :history :saveload) (@f8 dialog :options) (@f10 dialog :quit)) + (keys :focus) + (mode :transport (name Transport) (info A JACK transport controller.) (keys :clock :global) (view :transport)) + (mode :arranger (name Arranger) (info A grid of launchable clips arranged by track and scene.) (mode :editor (keys :editor)) (mode :dialog (keys :dialog)) (mode :message (keys :message)) (mode :add-device (keys :add-device)) (mode :browse (keys :browse)) (mode :rename (keys :input)) diff --git a/app/tek.rs b/app/tek.rs new file mode 100644 index 00000000..1ce0c252 --- /dev/null +++ b/app/tek.rs @@ -0,0 +1,468 @@ +#![feature( + adt_const_params, + associated_type_defaults, + closure_lifetime_binder, + if_let_guard, + impl_trait_in_assoc_type, + trait_alias, + type_alias_impl_trait, + type_changing_struct_update, +)] + +#![allow( + clippy::unit_arg +)] + +#[cfg(test)] mod tek_test; + +mod tek_bind; pub use self::tek_bind::*; +mod tek_cfg; pub use self::tek_cfg::*; +mod tek_deps; pub use self::tek_deps::*; +mod tek_mode; pub use self::tek_mode::*; +mod tek_view; pub use self::tek_view::*; + +/// Total state +#[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: Measure, + /// 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, +} + +audio!( + |self: App, client, scope|{ + 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 + }; + |self, event|{ + 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:?}"); } + } + } +); + +// Allow source to be read as Literal string +dsl_ns!(App: Arc { + literal = |dsl|Ok(dsl.src()?.map(|x|x.into())); +}); + +// Provide boolean values. +dsl_ns!(App: bool { + // TODO literal = ... + word = |app| { + ":mode/editor" => app.project.editor.is_some(), + ":focused/dialog" => !matches!(app.dialog, Dialog::None), + ":focused/message" => matches!(app.dialog, Dialog::Message(..)), + ":focused/add_device" => matches!(app.dialog, Dialog::Device(..)), + ":focused/browser" => app.dialog.browser().is_some(), + ":focused/pool/import" => matches!(app.pool.mode, Some(PoolMode::Import(..))), + ":focused/pool/export" => matches!(app.pool.mode, Some(PoolMode::Export(..))), + ":focused/pool/rename" => matches!(app.pool.mode, Some(PoolMode::Rename(..))), + ":focused/pool/length" => matches!(app.pool.mode, Some(PoolMode::Length(..))), + ":focused/clip" => !app.editor_focused() && matches!(app.selection(), + Selection::TrackClip{..}), + ":focused/track" => !app.editor_focused() && matches!(app.selection(), + Selection::Track(..)), + ":focused/scene" => !app.editor_focused() && matches!(app.selection(), + Selection::Scene(..)), + ":focused/mix" => !app.editor_focused() && matches!(app.selection(), + Selection::Mix), + }; +}); + +// TODO: provide colors here +dsl_ns!(App: ItemTheme {}); + +dsl_ns!(App: Dialog { + word = |app| { + ":dialog/none" => Dialog::None, + ":dialog/options" => Dialog::Options, + ":dialog/device" => Dialog::Device(0), + ":dialog/device/prev" => Dialog::Device(0), + ":dialog/device/next" => Dialog::Device(0), + ":dialog/help" => Dialog::Help(0), + ":dialog/save" => Dialog::Browse(BrowseTarget::SaveProject, + Browse::new(None).unwrap().into()), + ":dialog/load" => Dialog::Browse(BrowseTarget::LoadProject, + Browse::new(None).unwrap().into()), + ":dialog/import/clip" => Dialog::Browse(BrowseTarget::ImportClip(Default::default()), + Browse::new(None).unwrap().into()), + ":dialog/export/clip" => Dialog::Browse(BrowseTarget::ExportClip(Default::default()), + Browse::new(None).unwrap().into()), + ":dialog/import/sample" => Dialog::Browse(BrowseTarget::ImportSample(Default::default()), + Browse::new(None).unwrap().into()), + ":dialog/export/sample" => Dialog::Browse(BrowseTarget::ExportSample(Default::default()), + Browse::new(None).unwrap().into()), + }; +}); + +dsl_ns!(App: Selection { + word = |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(), + }; +}); + +dsl_ns!(App: Color { + word = |app| { + ":color/bg" => Color::Rgb(28, 32, 36), + }; + expr = |app| { + "g" (n: u8) => Color::Rgb(n, n, n), + "rgb" (r: u8, g: u8, b: u8) => Color::Rgb(r, g, b), + }; +}); + +dsl_ns!(App: Option { + word = |app| { + ":editor/pitch" => Some( + (app.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into() + ) + }; +}); + +dsl_ns!(App: Option { + word = |app| { + ":selected/scene" => app.selection().scene(), + ":selected/track" => app.selection().track(), + }; +}); + +dsl_ns!(App: Option>> { + word = |app| { + ":selected/clip" => if let Selection::TrackClip { track, scene } = app.selection() { + app.scenes()[*scene].clips[*track].clone() + } else { + None + } + }; +}); + +dsl_ns!(App: u8 { + literal = |dsl|Ok(if let Some(src) = dsl.src()? { + Some(to_number(src)? as u8) + } else { + None + }); +}); + +dsl_ns!(App: u16 { + literal = |dsl|Ok(if let Some(src) = dsl.src()? { + Some(to_number(src)? as u16) + } else { + None + }); + word = |app| { + ":w/sidebar" => app.project.w_sidebar(app.editor().is_some()), + ":h/sample-detail" => 6.max(app.height() as u16 * 3 / 9), + }; +}); + +dsl_ns!(App: usize { + literal = |dsl|Ok(if let Some(src) = dsl.src()? { + Some(to_number(src)? as usize) + } else { + None + }); + word = |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), + }; +}); + +dsl_ns!(App: isize { + literal = |dsl|Ok(if let Some(src) = dsl.src()? { + Some(to_number(src)? as isize) + } else { + None + }); +}); + +has!(Jack<'static>: |self: App|self.jack); +has!(Pool: |self: App|self.pool); +has!(Dialog: |self: App|self.dialog); +has!(Clock: |self: App|self.project.clock); +has!(Option: |self: App|self.project.editor); +has!(Selection: |self: App|self.project.selection); +has!(Vec: |self: App|self.project.midi_ins); +has!(Vec: |self: App|self.project.midi_outs); +has!(Vec: |self: App|self.project.scenes); +has!(Vec: |self: App|self.project.tracks); +has!(Measure: |self: App|self.size); +has_clips!( |self: App|self.pool.clips); +maybe_has!(Track: |self: App| { MaybeHas::::get(&self.project) }; + { MaybeHas::::get_mut(&mut self.project) }); +maybe_has!(Scene: |self: App| { MaybeHas::::get(&self.project) }; + { MaybeHas::::get_mut(&mut self.project) }); + +impl HasClipsSize for App { + fn clips_size (&self) -> &Measure { &self.project.size_inner } +} +impl HasTrackScroll for App { + fn track_scroll (&self) -> usize { self.project.track_scroll() } +} +impl HasSceneScroll for App { + fn scene_scroll (&self) -> usize { self.project.scene_scroll() } +} +impl HasJack<'static> for App { + fn jack (&self) -> &Jack<'static> { &self.jack } +} +impl ScenesView for App { + fn w_side (&self) -> u16 { 20 } + fn w_mid (&self) -> u16 { (self.width() as u16).saturating_sub(self.w_side()) } + fn h_scenes (&self) -> u16 { (self.height() as u16).saturating_sub(20) } +} + + +impl App { + + pub fn editor_focused (&self) -> bool { + false + } + + pub fn toggle_dialog (&mut self, mut dialog: Dialog) -> Dialog { + std::mem::swap(&mut self.dialog, &mut dialog); + dialog + } + + pub fn toggle_editor (&mut self, value: Option) { + //FIXME: self.editing.store(value.unwrap_or_else(||!self.is_editing()), Relaxed); + let value = value.unwrap_or_else(||!self.editor().is_some()); + if value { + // Create new clip in pool when entering empty cell + if let Selection::TrackClip { track, scene } = *self.selection() + && let Some(scene) = self.project.scenes.get_mut(scene) + && let Some(slot) = scene.clips.get_mut(track) + && slot.is_none() + && let Some(track) = self.project.tracks.get_mut(track) + { + let (index, mut clip) = self.pool.add_new_clip(); + // autocolor: new clip colors from scene and track color + let color = track.color.base.mix(scene.color.base, 0.5); + clip.write().unwrap().color = ItemColor::random_near(color, 0.2).into(); + if let Some(editor) = &mut self.project.editor { + editor.set_clip(Some(&clip)); + } + *slot = Some(clip.clone()); + //Some(clip) + } else { + //None + } + } else if let Selection::TrackClip { track, scene } = *self.selection() + && let Some(scene) = self.project.scenes.get_mut(scene) + && let Some(slot) = scene.clips.get_mut(track) + && let Some(clip) = slot.as_mut() + { + // Remove clip from arrangement when exiting empty clip editor + let mut swapped = None; + if clip.read().unwrap().count_midi_messages() == 0 { + std::mem::swap(&mut swapped, slot); + } + if let Some(clip) = swapped { + self.pool.delete_clip(&clip.read().unwrap()); + } + } + } + + pub fn browser (&self) -> Option<&Browse> { + if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None } + } + + pub fn device_pick (&mut self, index: usize) { + self.dialog = Dialog::Device(index); + } + + pub fn add_device (&mut self, index: usize) -> Usually<()> { + match index { + 0 => { + let name = self.jack.with_client(|c|c.name().to_string()); + let midi = self.project.track().expect("no active track").sequencer.midi_outs[0].port_name(); + let track = self.track().expect("no active track"); + let port = format!("{}/Sampler", &track.name); + let connect = Connect::exact(format!("{name}:{midi}")); + let sampler = if let Ok(sampler) = Sampler::new( + &self.jack, &port, &[connect], &[&[], &[]], &[&[], &[]] + ) { + self.dialog = Dialog::None; + Device::Sampler(sampler) + } else { + self.dialog = Dialog::Message("Failed to add device.".into()); + return Err("failed to add device".into()) + }; + let track = self.track_mut().expect("no active track"); + track.devices.push(sampler); + Ok(()) + }, + 1 => { + todo!(); + Ok(()) + }, + _ => unreachable!(), + } + } + + pub fn update_clock (&self) { + ViewCache::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80) + } +} + +/// Various possible dialog modes. +#[derive(Debug, Clone, Default, PartialEq)] +pub enum Dialog { + #[default] None, + Help(usize), + Menu(usize, MenuItems), + Device(usize), + Message(Arc), + Browse(BrowseTarget, Arc), + Options, +} + +#[derive(Debug, Clone, Default, PartialEq)] +pub struct MenuItems(pub Arc<[MenuItem]>); + +impl AsRef> for MenuItems { + fn as_ref (&self) -> &Arc<[MenuItem]> { + &self.0 + } +} + +#[derive(Clone)] +pub struct MenuItem( + /// Label + pub Arc, + /// Callback + pub ArcUsually<()> + Send + Sync>> +); + +impl Default for MenuItem { + fn default () -> Self { Self("".into(), Arc::new(Box::new(|_|Ok(())))) } +} + +impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) }); + +impl PartialEq for MenuItem { + fn eq (&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl Dialog { + pub fn welcome () -> Self { + Self::Menu(1, MenuItems([ + MenuItem("Resume session".into(), Arc::new(Box::new(|_|Ok(())))), + MenuItem("Create new session".into(), Arc::new(Box::new(|app|Ok({ + app.dialog = Dialog::None; + app.mode = app.config.modes.clone().read().unwrap().get(":arranger").cloned().unwrap(); + })))), + MenuItem("Load old session".into(), Arc::new(Box::new(|_|Ok(())))), + ].into())) + } + pub fn menu_next (&self) -> Self { + match self { + Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.0.len()), items.clone()), + _ => Self::None + } + } + pub fn menu_prev (&self) -> Self { + match self { + Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.0.len()), items.clone()), + _ => Self::None + } + } + pub fn menu_selected (&self) -> Option { + if let Self::Menu(selected, _) = self { Some(*selected) } else { None } + } + pub fn device_kind (&self) -> Option { + if let Self::Device(index) = self { Some(*index) } else { None } + } + pub fn device_kind_next (&self) -> Option { + self.device_kind().map(|index|(index + 1) % device_kinds().len()) + } + pub fn device_kind_prev (&self) -> Option { + self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1))) + } + pub fn message (&self) -> Option<&str> { + todo!() + } + pub fn browser (&self) -> Option<&Arc> { + todo!() + } + pub fn browser_target (&self) -> Option<&BrowseTarget> { + todo!() + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +//has_editor!(|self: App|{ + //editor = self.editor; + //editor_w = { + //let size = self.size.w(); + //let editor = self.editor.as_ref().expect("missing editor"); + //let time_len = editor.time_len().get(); + //let time_zoom = editor.time_zoom().get().max(1); + //(5 + (time_len / time_zoom)).min(size.saturating_sub(20)).max(16) + //}; + //editor_h = 15; + //is_editing = self.editor.is_some(); +//}); diff --git a/app/tek_bind.rs b/app/tek_bind.rs new file mode 100644 index 00000000..84763b70 --- /dev/null +++ b/app/tek_bind.rs @@ -0,0 +1,325 @@ +use crate::*; + +pub type Binds = Arc, EventMap>>>>; + +/// A collection of input bindings. +#[derive(Debug)] +pub struct EventMap( + /// Map of each event (e.g. key combination) to + /// all command expressions bound to it by + /// all loaded input layers. + pub BTreeMap>> +); + +/// An input binding. +#[derive(Debug, Clone)] +pub struct Binding { + pub commands: Arc<[C]>, + pub condition: Option, + pub description: Option>, + pub source: Option>, +} + +/// Input bindings are only returned if this evaluates to true +#[derive(Clone)] +pub struct Condition(Arcbool + Send + Sync>>); + +/// Default is always empty map regardless if `E` and `C` implement [Default]. +impl Default for EventMap { + fn default () -> Self { Self(Default::default()) } +} + +impl EventMap { + /// Create a new event map + pub fn new () -> Self { + Default::default() + } + /// Add a binding to an owned event map. + pub fn def (mut self, event: E, binding: Binding) -> Self { + self.add(event, binding); + self + } + /// Add a binding to an event map. + pub fn add (&mut self, event: E, binding: Binding) -> &mut Self { + if !self.0.contains_key(&event) { + self.0.insert(event.clone(), Default::default()); + } + self.0.get_mut(&event).unwrap().push(binding); + self + } + /// Return the binding(s) that correspond to an event. + pub fn query (&self, event: &E) -> Option<&[Binding]> { + self.0.get(event).map(|x|x.as_slice()) + } + /// Return the first binding that corresponds to an event, considering conditions. + pub fn dispatch (&self, event: &E) -> Option<&Binding> { + self.query(event) + .map(|bb|bb.iter().filter(|b|b.condition.as_ref().map(|c|(c.0)()).unwrap_or(true)).next()) + .flatten() + } +} + +impl EventMap> { + pub fn load_into (binds: &Binds, name: &impl AsRef, body: &impl Dsl) -> Usually<()> { + println!("EventMap::load_into: {}: {body:?}", name.as_ref()); + let mut map = Self::new(); + body.each(|item|if item.expr().head() == Ok(Some("see")) { + // TODO + Ok(()) + } else if let Ok(Some(_word)) = item.expr().head().word() { + if let Some(key) = TuiEvent::from_dsl(item.expr()?.head()?)? { + map.add(key, Binding { + commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(), + condition: None, + description: None, + source: None + }); + Ok(()) + } else if Some(":char") == item.expr()?.head()? { + // TODO + return Ok(()) + } else { + return Err(format!("Config::load_bind: invalid key: {:?}", item.expr()?.head()?).into()) + } + } else { + return Err(format!("Config::load_bind: unexpected: {item:?}").into()) + })?; + binds.write().unwrap().insert(name.as_ref().into(), map); + Ok(()) + } +} + +impl Binding { + pub fn from_dsl (dsl: impl Dsl) -> Usually { + let command: Option = None; + let condition: Option = None; + let description: Option> = None; + let source: Option> = None; + if let Some(command) = command { + Ok(Self { commands: [command].into(), condition, description, source }) + } else { + Err(format!("no command in {dsl:?}").into()) + } + } +} + +impl_debug!(Condition |self, w| { write!(w, "*") }); + +handle!(TuiIn:|self: App, input|{ + let mut commands = vec![]; + for id in self.mode.keys.iter() { + if let Some(event_map) = self.config.binds.clone().read().unwrap().get(id.as_ref()) { + if let Some(bindings) = event_map.query(input.event()) { + for binding in bindings { + for command in binding.commands.iter() { + if let Some(command) = self.from(command)? as Option { + commands.push(command) + } + } + } + } + } + } + for command in commands.into_iter() { + let result = command.execute(self); + match result { + Ok(undo) => { + self.history.push((command, undo)); + }, + Err(e) => { + self.history.push((command, None)); + return Err(e) + } + } + } + Ok(None) +}); + +#[derive(Debug, Copy, Clone)] +pub enum Axis { X, Y, Z, I } + +impl<'a> DslNs<'a, AppCommand> for App {} +impl<'a> DslNsExprs<'a, AppCommand> for App {} +impl<'a> DslNsWords<'a, AppCommand> for App { + dsl_words!('a |app| -> AppCommand { + "x/inc" => AppCommand::Inc { axis: Axis::X }, + "x/dec" => AppCommand::Dec { axis: Axis::X }, + "y/inc" => AppCommand::Inc { axis: Axis::Y }, + "y/dec" => AppCommand::Dec { axis: Axis::Y }, + "confirm" => AppCommand::Confirm, + "cancel" => AppCommand::Cancel, + }); +} + +impl Default for AppCommand { fn default () -> Self { Self::Nop } } + +def_command!(AppCommand: |app: App| { + Nop => Ok(None), + Confirm => Ok(match &app.dialog { + Dialog::Menu(index, items) => { + let callback = items.0[*index].1.clone(); + callback(app)?; + None + }, + _ => todo!(), + }), + Cancel => todo!(), // TODO delegate: + Inc { axis: Axis } => Ok(match (&app.dialog, axis) { + (Dialog::None, _) => todo!(), + (Dialog::Menu(_, _), Axis::Y) => AppCommand::SetDialog { dialog: app.dialog.menu_next() } + .execute(app)?, + _ => todo!() + }), + Dec { axis: Axis } => Ok(match (&app.dialog, axis) { + (Dialog::None, _) => None, + (Dialog::Menu(_, _), Axis::Y) => AppCommand::SetDialog { dialog: app.dialog.menu_prev() } + .execute(app)?, + _ => todo!() + }), + SetDialog { dialog: Dialog } => { + swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog }) + }, +}); + + //AppCommand => { + //("x/inc" / + //("stop-all") => todo!(),//app.project.stop_all(), + //("enqueue", clip: Option>>) => todo!(), + //("history", delta: isize) => todo!(), + //("zoom", zoom: usize) => todo!(), + //("select", selection: Selection) => todo!(), + //("dialog" / command: DialogCommand) => todo!(), + //("project" / command: ArrangementCommand) => todo!(), + //("clock" / command: ClockCommand) => todo!(), + //("sampler" / command: SamplerCommand) => todo!(), + //("pool" / command: PoolCommand) => todo!(), + //("edit" / editor: MidiEditCommand) => todo!(), + //}; + + //DialogCommand; + + //ArrangementCommand; + + //ClockCommand; + + //SamplerCommand; + + //PoolCommand; + + //MidiEditCommand; + + +//take!(DialogCommand |state: App, iter|Take::take(&state.dialog, iter)); +//#[derive(Clone, Debug)] +//pub enum DialogCommand { + //Open { dialog: Dialog }, + //Close +//} + +//impl Command> for DialogCommand { + //fn execute (self, state: &mut Option) -> Perhaps { + //match self { + //Self::Open { dialog } => { + //*state = Some(dialog); + //}, + //Self::Close => { + //*state = None; + //} + //}; + //Ok(None) + //} +//} + +//dsl!(DialogCommand: |self: Dialog, iter|todo!()); +//Dsl::take(&mut self.dialog, iter)); + +//#[tengri_proc::command(Option)]//Nope. +//impl DialogCommand { + //fn open (dialog: &mut Option, new: Dialog) -> Perhaps { + //*dialog = Some(new); + //Ok(None) + //} + //fn close (dialog: &mut Option) -> Perhaps { + //*dialog = None; + //Ok(None) + //} +//} +// +//dsl_bind!(AppCommand: App { + //enqueue = |app, clip: Option>>| { todo!() }; + //history = |app, delta: isize| { todo!() }; + //zoom = |app, zoom: usize| { todo!() }; + //stop_all = |app| { app.tracks_stop_all(); Ok(None) }; + ////dialog = |app, command: DialogCommand| + ////Ok(command.delegate(&mut app.dialog, |c|Self::Dialog{command: c})?); + //project = |app, command: ArrangementCommand| + //Ok(command.delegate(&mut app.project, |c|Self::Project{command: c})?); + //clock = |app, command: ClockCommand| + //Ok(command.execute(app.clock_mut())?.map(|c|Self::Clock{command: c})); + //sampler = |app, command: SamplerCommand| + //Ok(app.project.sampler_mut().map(|s|command.delegate(s, |command|Self::Sampler{command})) + //.transpose()?.flatten()); + //pool = |app, command: PoolCommand| { + //let undo = command.clone().delegate(&mut app.pool, |command|AppCommand::Pool{command})?; + //// update linked editor after pool action + //match command { + //// autoselect: automatically load selected clip in editor + //PoolCommand::Select { .. } | + //// autocolor: update color in all places simultaneously + //PoolCommand::Clip { command: PoolClipCommand::SetColor { .. } } => { + //let clip = app.pool.clip().clone(); + //app.editor_mut().map(|editor|editor.set_clip(clip.as_ref())) + //}, + //_ => None + //}; + //Ok(undo) + //}; + //select = |app, selection: Selection| { + //*app.project.selection_mut() = selection; + ////todo! + ////if let Some(ref mut editor) = app.editor_mut() { + ////editor.set_clip(match selection { + ////Selection::TrackClip { track, scene } if let Some(Some(Some(clip))) = app + ////.project + ////.scenes.get(scene) + ////.map(|s|s.clips.get(track)) + ////=> + ////Some(clip), + ////_ => + ////None + ////}); + ////} + //Ok(None) + ////("select" [t: usize, s: usize] Some(match (t.expect("no track"), s.expect("no scene")) { + ////(0, 0) => Self::Select(Selection::Mix), + ////(t, 0) => Self::Select(Selection::Track(t)), + ////(0, s) => Self::Select(Selection::Scene(s)), + ////(t, s) => Self::Select(Selection::TrackClip { track: t, scene: s }) }))) + //// autoedit: load focused clip in editor. + //}; + ////fn color (app: &mut App, theme: ItemTheme) -> Perhaps { + ////Ok(app.set_color(Some(theme)).map(|theme|Self::Color{theme})) + ////} + ////fn launch (app: &mut App) -> Perhaps { + ////app.project.launch(); + ////Ok(None) + ////} + //toggle_editor = |app, value: bool|{ app.toggle_editor(Some(value)); Ok(None) }; + //editor = |app, command: MidiEditCommand| Ok(if let Some(editor) = app.editor_mut() { + //let undo = command.clone().delegate(editor, |command|AppCommand::Editor{command})?; + //// update linked sampler after editor action + //app.project.sampler_mut().map(|sampler|match command { + //// autoselect: automatically select sample in sampler + //MidiEditCommand::SetNotePos { pos } => { sampler.set_note_pos(pos); }, + //_ => {} + //}); + //undo + //} else { + //None + //}); +//}); +//take!(ClockCommand |state: App, iter|Take::take(state.clock(), iter)); +//take!(MidiEditCommand |state: App, iter|Ok(state.editor().map(|x|Take::take(x, iter)).transpose()?.flatten())); +//take!(PoolCommand |state: App, iter|Take::take(&state.pool, iter)); +//take!(SamplerCommand |state: App, iter|Ok(state.project.sampler().map(|x|Take::take(x, iter)).transpose()?.flatten())); +//take!(ArrangementCommand |state: App, iter|Take::take(&state.project, iter)); diff --git a/tek/config.rs b/app/tek_cfg.rs similarity index 88% rename from tek/config.rs rename to app/tek_cfg.rs index ac8a2114..6ef37c00 100644 --- a/tek/config.rs +++ b/app/tek_cfg.rs @@ -1,21 +1,6 @@ pub(self) use ::{ - std::{ - sync::{Arc, RwLock}, - collections::BTreeMap, - path::PathBuf - }, - tengri::{ - Usually, impl_debug, - tui::TuiEvent, - dsl::{Dsl, DslWord, DslExpr} - }, - xdg::BaseDirectories, }; -mod bind; pub use self::bind::*; -mod mode; pub use self::mode::*; -mod view; pub use self::view::*; - /// Configuration. /// /// Contains mode, view, and bind definitions. diff --git a/tek/tek_cli.rs b/app/tek_cli.rs similarity index 100% rename from tek/tek_cli.rs rename to app/tek_cli.rs diff --git a/tek/deps.rs b/app/tek_deps.rs similarity index 61% rename from tek/deps.rs rename to app/tek_deps.rs index 12e55caa..0741b40d 100644 --- a/tek/deps.rs +++ b/app/tek_deps.rs @@ -1,9 +1,10 @@ -pub use ::{ - tengri::{ +pub(crate) use ::{ + tek_device::*, + tek_device::tengri::{ Usually, Perhaps, Has, MaybeHas, has, maybe_has, impl_debug, from, dsl::*, input::*, - output::{*, Layout}, + output::*, tui::{ *, ratatui::{ @@ -22,15 +23,14 @@ pub use ::{ error::Error, collections::BTreeMap, fmt::Write, + cmp::Ord, + ffi::OsString, + fmt::{Debug, Formatter}, + fs::File, + ops::{Add, Sub, Mul, Div, Rem}, + thread::JoinHandle }, xdg::BaseDirectories, + atomic_float::*, + tengri::tui::ratatui::{prelude::Rect, widgets::{Widget, canvas::{Canvas, Line}}}, }; - -pub(crate) use atomic_float::*; -pub(crate) use ratatui::{prelude::Rect, widgets::{Widget, canvas::{Canvas, Line}}}; -pub(crate) use std::cmp::Ord; -pub(crate) use std::ffi::OsString; -pub(crate) use std::fmt::{Debug, Formatter}; -pub(crate) use std::fs::File; -pub(crate) use std::ops::{Add, Sub, Mul, Div, Rem}; -pub(crate) use std::thread::JoinHandle; diff --git a/tek/config/mode.rs b/app/tek_mode.rs similarity index 91% rename from tek/config/mode.rs rename to app/tek_mode.rs index 71e435c2..ea8e219f 100644 --- a/tek/config/mode.rs +++ b/app/tek_mode.rs @@ -14,7 +14,14 @@ pub struct Mode { pub modes: Modes, } +impl Draw for Mode { + fn draw (&self, to: &mut TuiOut) { + self.content().draw(to) + } +} + impl Mode> { + pub fn load_into (modes: &Modes, name: &impl AsRef, body: &impl Dsl) -> Usually<()> { let mut mode = Self::default(); println!("Mode::load_into: {}: {body:?}", name.as_ref()); @@ -22,7 +29,8 @@ impl Mode> { modes.write().unwrap().insert(name.as_ref().into(), Arc::new(mode)); Ok(()) } - pub fn load_one (&mut self, dsl: impl Dsl) -> Usually<()> { + + fn load_one (&mut self, dsl: impl Dsl) -> Usually<()> { Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() { println!("Mode::load_one: {head} {:?}", expr.tail()); let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); @@ -46,4 +54,5 @@ impl Mode> { return Err(format!("Mode::load_one: unexpected: {dsl:?}").into()); }) } + } diff --git a/tek/test.rs b/app/tek_test.rs similarity index 100% rename from tek/test.rs rename to app/tek_test.rs diff --git a/app/tek_view.rs b/app/tek_view.rs new file mode 100644 index 00000000..7c1fce25 --- /dev/null +++ b/app/tek_view.rs @@ -0,0 +1,353 @@ +use crate::*; + +pub type Views = Arc, Arc>>>; + +impl Draw for App { + fn draw (&self, to: &mut TuiOut) { + for (index, dsl) in self.mode.view.iter().enumerate() { + if let Err(e) = self.view(to, dsl) { + panic!("render #{index} failed ({e}): {dsl}"); + } + } + } +} + +impl View for App { + fn view_expr <'a> (&'a self, to: &mut TuiOut, expr: &'a impl DslExpr) -> Usually<()> { + if evaluate_output_expression(self, to, expr)? + || evaluate_output_expression_tui(self, to, expr)? { + Ok(()) + } else { + Err(format!("App::view_expr: unexpected: {expr:?}").into()) + } + } + fn view_word <'a> (&'a self, to: &mut TuiOut, dsl: &'a impl DslExpr) -> Usually<()> { + let mut frags = dsl.src()?.unwrap().split("/"); + match frags.next() { + Some(":logo") => to.place(&view_logo()), + Some(":status") => to.place(&Fixed::Y(1, "TODO: Status Bar")), + Some(":meters") => match frags.next() { + Some("input") => to.place(&Tui::bg(Rgb(30, 30, 30), Fill::Y(Align::s("Input Meters")))), + Some("output") => to.place(&Tui::bg(Rgb(30, 30, 30), Fill::Y(Align::s("Output Meters")))), + _ => panic!() + }, + Some(":tracks") => match frags.next() { + None => to.place(&"TODO tracks"), + Some("names") => to.place(&self.project.view_track_names(self.color.clone())),//Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Names")))), + Some("inputs") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Inputs")))), + Some("devices") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Devices")))), + Some("outputs") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Outputs")))), + _ => panic!() + }, + Some(":scenes") => match frags.next() { + None => to.place(&"TODO scenes"), + Some(":scenes/names") => to.place(&"TODO Scene Names"), + _ => panic!() + }, + Some(":editor") => to.place(&"TODO Editor"), + Some(":dialog") => match frags.next() { + Some("menu") => to.place(&if let Dialog::Menu(selected, items) = &self.dialog { + let items = items.clone(); + let selected = selected; + Some(Fill::XY(Thunk::new(move|to: &mut TuiOut|{ + for (index, MenuItem(item, _)) in items.0.iter().enumerate() { + to.place(&Push::y((2 * index) as u16, + Tui::fg_bg( + if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) }, + if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) }, + Fixed::Y(2, Align::n(Fill::X(item))) + ))); + } + }))) + } else { + None + }), + _ => unimplemented!("App::view_word: {dsl:?} ({frags:?})"), + }, + Some(":templates") => to.place(&{ + let modes = self.config.modes.clone(); + let height = (modes.read().unwrap().len() * 2) as u16; + Fixed::Y(height, Min::x(30, Thunk::new(move |to: &mut TuiOut|{ + for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() { + let bg = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) }; + let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or(""); + let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or(""); + let fg1 = Rgb(224, 192, 128); + let fg2 = Rgb(224, 128, 32); + let field_name = Fill::X(Align::w(Tui::fg(fg1, name))); + let field_id = Fill::X(Align::e(Tui::fg(fg2, id))); + let field_info = Fill::X(Align::w(info)); + to.place(&Push::y((2 * index) as u16, + Fixed::Y(2, Fill::X(Tui::bg(bg, Bsp::s( + Bsp::a(field_name, field_id), field_info)))))); + } + }))) + }), + Some(":sessions") => to.place(&Fixed::Y(6, Min::x(30, Thunk::new(|to: &mut TuiOut|{ + let fg = Rgb(224, 192, 128); + for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { + let bg = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; + to.place(&Push::y((2 * index) as u16, + &Fixed::Y(2, Fill::X(Tui::bg(bg, Align::w(Tui::fg(fg, name))))))); + } + })))), + Some(":browse/title") => to.place(&Fill::X(Align::w(FieldV(Default::default(), + match self.dialog.browser_target().unwrap() { + BrowseTarget::SaveProject => "Save project:", + BrowseTarget::LoadProject => "Load project:", + BrowseTarget::ImportSample(_) => "Import sample:", + BrowseTarget::ExportSample(_) => "Export sample:", + BrowseTarget::ImportClip(_) => "Import clip:", + BrowseTarget::ExportClip(_) => "Export clip:", + }, Shrink::x(3, Fixed::Y(1, Tui::fg(Tui::g(96), RepeatH("๐Ÿญป")))))))), + Some(":device") => { + let selected = self.dialog.device_kind().unwrap(); + to.place(&Bsp::s(Tui::bold(true, "Add device"), Map::south(1, + move||device_kinds().iter(), + move|_label: &&'static str, i|{ + let bg = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }; + let lb = if i == selected { "[ " } else { " " }; + let rb = if i == selected { " ]" } else { " " }; + Fill::X(Tui::bg(bg, Bsp::e(lb, Bsp::w(rb, "FIXME device name")))) }))) + }, + Some(":debug") => to.place(&Fixed::Y(1, format!("[{:?}]", to.area()))), + Some(_) => { + let views = self.config.views.read().unwrap(); + if let Some(dsl) = views.get(dsl.src()?.unwrap()) { + let dsl = dsl.clone(); + std::mem::drop(views); + self.view(to, &dsl)? + } else { + unimplemented!("{dsl:?}"); + } + }, + _ => unreachable!() + } + Ok(()) + } +} + +fn view_logo () -> impl Content { + Fixed::XY(32, 7, Tui::bold(true, Tui::fg(Rgb(240,200,180), col!{ + Fixed::Y(1, ""), + Fixed::Y(1, ""), + Fixed::Y(1, "~~ โ•“โ”€โ•ฅโ”€โ•– โ•“โ”€โ”€โ•– โ•ฅ โ•– ~~~~~~~~~~~~"), + Fixed::Y(1, Bsp::e("~~~~ โ•‘ ~ โ•Ÿโ”€โ•Œ ~โ•Ÿโ”€< ~~ ", Bsp::e(Tui::fg(Rgb(230,100,40), "v0.3.0"), " ~~"))), + Fixed::Y(1, "~~~~ โ•จ ~ โ•™โ”€โ”€โ•œ โ•จ โ•œ ~~~~~~~~~~~~"), + }))) +} + +//pub fn view_nil (_: &App) -> TuiCb { + //|to|to.place(&Fill::XY("ยท")) +//} + + //Bsp::s("", + //Map::south(1, + //move||app.config.binds.layers.iter() + //.filter_map(|a|(a.0)(app).then_some(a.1)) + //.flat_map(|a|a) + //.filter_map(|x|if let Value::Exp(_, iter)=x.value{ Some(iter) } else { None }) + //.skip(offset) + //.take(20), + //|mut b,i|Fixed::x(60, Align::w(Bsp::e("(", Bsp::e( + //b.next().map(|t|Fixed::x(16, Align::w(Tui::fg(Rgb(64,224,0), format!("{}", t.value))))), + //Bsp::e(" ", Align::w(format!("{}", b.0.0.trim()))))))))))), + + //Dialog::Browse(BrowseTarget::Load, browser) => { + //"bobcat".boxed() + ////Bsp::s( + ////Fill::X(Align::w(Margin::XY(1, 1, Bsp::e( + ////Tui::bold(true, " Load project: "), + ////Shrink::x(3, Fixed::Y(1, RepeatH("๐Ÿญป"))))))), + ////Outer(true, Style::default().fg(Tui::g(96))) + ////.enclose(Fill::XY(browser))) + //}, + //Dialog::Browse(BrowseTarget::Export, browser) => { + //"bobcat".boxed() + ////Bsp::s( + ////Fill::X(Align::w(Margin::XY(1, 1, Bsp::e( + ////Tui::bold(true, " Export: "), + ////Shrink::x(3, Fixed::Y(1, RepeatH("๐Ÿญป"))))))), + ////Outer(true, Style::default().fg(Tui::g(96))) + ////.enclose(Fill::XY(browser))) + //}, + //Dialog::Browse(BrowseTarget::Import, browser) => { + //"bobcat".boxed() + ////Bsp::s( + ////Fill::X(Align::w(Margin::XY(1, 1, Bsp::e( + ////Tui::bold(true, " Import: "), + ////Shrink::x(3, Fixed::Y(1, RepeatH("๐Ÿญป"))))))), + ////Outer(true, Style::default().fg(Tui::g(96))) + ////.enclose(Fill::XY(browser))) + //}, +// + //pub fn view_history (&self) -> impl Content { + //Fixed::Y(1, Fill::X(Align::w(FieldH(self.color, + //format!("History ({})", self.history.len()), + //self.history.last().map(|last|Fill::X(Align::w(format!("{:?}", last.0)))))))) + //} + //pub fn view_status_h2 (&self) -> impl Content { + //self.update_clock(); + //let theme = self.color; + //let clock = self.clock(); + //let playing = clock.is_rolling(); + //let cache = clock.view_cache.clone(); + ////let selection = self.selection().describe(self.tracks(), self.scenes()); + //let hist_len = self.history.len(); + //let hist_last = self.history.last(); + //Fixed::Y(2, Stack::east(move|add: &mut dyn FnMut(&dyn Draw)|{ + //add(&Fixed::x(5, Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, + //Either::new(false, // TODO + //Thunk::new(move||Fixed::x(9, Either::new(playing, + //Tui::fg(Rgb(0, 255, 0), " PLAYING "), + //Tui::fg(Rgb(255, 128, 0), " STOPPED "))) + //), + //Thunk::new(move||Fixed::x(5, Either::new(playing, + //Tui::fg(Rgb(0, 255, 0), Bsp::s(" ๐Ÿญ๐Ÿญ‘๐Ÿฌฝ ", " ๐Ÿญž๐Ÿญœ๐Ÿญ˜ ",)), + //Tui::fg(Rgb(255, 128, 0), Bsp::s(" โ–—โ–„โ–– ", " โ–โ–€โ–˜ ",)))) + //) + //) + //))); + //add(&" "); + //{ + //let cache = cache.read().unwrap(); + //add(&Fixed::x(15, Align::w(Bsp::s( + //FieldH(theme, "Beat", cache.beat.view.clone()), + //FieldH(theme, "Time", cache.time.view.clone()), + //)))); + //add(&Fixed::x(13, Align::w(Bsp::s( + //Fill::X(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))), + //Fill::X(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))), + //)))); + //add(&Fixed::x(12, Align::w(Bsp::s( + //Fill::X(Align::w(FieldH(theme, "Buf", cache.buf.view.clone()))), + //Fill::X(Align::w(FieldH(theme, "Lat", cache.lat.view.clone()))), + //)))); + ////add(&Bsp::s( + //////Fill::X(Align::w(FieldH(theme, "Selected", Align::w(selection)))), + ////Fill::X(Align::w(FieldH(theme, format!("History ({})", hist_len), + ////hist_last.map(|last|Fill::X(Align::w(format!("{:?}", last.0))))))), + ////"" + ////)); + //////if let Some(last) = self.history.last() { + //////add(&FieldV(theme, format!("History ({})", self.history.len()), + //////Fill::X(Align::w(format!("{:?}", last.0))))); + //////} + //} + //})) + //} + //pub fn view_status_v (&self) -> impl Content + use<'_> { + //self.update_clock(); + //let cache = self.project.clock.view_cache.read().unwrap(); + //let theme = self.color; + //let playing = self.clock().is_rolling(); + //Tui::bg(theme.darker.rgb, Fixed::XY(20, 5, Outer(true, Style::default().fg(Tui::g(96))).enclose( + //col!( + //Fill::X(Align::w(Bsp::e( + //Align::w(Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, + //Either::new(false, // TODO + //Thunk::new(move||Fixed::x(9, Either::new(playing, + //Tui::fg(Rgb(0, 255, 0), " PLAYING "), + //Tui::fg(Rgb(255, 128, 0), " STOPPED "))) + //), + //Thunk::new(move||Fixed::x(5, Either::new(playing, + //Tui::fg(Rgb(0, 255, 0), Bsp::s(" ๐Ÿญ๐Ÿญ‘๐Ÿฌฝ ", " ๐Ÿญž๐Ÿญœ๐Ÿญ˜ ",)), + //Tui::fg(Rgb(255, 128, 0), Bsp::s(" โ–—โ–„โ–– ", " โ–โ–€โ–˜ ",)))) + //) + //) + //)), + //Bsp::s( + //FieldH(theme, "Beat", cache.beat.view.clone()), + //FieldH(theme, "Time", cache.time.view.clone()), + //), + //))), + //Fill::X(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))), + //Fill::X(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))), + //Fill::X(Align::w(FieldH(theme, "Buf", Bsp::e(cache.buf.view.clone(), Bsp::e(" = ", cache.lat.view.clone()))))), + //)))) + //} + //pub fn view_status (&self) -> impl Content + use<'_> { + //self.update_clock(); + //let cache = self.project.clock.view_cache.read().unwrap(); + //view_status(Some(self.project.selection.describe(self.tracks(), self.scenes())), + //cache.sr.view.clone(), cache.buf.view.clone(), cache.lat.view.clone()) + //} + //pub fn view_transport (&self) -> impl Content + use<'_> { + //self.update_clock(); + //let cache = self.project.clock.view_cache.read().unwrap(); + //view_transport(self.project.clock.is_rolling(), + //cache.bpm.view.clone(), cache.beat.view.clone(), cache.time.view.clone()) + //} + //pub fn view_editor (&self) -> impl Content + use<'_> { + //let bg = self.editor() + //.and_then(|editor|editor.clip().clone()) + //.map(|clip|clip.read().unwrap().color.darker) + //.unwrap_or(self.color.darker); + //Fill::XY(Tui::bg(bg.rgb, self.editor())) + //} + //pub fn view_editor_status (&self) -> impl Content + use<'_> { + //self.editor().map(|e|Fixed::x(20, Outer(true, Style::default().fg(Tui::g(96))).enclose( + //Fill::Y(Align::n(Bsp::s(e.clip_status(), e.edit_status())))))) + //} + //pub fn view_midi_ins_status (&self) -> impl Content + use<'_> { + //self.project.view_midi_ins_status(self.color) + //} + //pub fn view_midi_outs_status (&self) -> impl Content + use<'_> { + //self.project.view_midi_outs_status(self.color) + //} + //pub fn view_audio_ins_status (&self) -> impl Content + use<'_> { + //self.project.view_audio_ins_status(self.color) + //} + //pub fn view_audio_outs_status (&self) -> impl Content + use<'_> { + //self.project.view_audio_outs_status(self.color) + //} + //pub fn view_scenes (&self) -> impl Content + use<'_> { + //Bsp::e( + //Fixed::x(20, Align::nw(self.project.view_scenes_names())), + //self.project.view_scenes_clips(), + //) + //} + //pub fn view_scenes_names (&self) -> impl Content + use<'_> { + //self.project.view_scenes_names() + //} + //pub fn view_scenes_clips (&self) -> impl Content + use<'_> { + //self.project.view_scenes_clips() + //} + //pub fn view_tracks_inputs <'a> (&'a self) -> impl Content + use<'a> { + //Fixed::Y(1 + self.project.midi_ins.len() as u16, + //self.project.view_inputs(self.color)) + //} + //pub fn view_tracks_outputs <'a> (&'a self) -> impl Content + use<'a> { + //self.project.view_outputs(self.color) + //} + //pub fn view_tracks_devices <'a> (&'a self) -> impl Content + use<'a> { + //Fixed::Y(4, self.project.view_track_devices(self.color)) + //} + //pub fn view_tracks_names <'a> (&'a self) -> impl Content + use<'a> { + //Fixed::Y(2, self.project.view_track_names(self.color)) + //} + //pub fn view_pool (&self) -> impl Content + use<'_> { + //Fixed::x(20, Bsp::s( + //Fill::X(Align::w(FieldH(self.color, "Clip pool:", ""))), + //Fill::Y(Align::n(Tui::bg(Rgb(0, 0, 0), Outer(true, Style::default().fg(Tui::g(96))) + //.enclose(PoolView(&self.pool))))))) + //} + //pub fn view_samples_keys (&self) -> impl Content + use<'_> { + //self.project.sampler().map(|s|s.view_list(true, self.editor().unwrap())) + //} + //pub fn view_samples_grid (&self) -> impl Content + use<'_> { + //self.project.sampler().map(|s|s.view_grid()) + //} + //pub fn view_sample_viewer (&self) -> impl Content + use<'_> { + //self.project.sampler().map(|s|s.view_sample(self.editor().unwrap().get_note_pos())) + //} + //pub fn view_sample_info (&self) -> impl Content + use<'_> { + //self.project.sampler().map(|s|s.view_sample_info(self.editor().unwrap().get_note_pos())) + //} + //pub fn view_sample_status (&self) -> impl Content + use<'_> { + //self.project.sampler().map(|s|Outer(true, Style::default().fg(Tui::g(96))).enclose( + //Fill::Y(Align::n(s.view_sample_status(self.editor().unwrap().get_note_pos()))))) + //} + ////let options = ||["Projects", "Settings", "Help", "Quit"].iter(); + ////let option = |a,i|Tui::fg(Rgb(255,255,255), format!("{}", a)); + ////Bsp::s(Tui::bold(true, "tek!"), Bsp::s("", Map::south(1, options, option))) diff --git a/bacon.toml b/bacon.toml index 5a611b08..4af91cd0 100644 --- a/bacon.toml +++ b/bacon.toml @@ -10,24 +10,25 @@ l = "job:clippy" [jobs.check] command = ["cargo", "check"] need_stdout = false -watch = ["tek", "deps"] +watch = ["deps", "engine", "device", "app"] [jobs.check-all] command = ["cargo", "check", "--all-targets"] need_stdout = false -watch = ["tek", "deps"] +watch = ["deps", "engine", "device", "app"] [jobs.clippy] command = ["cargo", "clippy"] need_stdout = false -watch = ["tek", "deps"] +watch = ["deps", "engine", "device", "app"] [jobs.clippy-all] command = ["cargo", "clippy", "--all-targets"] need_stdout = false -watch = ["tek", "deps"] +watch = ["deps", "engine", "device", "app"] [jobs.test] command = ["cargo", "test", "--workspace", "--exclude", "jack"] need_stdout = true -watch = ["tek", "deps"] +watch = ["deps", "engine", "device", "app"] [jobs.nextest] +watch = ["deps", "engine", "device", "app"] command = [ "cargo", "nextest", "run", "--hide-progress-bar", "--failure-output", "final" @@ -50,12 +51,14 @@ need_stdout = true allow_warnings = true background = true [jobs.run-long] +watch = ["deps", "engine", "device", "app"] command = [ "cargo", "run", ] need_stdout = true allow_warnings = true background = false on_change_strategy = "kill_then_restart" [jobs.ex] +watch = ["deps", "engine", "device", "app"] command = ["cargo", "run", "--example"] need_stdout = true allow_warnings = true diff --git a/deps/tengri b/deps/tengri index ca862b98..41fa55fa 160000 --- a/deps/tengri +++ b/deps/tengri @@ -1 +1 @@ -Subproject commit ca862b9802524ee1713c0088c7a525ad07f370a0 +Subproject commit 41fa55fa6ca15abd0a80fd50a30326b1fea6b5f1 diff --git a/device/Cargo.toml b/device/Cargo.toml new file mode 100644 index 00000000..345640b0 --- /dev/null +++ b/device/Cargo.toml @@ -0,0 +1,59 @@ +[package] +name = "tek_device" +edition = { workspace = true } +version = { workspace = true } + +[lib] +path = "device.rs" + +[target.'cfg(target_os = "linux")'] +rustflags = ["-C", "link-arg=-fuse-ld=mold"] + +[dependencies] +tek_engine = { path = "../engine" } + +atomic_float = { workspace = true } +backtrace = { workspace = true } +clap = { workspace = true, optional = true } +jack = { workspace = true } +konst = { workspace = true } +livi = { workspace = true, optional = true } +midly = { workspace = true } +palette = { workspace = true } +rand = { workspace = true } +symphonia = { workspace = true, optional = true } +tengri = { workspace = true } +toml = { workspace = true } +uuid = { workspace = true, optional = true } +wavers = { workspace = true, optional = true } +winit = { workspace = true, optional = true } +xdg = { workspace = true } + +[dev-dependencies] +proptest = { workspace = true } +proptest-derive = { workspace = true } + +[features] +arranger = ["port", "editor", "sequencer", "editor", "track", "scene", "clip", "select"] +browse = [] +clap = [] +cli = ["dep:clap"] +clip = [] +clock = [] +default = ["cli", "arranger", "sampler", "track", "lv2"] +editor = [] +host = ["lv2"] +lv2 = ["port", "livi"] +lv2_gui = ["lv2", "winit"] +meter = [] +mixer = [] +pool = [] +port = [] +sampler = ["port", "meter", "mixer", "browse", "symphonia", "wavers"] +select = [] +scene = [] +sequencer = ["port", "clock", "uuid", "pool"] +sf2 = [] +track = [] +vst2 = [] +vst3 = [] diff --git a/device/arranger.rs b/device/arranger.rs new file mode 100644 index 00000000..10a624b1 --- /dev/null +++ b/device/arranger.rs @@ -0,0 +1,626 @@ +use crate::*; + +#[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: Measure, + /// Display size of clips area + pub size_inner: Measure, + /// 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 + } +} +has!(Jack<'static>: |self: Arrangement|self.jack); +has!(Measure: |self: Arrangement|self.size); +#[cfg(feature = "editor")] has!(Option: |self: Arrangement|self.editor); +#[cfg(feature = "port")] has!(Vec: |self: Arrangement|self.midi_ins); +#[cfg(feature = "port")] has!(Vec: |self: Arrangement|self.midi_outs); +#[cfg(feature = "clock")] has!(Clock: |self: Arrangement|self.clock); +#[cfg(feature = "select")] has!(Selection: |self: Arrangement|self.selection); +#[cfg(all(feature = "select", feature = "track"))] has!(Vec: |self: Arrangement|self.tracks); +#[cfg(all(feature = "select", feature = "track"))] maybe_has!(Track: |self: Arrangement| + { Has::::get(self).track().map(|index|Has::>::get(self).get(index)).flatten() }; + { Has::::get(self).track().map(|index|Has::>::get_mut(self).get_mut(index)).flatten() }); +#[cfg(all(feature = "select", feature = "scene"))] has!(Vec: |self: Arrangement|self.scenes); +#[cfg(all(feature = "select", feature = "scene"))] maybe_has!(Scene: |self: Arrangement| + { Has::::get(self).track().map(|index|Has::>::get(self).get(index)).flatten() }; + { Has::::get(self).track().map(|index|Has::>::get_mut(self).get_mut(index)).flatten() }); + +#[cfg(feature = "select")] +impl Arrangement { + #[cfg(feature = "clip")] fn selected_clip (&self) -> Option { todo!() } + #[cfg(feature = "scene")] fn selected_scene (&self) -> Option { todo!() } + #[cfg(feature = "track")] fn selected_track (&self) -> Option { todo!() } + #[cfg(feature = "port")] fn selected_midi_in (&self) -> Option { todo!() } + #[cfg(feature = "port")] fn selected_midi_out (&self) -> Option { todo!() } + fn selected_device (&self) -> Option { todo!() } + fn unselect (&self) -> Selection { + Selection::Nothing + } +} + +impl Arrangement { + /// Width of display + pub fn w (&self) -> u16 { + self.size.w() as u16 + } + /// Width allocated for sidebar. + pub fn w_sidebar (&self, is_editing: bool) -> u16 { + self.w() / if is_editing { 16 } else { 8 } as u16 + } + /// Width available to display tracks. + pub fn w_tracks_area (&self, is_editing: bool) -> u16 { + self.w().saturating_sub(self.w_sidebar(is_editing)) + } + /// Height of display + pub fn h (&self) -> u16 { + self.size.h() as u16 + } + /// Height taken by visible device slots. + pub fn h_devices (&self) -> u16 { + 2 + //1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) + } +} + +#[cfg(feature = "track")] +impl TracksView for Arrangement {} + +#[cfg(feature = "track")] +impl Arrangement { + /// Get the active track + pub fn get_track (&self) -> Option<&Track> { + let index = self.selection().track()?; + Has::>::get(self).get(index) + } + /// Get a mutable reference to the active track + pub fn get_track_mut (&mut self) -> Option<&mut Track> { + let index = self.selection().track()?; + Has::>::get_mut(self).get_mut(index) + } + /// Add multiple tracks + pub fn tracks_add ( + &mut self, + count: usize, width: Option, + mins: &[Connect], mouts: &[Connect], + ) -> Usually<()> { + let track_color_1 = ItemColor::random(); + let track_color_2 = ItemColor::random(); + for i in 0..count { + let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into(); + let track = self.track_add(None, Some(color), mins, mouts)?.1; + if let Some(width) = width { + track.width = width; + } + } + Ok(()) + } + /// Add a track + pub fn track_add ( + &mut self, + name: Option<&str>, color: Option, + mins: &[Connect], mouts: &[Connect], + ) -> Usually<(usize, &mut Track)> { + let name: Arc = name.map_or_else( + ||format!("trk{:02}", self.track_last).into(), + |x|x.to_string().into() + ); + self.track_last += 1; + let track = Track { + width: (name.len() + 2).max(12), + color: color.unwrap_or_else(ItemTheme::random), + sequencer: Sequencer::new( + &format!("{name}"), + self.jack(), + Some(self.clock()), + None, + mins, + mouts + )?, + name, + ..Default::default() + }; + self.tracks_mut().push(track); + let len = self.tracks().len(); + let index = len - 1; + for scene in self.scenes_mut().iter_mut() { + while scene.clips.len() < len { + scene.clips.push(None); + } + } + Ok((index, &mut self.tracks_mut()[index])) + } + + pub fn view_inputs (&self, _theme: ItemTheme) -> impl Content + '_ { + Bsp::s( + Fixed::Y(1, self.view_inputs_header()), + Thunk::new(|to: &mut TuiOut|{ + for (index, port) in self.midi_ins().iter().enumerate() { + to.place(&Push::X(index as u16 * 10, Fixed::Y(1, self.view_inputs_row(port)))) + } + }) + ) + } + + fn view_inputs_header (&self) -> impl Content + '_ { + Bsp::e(Fixed::X(20, Align::w(button_3("i", "nput ", format!("{}", self.midi_ins.len()), false))), + Bsp::w(Fixed::X(4, button_2("I", "+", false)), Thunk::new(move|to: &mut TuiOut|for (_index, track, x1, _x2) in self.tracks_with_sizes() { + #[cfg(feature = "track")] + to.place(&Push::X(x1 as u16, Tui::bg(track.color.dark.rgb, Align::w(Fixed::X(track.width as u16, row!( + Either::new(track.sequencer.monitoring, Tui::fg(Green, "mon "), "mon "), + Either::new(track.sequencer.recording, Tui::fg(Red, "rec "), "rec "), + Either::new(track.sequencer.overdub, Tui::fg(Yellow, "dub "), "dub "), + )))))) + }))) + } + + fn view_inputs_row (&self, port: &MidiInput) -> impl Content { + Bsp::e(Fixed::X(20, Align::w(Bsp::e(" โ— ", Tui::bold(true, Tui::fg(Rgb(255,255,255), port.port_name()))))), + Bsp::w(Fixed::X(4, ()), Thunk::new(move|to: &mut TuiOut|for (_index, track, _x1, _x2) in self.tracks_with_sizes() { + #[cfg(feature = "track")] + to.place(&Tui::bg(track.color.darker.rgb, Align::w(Fixed::X(track.width as u16, row!( + Either::new(track.sequencer.monitoring, Tui::fg(Green, " โ— "), " ยท "), + Either::new(track.sequencer.recording, Tui::fg(Red, " โ— "), " ยท "), + Either::new(track.sequencer.overdub, Tui::fg(Yellow, " โ— "), " ยท "), + ))))) + }))) + } + + pub fn view_outputs (&self, theme: ItemTheme) -> impl Content { + let mut h = 1; + for output in self.midi_outs().iter() { + h += 1 + output.connections.len(); + } + let h = h as u16; + let list = Bsp::s( + Fixed::Y(1, Fill::X(Align::w(button_3("o", "utput", format!("{}", self.midi_outs.len()), false)))), + Fixed::Y(h - 1, Fill::XY(Align::nw(Thunk::new(|to: &mut TuiOut|{ + for (_index, port) in self.midi_outs().iter().enumerate() { + to.place(&Fixed::Y(1,Fill::X(Bsp::e( + Align::w(Bsp::e(" โ— ", Tui::fg(Rgb(255,255,255),Tui::bold(true, port.port_name())))), + Fill::X(Align::e(format!("{}/{} ", + port.port().get_connections().len(), + port.connections.len()))))))); + for (index, conn) in port.connections.iter().enumerate() { + to.place(&Fixed::Y(1, Fill::X(Align::w(format!(" c{index:02}{}", conn.info()))))); + } + } + }))))); + Fixed::Y(h, view_track_row_section(theme, list, button_2("O", "+", false), + Tui::bg(theme.darker.rgb, Align::w(Fill::X( + Thunk::new(|to: &mut TuiOut|{ + for (index, track, _x1, _x2) in self.tracks_with_sizes() { + to.place(&Fixed::X(track_width(index, track), + Thunk::new(|to: &mut TuiOut|{ + to.place(&Fixed::Y(1, Align::w(Bsp::e( + Either::new(true, Tui::fg(Green, "play "), "play "), + Either::new(false, Tui::fg(Yellow, "solo "), "solo "), + )))); + for (_index, port) in self.midi_outs().iter().enumerate() { + to.place(&Fixed::Y(1, Align::w(Bsp::e( + Either::new(true, Tui::fg(Green, " โ— "), " ยท "), + Either::new(false, Tui::fg(Yellow, " โ— "), " ยท "), + )))); + for (_index, _conn) in port.connections.iter().enumerate() { + to.place(&Fixed::Y(1, Fill::X(""))); + } + }})))}})))))) + } + + pub fn view_track_devices (&self, theme: ItemTheme) -> impl Content { + let mut h = 2u16; + for track in self.tracks().iter() { + h = h.max(track.devices.len() as u16 * 2); + } + view_track_row_section(theme, + button_3("d", "evice", format!("{}", self.track().map(|t|t.devices.len()).unwrap_or(0)), false), + button_2("D", "+", false), + Thunk::new(move|to: &mut TuiOut|for (index, track, _x1, _x2) in self.tracks_with_sizes() { + to.place(&Fixed::XY(track_width(index, track), h + 1, + Tui::bg(track.color.dark.rgb, Align::nw(Map::south(2, move||0..h, + |_, _index|Fixed::XY(track.width as u16, 2, + Tui::fg_bg( + ItemTheme::G[32].lightest.rgb, + ItemTheme::G[32].dark.rgb, + Align::nw(format!(" ยท {}", "--"))))))))); + })) + } + +} + +#[cfg(feature = "track")] +pub fn view_track_row_section ( + _theme: ItemTheme, + button: impl Content, + button_add: impl Content, + content: impl Content, +) -> impl Content { + Bsp::w(Fill::Y(Fixed::X(4, Align::nw(button_add))), + Bsp::e(Fixed::X(20, Fill::Y(Align::nw(button))), Fill::XY(Align::c(content)))) +} + +#[cfg(feature = "scene")] +impl Arrangement { + /// Get the active scene + pub fn get_scene (&self) -> Option<&Scene> { + let index = self.selection().scene()?; + Has::>::get(self).get(index) + } + /// Get a mutable reference to the active scene + pub fn get_scene_mut (&mut self) -> Option<&mut Scene> { + let index = self.selection().scene()?; + Has::>::get_mut(self).get_mut(index) + } +} + +#[cfg(feature = "scene")] +impl ScenesView for Arrangement { + fn h_scenes (&self) -> u16 { + (self.height() as u16).saturating_sub(20) + } + fn w_side (&self) -> u16 { + (self.width() as u16 * 2 / 10).max(20) + } + fn w_mid (&self) -> u16 { + (self.width() as u16).saturating_sub(2 * self.w_side()).max(40) + } +} + +#[cfg(feature = "clip")] +impl Arrangement { + /// Get the active clip + pub fn get_clip (&self) -> Option>> { + self.get_scene()?.clips.get(self.selection().track()?)?.clone() + } + /// Put a clip in a slot + pub fn clip_put ( + &mut self, track: usize, scene: usize, clip: Option>> + ) -> Option>> { + let old = self.scenes[scene].clips[track].clone(); + self.scenes[scene].clips[track] = clip; + old + } + /// Change the color of a clip, returning the previous one + pub fn clip_set_color (&self, track: usize, scene: usize, color: ItemTheme) + -> Option + { + self.scenes[scene].clips[track].as_ref().map(|clip|{ + let mut clip = clip.write().unwrap(); + let old = clip.color.clone(); + clip.color = color.clone(); + panic!("{color:?} {old:?}"); + old + }) + } + /// Toggle looping for the active clip + pub fn toggle_loop (&mut self) { + if let Some(clip) = self.get_clip() { + clip.write().unwrap().toggle_loop() + } + } +} + +#[cfg(feature = "sampler")] +impl Arrangement { + /// Get the first sampler of the active track + pub fn sampler (&self) -> Option<&Sampler> { + self.get_track()?.sampler(0) + } + /// Get the first sampler of the active track + pub fn sampler_mut (&mut self) -> Option<&mut Sampler> { + self.get_track_mut()?.sampler_mut(0) + } +} + +pub(crate) fn wrap (bg: Color, fg: Color, content: impl Content) -> impl Content { + let left = Tui::fg_bg(bg, Reset, Fixed::X(1, RepeatV("โ–"))); + let right = Tui::fg_bg(bg, Reset, Fixed::X(1, RepeatV("โ–Œ"))); + Bsp::e(left, Bsp::w(right, Tui::fg_bg(fg, bg, content))) +} + +pub trait HasClipsSize { + fn clips_size (&self) -> &Measure; +} + +impl HasClipsSize for Arrangement { + fn clips_size (&self) -> &Measure { &self.size_inner } +} + +pub trait HasWidth { + const MIN_WIDTH: usize; + /// Increment track width. + fn width_inc (&mut self); + /// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH]. + fn width_dec (&mut self); +} + +//def_command!(ArrangementCommand: |arranger: Arrangement| { + + //Home => { arranger.editor = None; Ok(None) }, + + //Edit => { + //let selection = arranger.selection().clone(); + //arranger.editor = if arranger.editor.is_some() { + //None + //} else { + //match selection { + //Selection::TrackClip { track, scene } => { + //let clip = &mut arranger.scenes_mut()[scene].clips[track]; + //if clip.is_none() { + ////app.clip_auto_create(); + //*clip = Some(Arc::new(RwLock::new(MidiClip::new( + //&format!("t{track:02}s{scene:02}"), + //false, 384, None, Some(ItemTheme::random()) + //)))); + //} + //clip.as_ref().map(|c|c.into()) + //} + //_ => { + //None + //} + //} + //}; + //if let Some(editor) = arranger.editor.as_mut() { + //if let Some(clip) = editor.clip() { + //let length = clip.read().unwrap().length.max(1); + //let width = arranger.size_inner.w().saturating_sub(20).max(1); + //editor.set_time_zoom(length / width); + //editor.redraw(); + //} + //} + //Ok(None) + //}, + + ////// Set the selection + //Select { selection: Selection } => { *arranger.selection_mut() = *selection; Ok(None) }, + + ////// Launch the selected clip or scene + //Launch => { + //match *arranger.selection() { + //Selection::Track(t) => { + //arranger.tracks[t].sequencer.enqueue_next(None) + //}, + //Selection::TrackClip { track, scene } => { + //arranger.tracks[track].sequencer.enqueue_next(arranger.scenes[scene].clips[track].as_ref()) + //}, + //Selection::Scene(s) => { + //for t in 0..arranger.tracks.len() { + //arranger.tracks[t].sequencer.enqueue_next(arranger.scenes[s].clips[t].as_ref()) + //} + //}, + //_ => {} + //}; + //Ok(None) + //}, + + ////// Set the color of the selected entity + //SetColor { palette: Option } => { + //let mut palette = palette.unwrap_or_else(||ItemTheme::random()); + //let selection = *arranger.selection(); + //Ok(Some(Self::SetColor { palette: Some(match selection { + //Selection::Mix => { + //std::mem::swap(&mut palette, &mut arranger.color); + //palette + //}, + //Selection::Scene(s) => { + //std::mem::swap(&mut palette, &mut arranger.scenes[s].color); + //palette + //} + //Selection::Track(t) => { + //std::mem::swap(&mut palette, &mut arranger.tracks[t].color); + //palette + //} + //Selection::TrackClip { track, scene } => { + //if let Some(ref clip) = arranger.scenes[scene].clips[track] { + //let mut clip = clip.write().unwrap(); + //std::mem::swap(&mut palette, &mut clip.color); + //palette + //} else { + //return Ok(None) + //} + //}, + //_ => todo!() + //}) })) + //}, + + //Track { track: TrackCommand } => { todo!("delegate") }, + + //TrackAdd => { + //let index = arranger.track_add(None, None, &[], &[])?.0; + //*arranger.selection_mut() = match arranger.selection() { + //Selection::Track(_) => Selection::Track(index), + //Selection::TrackClip { track: _, scene } => Selection::TrackClip { + //track: index, scene: *scene + //}, + //_ => *arranger.selection() + //}; + //Ok(Some(Self::TrackDelete { index })) + //}, + + //TrackSwap { index: usize, other: usize } => { + //let index = *index; + //let other = *other; + //Ok(Some(Self::TrackSwap { index, other })) + //}, + + //TrackDelete { index: usize } => { + //let index = *index; + //let exists = arranger.tracks().get(index).is_some(); + //if exists { + //let track = arranger.tracks_mut().remove(index); + //let Track { sequencer: Sequencer { midi_ins, midi_outs, .. }, .. } = track; + //for port in midi_ins.into_iter() { + //port.close()?; + //} + //for port in midi_outs.into_iter() { + //port.close()?; + //} + //for scene in arranger.scenes_mut().iter_mut() { + //scene.clips.remove(index); + //} + //} + //Ok(None) + ////TODO:Ok(Some(Self::TrackAdd ( index, track: Some(deleted_track) }) + //}, + + //MidiIn { input: MidiInputCommand } => { + //todo!("delegate"); Ok(None) + //}, + + //MidiInAdd => { + //arranger.midi_in_add()?; + //Ok(None) + //}, + + //MidiOut { output: MidiOutputCommand } => { + //todo!("delegate"); + //Ok(None) + //}, + + //MidiOutAdd => { + //arranger.midi_out_add()?; + //Ok(None) + //}, + + //Device { command: DeviceCommand } => { + //todo!("delegate"); + //Ok(None) + //}, + + //DeviceAdd { index: usize } => { + //todo!("delegate"); + //Ok(None) + //}, + + //Scene { scene: SceneCommand } => { + //todo!("delegate"); + //Ok(None) + //}, + + //OutputAdd => { + //arranger.midi_outs.push(MidiOutput::new( + //arranger.jack(), + //&format!("/M{}", arranger.midi_outs.len() + 1), + //&[] + //)?); + //Ok(None) + //}, + + //InputAdd => { + //arranger.midi_ins.push(MidiInput::new( + //arranger.jack(), + //&format!("M{}/", arranger.midi_ins.len() + 1), + //&[] + //)?); + //Ok(None) + //}, + + //SceneAdd => { + //let index = arranger.scene_add(None, None)?.0; + //*arranger.selection_mut() = match arranger.selection() { + //Selection::Scene(_) => Selection::Scene(index), + //Selection::TrackClip { track, scene } => Selection::TrackClip { + //track: *track, + //scene: index + //}, + //_ => *arranger.selection() + //}; + //Ok(None) // TODO + //}, + + //SceneSwap { index: usize, other: usize } => { + //let index = *index; + //let other = *other; + //Ok(Some(Self::SceneSwap { index, other })) + //}, + + //SceneDelete { index: usize } => { + //let index = *index; + //let scenes = arranger.scenes_mut(); + //Ok(if scenes.get(index).is_some() { + //let _scene = scenes.remove(index); + //None + //} else { + //None + //}) + //}, + + //SceneLaunch { index: usize } => { + //let index = *index; + //for track in 0..arranger.tracks.len() { + //let clip = arranger.scenes[index].clips[track].as_ref(); + //arranger.tracks[track].sequencer.enqueue_next(clip); + //} + //Ok(None) + //}, + + //Clip { scene: ClipCommand } => { + //todo!("delegate") + //}, + + //ClipGet { a: usize, b: usize } => { + ////(Get [a: usize, b: usize] cmd_todo!("\n\rtodo: clip: get: {a} {b}")) + ////("get" [a: usize, b: usize] Some(Self::Get(a.unwrap(), b.unwrap()))) + //todo!() + //}, + + //ClipPut { a: usize, b: usize } => { + ////(Put [t: usize, s: usize, c: MaybeClip] + ////Some(Self::Put(t, s, arranger.clip_put(t, s, c)))) + ////("put" [a: usize, b: usize, c: MaybeClip] Some(Self::Put(a.unwrap(), b.unwrap(), c.unwrap()))) + //todo!() + //}, + + //ClipDel { a: usize, b: usize } => { + ////("delete" [a: usize, b: usize] Some(Self::Put(a.unwrap(), b.unwrap(), None)))) + //todo!() + //}, + + //ClipEnqueue { a: usize, b: usize } => { + ////(Enqueue [t: usize, s: usize] + ////cmd!(arranger.tracks[t].sequencer.enqueue_next(arranger.scenes[s].clips[t].as_ref()))) + ////("enqueue" [a: usize, b: usize] Some(Self::Enqueue(a.unwrap(), b.unwrap()))) + //todo!() + //}, + + //ClipSwap { a: usize, b: usize }=> { + ////(Edit [clip: MaybeClip] cmd_todo!("\n\rtodo: clip: edit: {clip:?}")) + ////("edit" [a: MaybeClip] Some(Self::Edit(a.unwrap()))) + //todo!() + //}, + +//}); diff --git a/device/browse.rs b/device/browse.rs new file mode 100644 index 00000000..02d41448 --- /dev/null +++ b/device/browse.rs @@ -0,0 +1,220 @@ +use crate::*; +use std::path::PathBuf; +use std::ffi::OsString; + +#[derive(Clone, Debug)] +pub enum BrowseTarget { + SaveProject, + LoadProject, + ImportSample(Arc>>), + ExportSample(Arc>>), + ImportClip(Arc>>), + ExportClip(Arc>>), +} + +impl PartialEq for BrowseTarget { + fn eq (&self, other: &Self) -> bool { + match self { + Self::ImportSample(_) => false, + Self::ExportSample(_) => false, + Self::ImportClip(_) => false, + Self::ExportClip(_) => false, + t => matches!(other, t) + } + } +} + +/// Browses for phrase to import/export +#[derive(Debug, Clone, Default, PartialEq)] +pub struct Browse { + pub cwd: PathBuf, + pub dirs: Vec<(OsString, String)>, + pub files: Vec<(OsString, String)>, + pub filter: String, + pub index: usize, + pub scroll: usize, + pub size: Measure, +} + +impl Browse { + + pub fn new (cwd: Option) -> Usually { + let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? }; + let mut dirs = vec![]; + let mut files = vec![]; + for entry in std::fs::read_dir(&cwd)? { + let entry = entry?; + let name = entry.file_name(); + let decoded = name.clone().into_string().unwrap_or_else(|_|"".to_string()); + let meta = entry.metadata()?; + if meta.is_dir() { + dirs.push((name, format!("๐Ÿ“ {decoded}"))); + } else if meta.is_file() { + files.push((name, format!("๐Ÿ“„ {decoded}"))); + } + } + Ok(Self { + cwd, + dirs, + files, + filter: "".to_string(), + index: 0, + scroll: 0, + size: Measure::new(), + }) + } + + pub fn len (&self) -> usize { + self.dirs.len() + self.files.len() + } + + pub fn is_dir (&self) -> bool { + self.index < self.dirs.len() + } + + pub fn is_file (&self) -> bool { + self.index >= self.dirs.len() + } + + pub fn path (&self) -> PathBuf { + self.cwd.join(if self.is_dir() { + &self.dirs[self.index].0 + } else if self.is_file() { + &self.files[self.index - self.dirs.len()].0 + } else { + unreachable!() + }) + } + + pub fn chdir (&self) -> Usually { + Self::new(Some(self.path())) + } + +} + +impl Browse { + fn _todo_stub_path_buf (&self) -> PathBuf { + todo!() + } + fn _todo_stub_usize (&self) -> usize { + todo!() + } + fn _todo_stub_arc_str (&self) -> Arc { + todo!() + } +} + +def_command!(BrowseCommand: |browse: Browse| { + SetVisible => Ok(None), + SetPath { address: PathBuf } => Ok(None), + SetSearch { filter: Arc } => Ok(None), + SetCursor { cursor: usize } => Ok(None), +}); + +impl HasContent for Browse { + fn content (&self) -> impl Content { + Map::south(1, ||EntriesIterator { + offset: 0, + index: 0, + length: self.dirs.len() + self.files.len(), + browser: self, + }, |entry, _index|Fill::X(Align::w(entry))) + } +} + +struct EntriesIterator<'a> { + browser: &'a Browse, + offset: usize, + length: usize, + index: usize, +} + +impl<'a> Iterator for EntriesIterator<'a> { + type Item = Modify<&'a str>; + fn next (&mut self) -> Option { + let dirs = self.browser.dirs.len(); + let files = self.browser.files.len(); + let index = self.index; + if self.index < dirs { + self.index += 1; + Some(Tui::bold(true, self.browser.dirs[index].1.as_str())) + } else if self.index < dirs + files { + self.index += 1; + Some(Tui::bold(false, self.browser.files[index - dirs].1.as_str())) + } else { + None + } + } +} + +// Commands supported by [Browse] +//#[derive(Debug, Clone, PartialEq)] +//pub enum BrowseCommand { + //Begin, + //Cancel, + //Confirm, + //Select(usize), + //Chdir(PathBuf), + //Filter(Arc), +//} + //fn begin (browse: &mut Browse) => { + //unreachable!(); + //} + //fn cancel (browse: &mut Browse) => { + //todo!() + ////browse.mode = None; + ////Ok(None) + //} + //fn confirm (browse: &mut Browse) => { + //todo!() + ////Ok(match browse.mode { + ////Some(PoolMode::Import(index, ref mut browse)) => { + ////if browse.is_file() { + ////let path = browse.path(); + ////browse.mode = None; + ////let _undo = PoolClipCommand::import(browse, index, path)?; + ////None + ////} else if browse.is_dir() { + ////browse.mode = Some(PoolMode::Import(index, browse.chdir()?)); + ////None + ////} else { + ////None + ////} + ////}, + ////Some(PoolMode::Export(index, ref mut browse)) => { + ////todo!() + ////}, + ////_ => unreachable!(), + ////}) + //} + //fn select (browse: &mut Browse, index: usize) => { + //todo!() + ////Ok(match browse.mode { + ////Some(PoolMode::Import(index, ref mut browse)) => { + ////browse.index = index; + ////None + ////}, + ////Some(PoolMode::Export(index, ref mut browse)) => { + ////browse.index = index; + ////None + ////}, + ////_ => unreachable!(), + ////}) + //} + //fn chdir (browse: &mut Browse, dir: PathBuf) => { + //todo!() + ////Ok(match browse.mode { + ////Some(PoolMode::Import(index, ref mut browse)) => { + ////browse.mode = Some(PoolMode::Import(index, Browse::new(Some(dir))?)); + ////None + ////}, + ////Some(PoolMode::Export(index, ref mut browse)) => { + ////browse.mode = Some(PoolMode::Export(index, Browse::new(Some(dir))?)); + ////None + ////}, + ////_ => unreachable!(), + ////}) + //} + //fn filter (browse: &mut Browse, filter: Arc) => { + //todo!() + //} diff --git a/tek/device/clap.rs b/device/clap.rs similarity index 100% rename from tek/device/clap.rs rename to device/clap.rs diff --git a/device/clip.rs b/device/clip.rs new file mode 100644 index 00000000..ac0ca95b --- /dev/null +++ b/device/clip.rs @@ -0,0 +1,215 @@ +use crate::*; + +pub trait HasMidiClip { + fn clip (&self) -> Option>>; +} + +#[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 } + } + } +} + +/// A MIDI sequence. +#[derive(Debug, Clone, Default)] +pub struct MidiClip { + pub uuid: uuid::Uuid, + /// Name of clip + pub name: Arc, + /// Temporal resolution in pulses per quarter note + pub ppq: usize, + /// Length of clip in pulses + pub length: usize, + /// Notes in clip + pub notes: MidiData, + /// Whether to loop the clip or play it once + pub looped: bool, + /// Start of loop + pub loop_start: usize, + /// Length of loop + pub loop_length: usize, + /// All notes are displayed with minimum length + pub percussive: bool, + /// Identifying color of clip + pub color: ItemTheme, +} + +/// MIDI message structural +pub type MidiData = Vec>; + +impl MidiClip { + pub fn new ( + name: impl AsRef, + looped: bool, + length: usize, + notes: Option, + color: Option, + ) -> Self { + Self { + uuid: uuid::Uuid::new_v4(), + name: name.as_ref().into(), + ppq: PPQ, + length, + notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]), + looped, + loop_start: 0, + loop_length: length, + percussive: true, + color: color.unwrap_or_else(ItemTheme::random) + } + } + pub fn count_midi_messages (&self) -> usize { + let mut count = 0; + for tick in self.notes.iter() { + count += tick.len(); + } + count + } + pub fn set_length (&mut self, length: usize) { + self.length = length; + self.notes = vec![Vec::with_capacity(16);length]; + } + pub fn duplicate (&self) -> Self { + let mut clone = self.clone(); + clone.uuid = uuid::Uuid::new_v4(); + clone + } + pub fn toggle_loop (&mut self) { self.looped = !self.looped; } + pub fn record_event (&mut self, pulse: usize, message: MidiMessage) { + if pulse >= self.length { panic!("extend clip first") } + self.notes[pulse].push(message); + } + /// Check if a range `start..end` contains MIDI Note On `k` + pub fn contains_note_on (&self, k: u7, start: usize, end: usize) -> bool { + for events in self.notes[start.max(0)..end.min(self.notes.len())].iter() { + for event in events.iter() { + if let MidiMessage::NoteOn {key,..} = event { if *key == k { return true } } + } + } + false + } + pub fn stop_all () -> Self { + Self::new( + "Stop", + false, + 1, + Some(vec![vec![MidiMessage::Controller { + controller: 123.into(), + value: 0.into() + }]]), + Some(ItemColor::from_rgb(Color::Rgb(32, 32, 32)).into()) + ) + } +} + +impl PartialEq for MidiClip { + fn eq (&self, other: &Self) -> bool { + self.uuid == other.uuid + } +} + +impl Eq for MidiClip {} + +impl MidiClip { + fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } + fn _todo_bool_stub_ (&self) -> bool { todo!() } + fn _todo_usize_stub_ (&self) -> usize { todo!() } + fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } + fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } + fn _todo_opt_item_theme_stub (&self) -> Option { todo!() } +} + +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!() + } + +}); + +pub trait ClipsView: + TracksView + + ScenesView + + HasClipsSize + + Send + + Sync +{ + fn view_scenes_clips <'a> (&'a self) + -> impl Content + 'a + { + self.clips_size().of(Fill::XY(Bsp::a( + Fill::XY(Align::se(Tui::fg(Green, format!("{}x{}", self.clips_size().w(), self.clips_size().h())))), + Thunk::new(|to: &mut TuiOut|for ( + track_index, track, _, _ + ) in self.tracks_with_sizes() { + to.place(&Fixed::X(track.width as u16, + Fill::Y(self.view_track_clips(track_index, track)))) + })))) + } + + fn view_track_clips <'a> (&'a self, track_index: usize, track: &'a Track) -> impl Content + 'a { + Thunk::new(move|to: &mut TuiOut|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.rgb; + let mut outline = theme.base.rgb; + let bg = if self.selection().track() == Some(track_index) + && self.selection().scene() == Some(scene_index) + { + outline = theme.lighter.rgb; + theme.light.rgb + } else if self.selection().track() == Some(track_index) + || self.selection().scene() == Some(scene_index) + { + outline = theme.darkest.rgb; + theme.base.rgb + } else { + theme.dark.rgb + }; + let w = if self.selection().track() == Some(track_index) + && let Some(editor) = self.editor () + { + editor.width().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.height().max(12) + } else { + Self::H_SCENE + } as u16; + + to.place(&Fixed::XY(w, y, Bsp::b( + Fill::XY(Outer(true, Style::default().fg(outline))), + Fill::XY(Bsp::b( + Bsp::b( + Tui::fg_bg(outline, bg, Fill::XY("")), + Fill::XY(Align::nw(Tui::fg_bg(fg, bg, Tui::bold(true, name)))), + ), + Fill::XY(When::new(self.selection().track() == Some(track_index) + && self.selection().scene() == Some(scene_index) + && self.is_editing(), self.editor()))))))); + }) + } +} +//take!(ClipCommand |state: Arrangement, iter|state.selected_clip().as_ref() + //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); diff --git a/device/clock.rs b/device/clock.rs new file mode 100644 index 00000000..17ab9d36 --- /dev/null +++ b/device/clock.rs @@ -0,0 +1,421 @@ +use crate::*; +use std::fmt::Write; + +pub trait HasClock: Send + Sync { + fn clock (&self) -> &Clock; + fn clock_mut (&mut self) -> &mut Clock; +} + +impl> HasClock for T { + fn clock (&self) -> &Clock { self.get() } + fn clock_mut (&mut self) -> &mut Clock { self.get_mut() } +} + +#[derive(Clone, Default)] +pub struct Clock { + /// JACK transport handle. + pub transport: Arc>, + /// Global temporal resolution (shared by [Moment] fields) + pub timebase: Arc, + /// Current global sample and usec (monotonic from JACK clock) + pub global: Arc, + /// Global sample and usec at which playback started + pub started: Arc>>, + /// Playback offset (when playing not from start) + pub offset: Arc, + /// Current playhead position + pub playhead: Arc, + /// Note quantization factor + pub quant: Arc, + /// Launch quantization factor + pub sync: Arc, + /// Size of buffer in samples + pub chunk: Arc, + // Cache of formatted strings + pub view_cache: Arc>, + /// For syncing the clock to an external source + #[cfg(feature = "port")] pub midi_in: Arc>>, + /// For syncing other devices to this clock + #[cfg(feature = "port")] pub midi_out: Arc>>, + /// For emitting a metronome + #[cfg(feature = "port")] pub click_out: Arc>>, +} + +impl std::fmt::Debug for Clock { + fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("Clock") + .field("timebase", &self.timebase) + .field("chunk", &self.chunk) + .field("quant", &self.quant) + .field("sync", &self.sync) + .field("global", &self.global) + .field("playhead", &self.playhead) + .field("started", &self.started) + .finish() + } +} + +impl Clock { + pub fn new (jack: &Jack<'static>, bpm: Option) -> Usually { + let (chunk, transport) = jack.with_client(|c|(c.buffer_size(), c.transport())); + let timebase = Arc::new(Timebase::default()); + let clock = Self { + quant: Arc::new(24.into()), + sync: Arc::new(384.into()), + transport: Arc::new(Some(transport)), + chunk: Arc::new((chunk as usize).into()), + global: Arc::new(Moment::zero(&timebase)), + playhead: Arc::new(Moment::zero(&timebase)), + offset: Arc::new(Moment::zero(&timebase)), + started: RwLock::new(None).into(), + timebase, + midi_in: Arc::new(RwLock::new(Some(MidiInput::new(jack, &"M/clock", &[])?))), + midi_out: Arc::new(RwLock::new(Some(MidiOutput::new(jack, &"clock/M", &[])?))), + click_out: Arc::new(RwLock::new(Some(AudioOutput::new(jack, &"click", &[])?))), + ..Default::default() + }; + if let Some(bpm) = bpm { + clock.timebase.bpm.set(bpm); + } + Ok(clock) + } + pub fn timebase (&self) -> &Arc { + &self.timebase + } + /// Current sample rate + pub fn sr (&self) -> &SampleRate { + &self.timebase.sr + } + /// Current tempo + pub fn bpm (&self) -> &BeatsPerMinute { + &self.timebase.bpm + } + /// Current MIDI resolution + pub fn ppq (&self) -> &PulsesPerQuaver { + &self.timebase.ppq + } + /// Next pulse that matches launch sync (for phrase switchover) + pub fn next_launch_pulse (&self) -> usize { + let sync = self.sync.get() as usize; + let pulse = self.playhead.pulse.get() as usize; + if pulse % sync == 0 { + pulse + } else { + (pulse / sync + 1) * sync + } + } + /// Start playing, optionally seeking to a given location beforehand + pub fn play_from (&self, start: Option) -> Usually<()> { + if let Some(transport) = self.transport.as_ref() { + if let Some(start) = start { + transport.locate(start)?; + } + transport.start()?; + } + Ok(()) + } + /// Pause, optionally seeking to a given location afterwards + pub fn pause_at (&self, pause: Option) -> Usually<()> { + if let Some(transport) = self.transport.as_ref() { + transport.stop()?; + if let Some(pause) = pause { + transport.locate(pause)?; + } + } + Ok(()) + } + /// Is currently paused? + pub fn is_stopped (&self) -> bool { + self.started.read().unwrap().is_none() + } + /// Is currently playing? + pub fn is_rolling (&self) -> bool { + self.started.read().unwrap().is_some() + } + /// Update chunk size + pub fn set_chunk (&self, n_frames: usize) { + self.chunk.store(n_frames, Relaxed); + } + pub fn update_from_scope (&self, scope: &ProcessScope) -> Usually<()> { + // Store buffer length + self.set_chunk(scope.n_frames() as usize); + + // Store reported global frame and usec + let CycleTimes { current_frames, current_usecs, .. } = scope.cycle_times()?; + self.global.sample.set(current_frames as f64); + self.global.usec.set(current_usecs as f64); + + let mut started = self.started.write().unwrap(); + + // If transport has just started or just stopped, + // update starting point: + if let Some(transport) = self.transport.as_ref() { + match (transport.query_state()?, started.as_ref()) { + (TransportState::Rolling, None) => { + let moment = Moment::zero(&self.timebase); + moment.sample.set(current_frames as f64); + moment.usec.set(current_usecs as f64); + *started = Some(moment); + }, + (TransportState::Stopped, Some(_)) => { + *started = None; + }, + _ => {} + }; + } + + self.playhead.update_from_sample(started.as_ref() + .map(|started|current_frames as f64 - started.sample.get()) + .unwrap_or(0.)); + + Ok(()) + } + + pub fn bbt (&self) -> PositionBBT { + let pulse = self.playhead.pulse.get() as i32; + let ppq = self.timebase.ppq.get() as i32; + let bpm = self.timebase.bpm.get(); + let bar = (pulse / ppq) / 4; + PositionBBT { + bar: 1 + bar, + beat: 1 + (pulse / ppq) % 4, + tick: (pulse % ppq), + bar_start_tick: (bar * 4 * ppq) as f64, + beat_type: 4., + beats_per_bar: 4., + beats_per_minute: bpm, + ticks_per_beat: ppq as f64 + } + } + + pub fn next_launch_instant (&self) -> Moment { + Moment::from_pulse(self.timebase(), self.next_launch_pulse() as f64) + } + + /// Get index of first sample to populate. + /// + /// Greater than 0 means that the first pulse of the clip + /// falls somewhere in the middle of the chunk. + pub fn get_sample_offset (&self, scope: &ProcessScope, started: &Moment) -> usize{ + (scope.last_frame_time() as usize).saturating_sub( + started.sample.get() as usize + + self.started.read().unwrap().as_ref().unwrap().sample.get() as usize + ) + } + + // Get iterator that emits sample paired with pulse. + // + // * Sample: index into output buffer at which to write MIDI event + // * Pulse: index into clip from which to take the MIDI event + // + // Emitted for each sample of the output buffer that corresponds to a MIDI pulse. + pub fn get_pulses (&self, scope: &ProcessScope, offset: usize) -> TicksIterator { + self.timebase().pulses_between_samples(offset, offset + scope.n_frames() as usize) + } +} + +impl Clock { + fn _todo_provide_u32 (&self) -> u32 { + todo!() + } + fn _todo_provide_opt_u32 (&self) -> Option { + todo!() + } + fn _todo_provide_f64 (&self) -> f64 { + todo!() + } +} + +impl Command for ClockCommand { + fn execute (&self, state: &mut T) -> Perhaps { + self.execute(state.clock_mut()) // awesome + } +} + +def_command!(ClockCommand: |clock: Clock| { + SeekUsec { usec: f64 } => { + clock.playhead.update_from_usec(*usec); Ok(None) }, + SeekSample { sample: f64 } => { + clock.playhead.update_from_sample(*sample); Ok(None) }, + SeekPulse { pulse: f64 } => { + clock.playhead.update_from_pulse(*pulse); Ok(None) }, + SetBpm { bpm: f64 } => Ok(Some( + Self::SetBpm { bpm: clock.timebase().bpm.set(*bpm) })), + SetQuant { quant: f64 } => Ok(Some( + Self::SetQuant { quant: clock.quant.set(*quant) })), + SetSync { sync: f64 } => Ok(Some( + Self::SetSync { sync: clock.sync.set(*sync) })), + + Play { position: Option } => { + clock.play_from(*position)?; Ok(None) /* TODO Some(Pause(previousPosition)) */ }, + Pause { position: Option } => { + clock.pause_at(*position)?; Ok(None) }, + + TogglePlayback { position: u32 } => Ok(if clock.is_rolling() { + clock.pause_at(Some(*position))?; None + } else { + clock.play_from(Some(*position))?; None + }), +}); + +pub fn view_transport ( + play: bool, + bpm: Arc>, + beat: Arc>, + time: Arc>, +) -> impl Content { + let theme = ItemTheme::G[96]; + Tui::bg(Black, row!(Bsp::a( + Fill::XY(Align::w(button_play_pause(play))), + Fill::XY(Align::e(row!( + FieldH(theme, "BPM", bpm), + FieldH(theme, "Beat", beat), + FieldH(theme, "Time", time), + ))) + ))) +} + +pub fn view_status ( + sel: Option>, + sr: Arc>, + buf: Arc>, + lat: Arc>, +) -> impl Content { + let theme = ItemTheme::G[96]; + Tui::bg(Black, row!(Bsp::a( + Fill::XY(Align::w(sel.map(|sel|FieldH(theme, "Selected", sel)))), + Fill::XY(Align::e(row!( + FieldH(theme, "SR", sr), + FieldH(theme, "Buf", buf), + FieldH(theme, "Lat", lat), + ))) + ))) +} + +pub(crate) fn button_play_pause (playing: bool) -> impl Content { + let compact = true;//self.is_editing(); + Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, + Either::new(compact, + Thunk::new(move|to: &mut TuiOut|to.place(&Fixed::X(9, Either::new(playing, + Tui::fg(Rgb(0, 255, 0), " PLAYING "), + Tui::fg(Rgb(255, 128, 0), " STOPPED "))) + )), + Thunk::new(move|to: &mut TuiOut|to.place(&Fixed::X(5, Either::new(playing, + Tui::fg(Rgb(0, 255, 0), Bsp::s(" ๐Ÿญ๐Ÿญ‘๐Ÿฌฝ ", " ๐Ÿญž๐Ÿญœ๐Ÿญ˜ ",)), + Tui::fg(Rgb(255, 128, 0), Bsp::s(" โ–—โ–„โ–– ", " โ–โ–€โ–˜ ",)))) + )) + ) + ) +} + +#[derive(Debug)] pub struct ViewCache { + pub sr: Memo, String>, + pub buf: Memo, String>, + pub lat: Memo, String>, + pub bpm: Memo, String>, + pub beat: Memo, String>, + pub time: Memo, String>, +} + +impl Default for ViewCache { + fn default () -> Self { + let mut beat = String::with_capacity(16); + let _ = write!(beat, "{}", Self::BEAT_EMPTY); + let mut time = String::with_capacity(16); + let _ = write!(time, "{}", Self::TIME_EMPTY); + let mut bpm = String::with_capacity(16); + let _ = write!(bpm, "{}", Self::BPM_EMPTY); + Self { + beat: Memo::new(None, beat), + time: Memo::new(None, time), + bpm: Memo::new(None, bpm), + sr: Memo::new(None, String::with_capacity(16)), + buf: Memo::new(None, String::with_capacity(16)), + lat: Memo::new(None, String::with_capacity(16)), + } + } +} + +impl ViewCache { + pub const BEAT_EMPTY: &'static str = "-.-.--"; + pub const TIME_EMPTY: &'static str = "-.---s"; + pub const BPM_EMPTY: &'static str = "---.---"; + + //pub fn track_counter (cache: &Arc>, track: usize, tracks: usize) + //-> Arc> + //{ + //let data = (track, tracks); + //cache.write().unwrap().trks.update(Some(data), rewrite!(buf, "{}/{}", data.0, data.1)); + //cache.read().unwrap().trks.view.clone() + //} + + //pub fn scene_add (cache: &Arc>, scene: usize, scenes: usize, is_editing: bool) + //-> impl Content + //{ + //let data = (scene, scenes); + //cache.write().unwrap().scns.update(Some(data), rewrite!(buf, "({}/{})", data.0, data.1)); + //button_3("S", "add scene", cache.read().unwrap().scns.view.clone(), is_editing) + //} + + pub fn update_clock (cache: &Arc>, clock: &Clock, compact: bool) { + let rate = clock.timebase.sr.get(); + let chunk = clock.chunk.load(Relaxed) as f64; + let lat = chunk / rate * 1000.; + let delta = |start: &Moment|clock.global.usec.get() - start.usec.get(); + let mut cache = cache.write().unwrap(); + cache.buf.update(Some(chunk), rewrite!(buf, "{chunk}")); + cache.lat.update(Some(lat), rewrite!(buf, "{lat:.1}ms")); + cache.sr.update(Some((compact, rate)), |buf,_,_|{ + buf.clear(); + if compact { + write!(buf, "{:.1}kHz", rate / 1000.) + } else { + write!(buf, "{:.0}Hz", rate) + } + }); + if let Some(now) = clock.started.read().unwrap().as_ref().map(delta) { + let pulse = clock.timebase.usecs_to_pulse(now); + let time = now/1000000.; + let bpm = clock.timebase.bpm.get(); + cache.beat.update(Some(pulse), |buf, _, _|{ + buf.clear(); + clock.timebase.format_beats_1_to(buf, pulse) + }); + cache.time.update(Some(time), rewrite!(buf, "{:.3}s", time)); + cache.bpm.update(Some(bpm), rewrite!(buf, "{:.3}", bpm)); + } else { + cache.beat.update(None, rewrite!(buf, "{}", ViewCache::BEAT_EMPTY)); + cache.time.update(None, rewrite!(buf, "{}", ViewCache::TIME_EMPTY)); + cache.bpm.update(None, rewrite!(buf, "{}", ViewCache::BPM_EMPTY)); + } + } + + //pub fn view_h2 (&self) -> impl Content { + //let cache = self.project.clock.view_cache.clone(); + //let cache = cache.read().unwrap(); + //add(&Fixed::x(15, Align::w(Bsp::s( + //FieldH(theme, "Beat", cache.beat.view.clone()), + //FieldH(theme, "Time", cache.time.view.clone()), + //)))); + //add(&Fixed::x(13, Align::w(Bsp::s( + //Fill::X(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))), + //Fill::X(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))), + //)))); + //add(&Fixed::x(12, Align::w(Bsp::s( + //Fill::X(Align::w(FieldH(theme, "Buf", cache.buf.view.clone()))), + //Fill::X(Align::w(FieldH(theme, "Lat", cache.lat.view.clone()))), + //)))); + //add(&Bsp::s( + //Fill::X(Align::w(FieldH(theme, "Selected", Align::w(self.selection().describe( + //self.tracks(), + //self.scenes() + //))))), + //Fill::X(Align::w(FieldH(theme, format!("History ({})", self.history.len()), + //self.history.last().map(|last|Fill::X(Align::w(format!("{:?}", last.0))))))) + //)); + ////if let Some(last) = self.history.last() { + ////add(&FieldV(theme, format!("History ({})", self.history.len()), + ////Fill::X(Align::w(format!("{:?}", last.0))))); + ////} + //} +} diff --git a/tek/device.rs b/device/device.rs similarity index 70% rename from tek/device.rs rename to device/device.rs index d534b0e9..4a7c6987 100644 --- a/tek/device.rs +++ b/device/device.rs @@ -1,4 +1,47 @@ -use crate::*; +#![feature(trait_alias)] + +pub(crate) use ::{ + tek_engine::*, + tek_engine::tengri::{ + Usually, Perhaps, Has, MaybeHas, has, maybe_has, from, + input::*, + output::*, + tui::*, + tui::ratatui, + tui::ratatui::widgets::{Widget, canvas::{Canvas, Line}}, + tui::ratatui::prelude::{Rect, Style, Stylize, Buffer, Color::{self, *}}, + }, + std::{ + cmp::Ord, + ffi::OsString, + fmt::{Debug, Formatter}, + fs::File, + path::PathBuf, + sync::{Arc, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}}, + } +}; + +#[cfg(feature = "sampler")] pub(crate) use symphonia::{ + core::{ + formats::Packet, + codecs::{Decoder, CODEC_TYPE_NULL}, + //errors::Error as SymphoniaError, + io::MediaSourceStream, + probe::Hint, + audio::SampleBuffer, + }, + default::get_codecs, +}; + +#[cfg(feature = "lv2")] use std::thread::{spawn, JoinHandle}; + +#[cfg(feature = "lv2_gui")] use ::winit::{ + application::ApplicationHandler, + event::WindowEvent, + event_loop::{ActiveEventLoop, ControlFlow, EventLoop}, + window::{Window, WindowId}, + platform::x11::EventLoopBuilderExtX11 +}; /// Define a type alias for iterators of sized items (columns). macro_rules! def_sizes_iter { @@ -11,6 +54,7 @@ macro_rules! def_sizes_iter { #[cfg(feature = "arranger")] mod arranger; #[cfg(feature = "arranger")] pub use self::arranger::*; #[cfg(feature = "browse")] mod browse; #[cfg(feature = "browse")] pub use self::browse::*; #[cfg(feature = "clap")] mod clap; #[cfg(feature = "clap")] pub use self::clap::*; +#[cfg(feature = "clip")] mod clip; #[cfg(feature = "clip")] pub use self::clip::*; #[cfg(feature = "clock")] mod clock; #[cfg(feature = "clock")] pub use self::clock::*; #[cfg(feature = "editor")] mod editor; #[cfg(feature = "editor")] pub use self::editor::*; #[cfg(feature = "lv2")] mod lv2; #[cfg(feature = "lv2")] pub use self::lv2::*; @@ -19,8 +63,11 @@ macro_rules! def_sizes_iter { #[cfg(feature = "pool")] mod pool; #[cfg(feature = "pool")] pub use self::pool::*; #[cfg(feature = "port")] mod port; #[cfg(feature = "port")] pub use self::port::*; #[cfg(feature = "sampler")] mod sampler; #[cfg(feature = "sampler")] pub use self::sampler::*; +#[cfg(feature = "scene")] mod scene; #[cfg(feature = "scene")] pub use self::scene::*; +#[cfg(feature = "select")] mod select; #[cfg(feature = "select")] pub use self::select::*; #[cfg(feature = "sequencer")] mod sequencer; #[cfg(feature = "sequencer")] pub use self::sequencer::*; #[cfg(feature = "sf2")] mod sf2; #[cfg(feature = "sf2")] pub use self::sf2::*; +#[cfg(feature = "track")] mod track; #[cfg(feature = "track")] pub use self::track::*; #[cfg(feature = "vst2")] mod vst2; #[cfg(feature = "vst2")] pub use self::vst2::*; #[cfg(feature = "vst3")] mod vst3; #[cfg(feature = "vst3")] pub use self::vst3::*; @@ -50,12 +97,12 @@ pub fn toggle_bool ( pub fn device_kinds () -> &'static [&'static str] { &[ - "Sampler", - "Plugin (LV2)", + #[cfg(feature = "sampler")] "Sampler", + #[cfg(feature = "lv2")] "Plugin (LV2)", ] } -impl> + Has> HasDevices for T { +impl>> HasDevices for T { fn devices (&self) -> &Vec { self.get() } @@ -64,7 +111,7 @@ impl> + Has> HasDevices for T { } } -pub trait HasDevices: Has { +pub trait HasDevices { fn devices (&self) -> &Vec; fn devices_mut (&mut self) -> &mut Vec; } @@ -133,3 +180,5 @@ audio!(|self: DeviceAudio<'a>, client, scope|{ }); def_command!(DeviceCommand: |device: Device| {}); +//take!(DeviceCommand|state: Arrangement, iter|state.selected_device().as_ref() + //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); diff --git a/device/editor.rs b/device/editor.rs new file mode 100644 index 00000000..2ec4d37e --- /dev/null +++ b/device/editor.rs @@ -0,0 +1,550 @@ +use crate::*; + +#[macro_export] macro_rules! has_editor { + (|$self:ident: $Struct:ident|{ + editor = $e0:expr; + editor_w = $e1:expr; + editor_h = $e2:expr; + is_editing = $e3:expr; + }) => { + impl HasEditor for $Struct { + fn editor (&$self) -> Option<&MidiEditor> { $e0.as_ref() } + fn editor_mut (&mut $self) -> Option<&mut MidiEditor> { $e0.as_mut() } + fn editor_w (&$self) -> usize { $e1 } + fn editor_h (&$self) -> usize { $e2 } + fn is_editing (&$self) -> bool { $e3 } + } + }; +} +impl>> HasEditor for T {} +pub trait HasEditor: Has> { + fn editor (&self) -> Option<&MidiEditor> { self.get().as_ref() } + fn editor_mut (&mut self) -> Option<&mut MidiEditor> { self.get_mut().as_mut() } + fn is_editing (&self) -> bool { self.editor().is_some() } + fn editor_w (&self) -> usize { self.editor().map(|e|e.size.w()).unwrap_or(0) } + fn editor_h (&self) -> usize { self.editor().map(|e|e.size.h()).unwrap_or(0) } +} + +/// Contains state for viewing and editing a clip +pub struct MidiEditor { + /// Size of editor on screen + pub size: Measure, + /// View mode and state of editor + pub mode: PianoHorizontal, +} + +has!(Measure: |self: MidiEditor|self.size); +impl Default for MidiEditor { fn default () -> Self { Self { size: Measure::new(), mode: PianoHorizontal::new(None), } } } +impl std::fmt::Debug for MidiEditor { + fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("MidiEditor").field("mode", &self.mode).finish() + } +} +from!(|clip: &Arc>|MidiEditor = { let model = Self::from(Some(clip.clone())); model.redraw(); model }); +from!(|clip: Option>>|MidiEditor = { + let mut model = Self::default(); + *model.clip_mut() = clip; + model.redraw(); + model +}); + +impl MidiEditor { + /// Put note at current position + pub fn put_note (&mut self, advance: bool) { + let mut redraw = false; + if let Some(clip) = self.clip() { + let mut clip = clip.write().unwrap(); + let note_start = self.get_time_pos(); + let note_pos = self.get_note_pos(); + let note_len = self.get_note_len(); + let note_end = note_start + (note_len.saturating_sub(1)); + let key: u7 = u7::from(note_pos as u8); + let vel: u7 = 100.into(); + let length = clip.length; + let note_end = note_end % length; + let note_on = MidiMessage::NoteOn { key, vel }; + if !clip.notes[note_start].iter().any(|msg|*msg == note_on) { + clip.notes[note_start].push(note_on); + } + let note_off = MidiMessage::NoteOff { key, vel }; + if !clip.notes[note_end].iter().any(|msg|*msg == note_off) { + clip.notes[note_end].push(note_off); + } + if advance { + self.set_time_pos((note_end + 1) % clip.length); + } + redraw = true; + } + if redraw { + self.mode.redraw(); + } + } +} +impl TimeRange for MidiEditor { + fn time_len (&self) -> &AtomicUsize { self.mode.time_len() } + fn time_zoom (&self) -> &AtomicUsize { self.mode.time_zoom() } + fn time_lock (&self) -> &AtomicBool { self.mode.time_lock() } + fn time_start (&self) -> &AtomicUsize { self.mode.time_start() } + fn time_axis (&self) -> &AtomicUsize { self.mode.time_axis() } +} +impl NoteRange for MidiEditor { + fn note_lo (&self) -> &AtomicUsize { self.mode.note_lo() } + fn note_axis (&self) -> &AtomicUsize { self.mode.note_axis() } +} +impl NotePoint for MidiEditor { + fn note_len (&self) -> &AtomicUsize { self.mode.note_len() } + fn note_pos (&self) -> &AtomicUsize { self.mode.note_pos() } +} +impl TimePoint for MidiEditor { + fn time_pos (&self) -> &AtomicUsize { self.mode.time_pos() } +} +impl MidiViewer for MidiEditor { + fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { self.mode.buffer_size(clip) } + fn redraw (&self) { self.mode.redraw() } + fn clip (&self) -> &Option>> { self.mode.clip() } + fn clip_mut (&mut self) -> &mut Option>> { self.mode.clip_mut() } + fn set_clip (&mut self, p: Option<&Arc>>) { self.mode.set_clip(p) } +} + +def_command!(MidiEditCommand: |editor: MidiEditor| { + Show { clip: Option>> } => { + editor.set_clip(clip.as_ref()); editor.redraw(); Ok(None) }, + DeleteNote => { + editor.redraw(); todo!() }, + AppendNote { advance: bool } => { + editor.put_note(*advance); editor.redraw(); Ok(None) }, + SetNotePos { pos: usize } => { + editor.set_note_pos((*pos).min(127)); editor.redraw(); Ok(None) }, + SetNoteLen { len: usize } => { + editor.set_note_len(*len); editor.redraw(); Ok(None) }, + SetNoteScroll { scroll: usize } => { + editor.set_note_lo((*scroll).min(127)); editor.redraw(); Ok(None) }, + SetTimePos { pos: usize } => { + editor.set_time_pos(*pos); editor.redraw(); Ok(None) }, + SetTimeScroll { scroll: usize } => { + editor.set_time_start(*scroll); editor.redraw(); Ok(None) }, + SetTimeZoom { zoom: usize } => { + editor.set_time_zoom(*zoom); editor.redraw(); Ok(None) }, + SetTimeLock { lock: bool } => { + editor.set_time_lock(*lock); editor.redraw(); Ok(None) }, + // TODO: 1-9 seek markers that by default start every 8th of the clip +}); + +impl MidiEditor { + fn _todo_opt_clip_stub (&self) -> Option>> { todo!() } + fn clip_length (&self) -> usize { self.clip().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1) } + fn note_length (&self) -> usize { self.get_note_len() } + fn note_pos (&self) -> usize { self.get_note_pos() } + fn note_pos_next (&self) -> usize { self.get_note_pos() + 1 } + fn note_pos_next_octave (&self) -> usize { self.get_note_pos() + 12 } + fn note_pos_prev (&self) -> usize { self.get_note_pos().saturating_sub(1) } + fn note_pos_prev_octave (&self) -> usize { self.get_note_pos().saturating_sub(12) } + fn note_len (&self) -> usize { self.get_note_len() } + fn note_len_next (&self) -> usize { self.get_note_len() + 1 } + fn note_len_prev (&self) -> usize { self.get_note_len().saturating_sub(1) } + fn note_range (&self) -> usize { self.get_note_axis() } + fn note_range_next (&self) -> usize { self.get_note_axis() + 1 } + fn note_range_prev (&self) -> usize { self.get_note_axis().saturating_sub(1) } + fn time_zoom (&self) -> usize { self.get_time_zoom() } + fn time_zoom_next (&self) -> usize { self.get_time_zoom() + 1 } + fn time_zoom_next_fine (&self) -> usize { self.get_time_zoom() + 1 } + fn time_zoom_prev (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } + fn time_zoom_prev_fine (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } + fn time_lock (&self) -> bool { self.get_time_lock() } + fn time_lock_toggled (&self) -> bool { !self.get_time_lock() } + fn time_pos (&self) -> usize { self.get_time_pos() } + fn time_pos_next (&self) -> usize { (self.get_time_pos() + self.get_note_len()) % self.clip_length() } + fn time_pos_next_fine (&self) -> usize { (self.get_time_pos() + 1) % self.clip_length() } + fn time_pos_prev (&self) -> usize { + let step = self.get_note_len(); + self.get_time_pos().overflowing_sub(step) + .0.min(self.clip_length().saturating_sub(step)) + } + fn time_pos_prev_fine (&self) -> usize { + self.get_time_pos().overflowing_sub(1) + .0.min(self.clip_length().saturating_sub(1)) + } +} + +impl Draw for MidiEditor { fn draw (&self, to: &mut TuiOut) { self.content().draw(to) } } +impl Layout for MidiEditor { fn layout (&self, to: [u16;4]) -> [u16;4] { self.content().layout(to) } } +impl HasContent for MidiEditor { + fn content (&self) -> impl Content { self.autoscroll(); /*self.autozoom();*/ self.size.of(&self.mode) } +} + +impl MidiEditor { + pub fn clip_status (&self) -> impl Content + '_ { + let (_color, name, length, looped) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { + (clip.color, clip.name.clone(), clip.length, clip.looped) + } else { (ItemTheme::G[64], String::new().into(), 0, false) }; + Fixed::X(20, col!( + Fill::X(Align::w(Bsp::e( + button_2("f2", "name ", false), + Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{name} "))))))), + Fill::X(Align::w(Bsp::e( + button_2("l", "ength ", false), + Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{length} "))))))), + Fill::X(Align::w(Bsp::e( + button_2("r", "epeat ", false), + Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{looped} "))))))), + )) + } + pub fn edit_status (&self) -> impl Content + '_ { + let (_color, length) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { + (clip.color, clip.length) + } else { (ItemTheme::G[64], 0) }; + let time_pos = self.get_time_pos(); + let time_zoom = self.get_time_zoom(); + let time_lock = if self.get_time_lock() { "[lock]" } else { " " }; + let note_pos = self.get_note_pos(); + let note_name = format!("{:4}", Note::pitch_to_name(note_pos)); + let note_pos = format!("{:>3}", note_pos); + let note_len = format!("{:>4}", self.get_note_len()); + Fixed::X(20, col!( + Fill::X(Align::w(Bsp::e( + button_2("t", "ime ", false), + Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), + format!("{length} /{time_zoom} +{time_pos} "))))))), + Fill::X(Align::w(Bsp::e( + button_2("z", "lock ", false), + Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), + format!("{time_lock}"))))))), + Fill::X(Align::w(Bsp::e( + button_2("x", "note ", false), + Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), + format!("{note_name} {note_pos} {note_len}"))))))), + )) + } +} + +/// A clip, rendered as a horizontal piano roll. +#[derive(Clone)] +pub struct PianoHorizontal { + pub clip: Option>>, + /// Buffer where the whole clip is rerendered on change + pub buffer: Arc>, + /// Size of actual notes area + pub size: Measure, + /// The display window + pub range: MidiRangeModel, + /// The note cursor + pub point: MidiPointModel, + /// The highlight color palette + pub color: ItemTheme, + /// Width of the keyboard + pub keys_width: u16, +} + +has!(Measure:|self:PianoHorizontal|self.size); + +impl PianoHorizontal { + pub fn new (clip: Option<&Arc>>) -> Self { + let size = Measure::new(); + let mut range = MidiRangeModel::from((12, true)); + range.time_axis = size.x.clone(); + range.note_axis = size.y.clone(); + let piano = Self { + keys_width: 5, + size, + range, + buffer: RwLock::new(Default::default()).into(), + point: MidiPointModel::default(), + clip: clip.cloned(), + color: clip.as_ref().map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]), + }; + piano.redraw(); + piano + } +} + +pub(crate) fn note_y_iter (note_lo: usize, note_hi: usize, y0: u16) + -> impl Iterator +{ + (note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n)) +} + +impl Draw for PianoHorizontal { fn draw (&self, to: &mut TuiOut) { self.content().draw(to) } } +impl Layout for PianoHorizontal { fn layout (&self, to: [u16;4]) -> [u16;4] { self.content().layout(to) } } +impl HasContent for PianoHorizontal { + fn content (&self) -> impl Content { + Bsp::s( + Bsp::e(Fixed::X(5, format!("{}x{}", self.size.w(), self.size.h())), self.timeline()), + Bsp::e(self.keys(), self.size.of(Bsp::b(Fill::XY(self.notes()), Fill::XY(self.cursor())))), + ) + } +} + +impl PianoHorizontal { + /// Draw the piano roll background. + /// + /// This mode uses full blocks on note on and half blocks on legato: โ–ˆโ–„ โ–ˆโ–„ โ–ˆโ–„ + fn draw_bg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize, note_len: usize, note_point: usize, time_point: usize) { + for (y, note) in (0..=127).rev().enumerate() { + for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) { + let cell = buf.get_mut(x, y).unwrap(); + if note == (127-note_point) || time == time_point { + cell.set_bg(Rgb(0,0,0)); + } else { + cell.set_bg(clip.color.darkest.rgb); + } + if time % 384 == 0 { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('โ”‚'); + } else if time % 96 == 0 { + cell.set_fg(clip.color.dark.rgb); + cell.set_char('โ•Ž'); + } else if time % note_len == 0 { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('โ”Š'); + } else if (127 - note) % 12 == 0 { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('='); + } else if (127 - note) % 6 == 0 { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('โ€”'); + } else { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('ยท'); + } + } + } + } + /// Draw the piano roll foreground. + /// + /// This mode uses full blocks on note on and half blocks on legato: โ–ˆโ–„ โ–ˆโ–„ โ–ˆโ–„ + fn draw_fg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize) { + let style = Style::default().fg(clip.color.base.rgb);//.bg(Rgb(0, 0, 0)); + let mut notes_on = [false;128]; + for (x, time_start) in (0..clip.length).step_by(zoom).enumerate() { + for (_y, note) in (0..=127).rev().enumerate() { + if let Some(cell) = buf.get_mut(x, note) { + if notes_on[note] { + cell.set_char('โ–‚'); + cell.set_style(style); + } + } + } + let time_end = time_start + zoom; + for time in time_start..time_end.min(clip.length) { + for event in clip.notes[time].iter() { + match event { + MidiMessage::NoteOn { key, .. } => { + let note = key.as_int() as usize; + if let Some(cell) = buf.get_mut(x, note) { + cell.set_char('โ–ˆ'); + cell.set_style(style); + } + notes_on[note] = true + }, + MidiMessage::NoteOff { key, .. } => { + notes_on[key.as_int() as usize] = false + }, + _ => {} + } + } + } + + } + } + fn notes (&self) -> impl Content { + let time_start = self.get_time_start(); + let note_lo = self.get_note_lo(); + let note_hi = self.get_note_hi(); + let buffer = self.buffer.clone(); + Thunk::new(move|to: &mut TuiOut|{ + let source = buffer.read().unwrap(); + let [x0, y0, w, _h] = to.area().xywh(); + //if h as usize != note_axis { + //panic!("area height mismatch: {h} <> {note_axis}"); + //} + for (area_x, screen_x) in (x0..x0+w).enumerate() { + for (area_y, screen_y, _note) in note_y_iter(note_lo, note_hi, y0) { + let source_x = time_start + area_x; + let source_y = note_hi - area_y; + // TODO: enable loop rollover: + //let source_x = (time_start + area_x) % source.width.max(1); + //let source_y = (note_hi - area_y) % source.height.max(1); + let is_in_x = source_x < source.width; + let is_in_y = source_y < source.height; + if is_in_x && is_in_y { + if let Some(source_cell) = source.get(source_x, source_y) { + if let Some(cell) = to.buffer.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) { + *cell = source_cell.clone(); + } + } + } + } + } + }) + } + fn cursor (&self) -> impl Content { + let note_hi = self.get_note_hi(); + let note_lo = self.get_note_lo(); + let note_pos = self.get_note_pos(); + let note_len = self.get_note_len(); + let time_pos = self.get_time_pos(); + let time_start = self.get_time_start(); + let time_zoom = self.get_time_zoom(); + let style = Some(Style::default().fg(self.color.lightest.rgb)); + Thunk::new(move|to: &mut TuiOut|{ + let [x0, y0, w, _] = to.area().xywh(); + for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { + if note == note_pos { + for x in 0..w { + let screen_x = x0 + x; + let time_1 = time_start + x as usize * time_zoom; + let time_2 = time_1 + time_zoom; + if time_1 <= time_pos && time_pos < time_2 { + to.blit(&"โ–ˆ", screen_x, screen_y, style); + let tail = note_len as u16 / time_zoom as u16; + for x_tail in (screen_x + 1)..(screen_x + tail) { + to.blit(&"โ–‚", x_tail, screen_y, style); + } + break + } + } + break + } + } + }) + } + fn keys (&self) -> impl Content { + let state = self; + let color = state.color; + let note_lo = state.get_note_lo(); + let note_hi = state.get_note_hi(); + let note_pos = state.get_note_pos(); + let key_style = Some(Style::default().fg(Rgb(192, 192, 192)).bg(Rgb(0, 0, 0))); + let off_style = Some(Style::default().fg(Tui::g(255))); + let on_style = Some(Style::default().fg(Rgb(255,0,0)).bg(color.base.rgb).bold()); + Fill::Y(Fixed::X(self.keys_width, Thunk::new(move|to: &mut TuiOut|{ + let [x, y0, _w, _h] = to.area().xywh(); + for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { + to.blit(&to_key(note), x, screen_y, key_style); + if note > 127 { + continue + } + if note == note_pos { + to.blit(&format!("{:<5}", Note::pitch_to_name(note)), x, screen_y, on_style) + } else { + to.blit(&Note::pitch_to_name(note), x, screen_y, off_style) + }; + } + }))) + } + fn timeline (&self) -> impl Content + '_ { + Fill::X(Fixed::Y(1, Thunk::new(move|to: &mut TuiOut|{ + let [x, y, w, _h] = to.area(); + let style = Some(Style::default().dim()); + let length = self.clip.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); + for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) { + let t = area_x as usize * self.time_zoom().get(); + if t < length { + to.blit(&"|", screen_x, y, style); + } + } + }))) + } +} + +impl TimeRange for PianoHorizontal { + fn time_len (&self) -> &AtomicUsize { self.range.time_len() } + fn time_zoom (&self) -> &AtomicUsize { self.range.time_zoom() } + fn time_lock (&self) -> &AtomicBool { self.range.time_lock() } + fn time_start (&self) -> &AtomicUsize { self.range.time_start() } + fn time_axis (&self) -> &AtomicUsize { self.range.time_axis() } +} + +impl NoteRange for PianoHorizontal { + fn note_lo (&self) -> &AtomicUsize { self.range.note_lo() } + fn note_axis (&self) -> &AtomicUsize { self.range.note_axis() } +} + +impl NotePoint for PianoHorizontal { + fn note_len (&self) -> &AtomicUsize { self.point.note_len() } + fn note_pos (&self) -> &AtomicUsize { self.point.note_pos() } +} + +impl TimePoint for PianoHorizontal { + fn time_pos (&self) -> &AtomicUsize { self.point.time_pos() } +} + +impl MidiViewer for PianoHorizontal { + fn clip (&self) -> &Option>> { &self.clip } + fn clip_mut (&mut self) -> &mut Option>> { &mut self.clip } + /// Determine the required space to render the clip. + fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { (clip.length / self.range.time_zoom().get(), 128) } + fn redraw (&self) { + *self.buffer.write().unwrap() = if let Some(clip) = self.clip.as_ref() { + let clip = clip.read().unwrap(); + let buf_size = self.buffer_size(&clip); + let mut buffer = BigBuffer::from(buf_size); + let time_zoom = self.get_time_zoom(); + self.time_len().set(clip.length); + PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom,self.get_note_len(), self.get_note_pos(), self.get_time_pos()); + PianoHorizontal::draw_fg(&mut buffer, &clip, time_zoom); + buffer + } else { + Default::default() + } + } + fn set_clip (&mut self, clip: Option<&Arc>>) { + *self.clip_mut() = clip.cloned(); + self.color = clip.map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]); + self.redraw(); + } +} + +impl std::fmt::Debug for PianoHorizontal { + fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + let buffer = self.buffer.read().unwrap(); + f.debug_struct("PianoHorizontal") + .field("time_zoom", &self.range.time_zoom) + .field("buffer", &format!("{}x{}", buffer.width, buffer.height)) + .finish() + } +} + +fn to_key (note: usize) -> &'static str { + match note % 12 { + 11 | 9 | 7 | 5 | 4 | 2 | 0 => "โ–ˆโ–ˆโ–ˆโ–ˆโ–Œ", + 10 | 8 | 6 | 3 | 1 => " ", + _ => unreachable!(), + } +} + +pub struct OctaveVertical { on: [bool; 12], colors: [Color; 3] } +impl Default for OctaveVertical { + fn default () -> Self { + Self { on: [false; 12], colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)] } + } +} +impl OctaveVertical { + fn color (&self, pitch: usize) -> Color { + let pitch = pitch % 12; + self.colors[if self.on[pitch] { 2 } else { match pitch { 0 | 2 | 4 | 5 | 6 | 8 | 10 => 0, _ => 1 } }] + } +} +impl HasContent for OctaveVertical { + fn content (&self) -> impl Content + '_ { + row!( + Tui::fg_bg(self.color(0), self.color(1), "โ–™"), + Tui::fg_bg(self.color(2), self.color(3), "โ–™"), + Tui::fg_bg(self.color(4), self.color(5), "โ–Œ"), + Tui::fg_bg(self.color(6), self.color(7), "โ–Ÿ"), + Tui::fg_bg(self.color(8), self.color(9), "โ–Ÿ"), + Tui::fg_bg(self.color(10), self.color(11), "โ–Ÿ"), + ) + } +} + // Update sequencer playhead indicator + //self.now().set(0.); + //if let Some((ref started_at, Some(ref playing))) = self.sequencer.play_clip { + //let clip = clip.read().unwrap(); + //if *playing.read().unwrap() == *clip { + //let pulse = self.current().pulse.get(); + //let start = started_at.pulse.get(); + //let now = (pulse - start) % clip.length as f64; + //self.now().set(now); + //} + //} diff --git a/device/lv2.rs b/device/lv2.rs new file mode 100644 index 00000000..2330ce38 --- /dev/null +++ b/device/lv2.rs @@ -0,0 +1,310 @@ +use crate::*; + +/// A LV2 plugin. +#[derive(Debug)] +pub struct Lv2 { + /// JACK client handle (needs to not be dropped for standalone mode to work). + pub jack: Jack<'static>, + pub name: Arc, + pub path: Option>, + pub selected: usize, + pub mapping: bool, + pub midi_ins: Vec>, + pub midi_outs: Vec>, + pub audio_ins: Vec>, + pub audio_outs: Vec>, + + pub lv2_world: livi::World, + pub lv2_instance: livi::Instance, + pub lv2_plugin: livi::Plugin, + pub lv2_features: Arc, + pub lv2_port_list: Vec, + pub lv2_input_buffer: Vec, + pub lv2_ui_thread: Option>, +} + +impl Lv2 { + + pub fn new ( + jack: &Jack<'static>, + name: &str, + uri: &str, + ) -> Usually { + let lv2_world = livi::World::with_load_bundle(&uri); + let lv2_features = lv2_world.build_features(livi::FeaturesBuilder { + min_block_length: 1, + max_block_length: 65536, + }); + let lv2_plugin = lv2_world.iter_plugins().nth(0) + .unwrap_or_else(||panic!("plugin not found: {uri}")); + Ok(Self { + jack: jack.clone(), + name: name.into(), + path: Some(String::from(uri).into()), + selected: 0, + mapping: false, + midi_ins: vec![], + midi_outs: vec![], + audio_ins: vec![], + audio_outs: vec![], + lv2_instance: unsafe { + lv2_plugin + .instantiate(lv2_features.clone(), 48000.0) + .expect(&format!("instantiate failed: {uri}")) + }, + lv2_port_list: lv2_plugin.ports().collect::>(), + lv2_input_buffer: Vec::with_capacity(Self::INPUT_BUFFER), + lv2_ui_thread: None, + lv2_world, + lv2_features, + lv2_plugin, + }) + } + + const INPUT_BUFFER: usize = 1024; + +} + + + //fn jack_from_lv2 (name: &str, plugin: &::livi::Plugin) -> Usually { + //let counts = plugin.port_counts(); + //let mut jack = Jack::new(name)?; + //for i in 0..counts.atom_sequence_inputs { + //jack = jack.midi_in(&format!("midi-in-{i}")) + //} + //for i in 0..counts.atom_sequence_outputs { + //jack = jack.midi_out(&format!("midi-out-{i}")); + //} + //for i in 0..counts.audio_inputs { + //jack = jack.audio_in(&format!("audio-in-{i}")); + //} + //for i in 0..counts.audio_outputs { + //jack = jack.audio_out(&format!("audio-out-{i}")); + //} + //Ok(jack) + //} + +audio!(|self: Lv2, _client, scope|{ + let Self { + midi_ins, + midi_outs, + audio_ins, + audio_outs, + lv2_features, + lv2_instance, + lv2_input_buffer, + .. + } = self; + let urid = lv2_features.midi_urid(); + lv2_input_buffer.clear(); + for port in midi_ins.iter() { + let mut atom = ::livi::event::LV2AtomSequence::new( + &lv2_features, + scope.n_frames() as usize + ); + for event in port.iter(scope) { + match event.bytes.len() { + 3 => atom.push_midi_event::<3>( + event.time as i64, + urid, + &event.bytes[0..3] + ).unwrap(), + _ => {} + } + } + lv2_input_buffer.push(atom); + } + let mut outputs = vec![]; + for _ in midi_outs.iter() { + outputs.push(::livi::event::LV2AtomSequence::new( + lv2_features, + scope.n_frames() as usize + )); + } + let ports = ::livi::EmptyPortConnections::new() + .with_atom_sequence_inputs(lv2_input_buffer.iter()) + .with_atom_sequence_outputs(outputs.iter_mut()) + .with_audio_inputs(audio_ins.iter().map(|o|o.as_slice(scope))) + .with_audio_outputs(audio_outs.iter_mut().map(|o|o.as_mut_slice(scope))); + unsafe { + lv2_instance.run(scope.n_frames() as usize, ports).unwrap() + }; + Control::Continue +}); + +impl Draw for Lv2 { + fn draw (&self, to: &mut TuiOut) { + let area = to.area(); + let [x, y, _, height] = area; + let mut width = 20u16; + let start = self.selected.saturating_sub((height as usize / 2).saturating_sub(1)); + let end = start + height as usize - 2; + //draw_box(buf, Rect { x, y, width, height }); + for i in start..end { + if let Some(port) = self.lv2_port_list.get(i) { + let value = if let Some(value) = self.lv2_instance.control_input(port.index) { + value + } else { + port.default_value + }; + //let label = &format!("Cยทยท Mยทยท {:25} = {value:.03}", port.name); + let label = &format!("{:25} = {value:.03}", port.name); + width = width.max(label.len() as u16 + 4); + let style = if i == self.selected { + Some(Style::default().green()) + } else { + None + } ; + to.blit(&label, x + 2, y + 1 + i as u16 - start as u16, style); + } else { + break + } + } + draw_header(self, to, x, y, width); + } +} + +fn draw_header (state: &Lv2, to: &mut TuiOut, x: u16, y: u16, w: u16) { + let style = Style::default().gray(); + let label1 = format!(" {}", state.name); + to.blit(&label1, x + 1, y, Some(style.white().bold())); + if let Some(ref path) = state.path { + let label2 = format!("{}โ€ฆ", &path[..((w as usize - 10).min(path.len()))]); + to.blit(&label2, x + 2 + label1.len() as u16, y, Some(style.not_dim())); + } + //Ok(Rect { x, y, width: w, height: 1 }) +} + +//handle!(TuiIn: |self:Plugin, from|{ + //match from.event() { + //kpat!(KeyCode::Up) => { + //self.selected = self.selected.saturating_sub(1); + //Ok(Some(true)) + //}, + //kpat!(KeyCode::Down) => { + //self.selected = (self.selected + 1).min(match &self.plugin { + //Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1, + //_ => unimplemented!() + //}); + //Ok(Some(true)) + //}, + //kpat!(KeyCode::PageUp) => { + //self.selected = self.selected.saturating_sub(8); + //Ok(Some(true)) + //}, + //kpat!(KeyCode::PageDown) => { + //self.selected = (self.selected + 10).min(match &self.plugin { + //Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1, + //_ => unimplemented!() + //}); + //Ok(Some(true)) + //}, + //kpat!(KeyCode::Char(',')) => { + //match self.plugin.as_mut() { + //Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => { + //let index = port_list[self.selected].index; + //if let Some(value) = instance.control_input(index) { + //instance.set_control_input(index, value - 0.01); + //} + //}, + //_ => {} + //} + //Ok(Some(true)) + //}, + //kpat!(KeyCode::Char('.')) => { + //match self.plugin.as_mut() { + //Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => { + //let index = port_list[self.selected].index; + //if let Some(value) = instance.control_input(index) { + //instance.set_control_input(index, value + 0.01); + //} + //}, + //_ => {} + //} + //Ok(Some(true)) + //}, + //kpat!(KeyCode::Char('g')) => { + //match self.plugin { + ////Some(PluginKind::LV2(ref mut plugin)) => { + ////plugin.ui_thread = Some(run_lv2_ui(LV2PluginUI::new()?)?); + ////}, + //Some(_) => unreachable!(), + //None => {} + //} + //Ok(Some(true)) + //}, + //_ => Ok(None) + //} +//}); + +//from_atom!("plugin/lv2" => |jack: &Jack, args| -> Plugin { + //let mut name = String::new(); + //let mut path = String::new(); + //atom!(atom in args { + //Atom::Map(map) => { + //if let Some(Atom::Str(n)) = map.get(&Atom::Key(":name")) { + //name = String::from(*n); + //} + //if let Some(Atom::Str(p)) = map.get(&Atom::Key(":path")) { + //path = String::from(*p); + //} + //}, + //_ => panic!("unexpected in lv2 '{name}'"), + //}); + //Plugin::new_lv2(jack, &name, &path) +//}); + +//pub struct LV2PluginUI { + //write: (), + //controller: (), + //widget: (), + //features: (), + //transfer: (), +//} + + +#[cfg(feature = "lv2_gui")] +pub fn run_lv2_ui (mut ui: LV2PluginUI) -> Usually> { + Ok(spawn(move||{ + let event_loop = EventLoop::builder().with_x11().with_any_thread(true).build().unwrap(); + event_loop.set_control_flow(ControlFlow::Wait); + event_loop.run_app(&mut ui).unwrap() + })) +} + +#[cfg(feature = "lv2_gui")] +/// A LV2 plugin's X11 UI. +pub struct LV2PluginUI { + pub window: Option +} + +#[cfg(feature = "lv2_gui")] +impl LV2PluginUI { + pub fn new () -> Usually { + Ok(Self { window: None }) + } +} + +#[cfg(feature = "lv2_gui")] +impl ApplicationHandler for LV2PluginUI { + fn resumed (&mut self, event_loop: &ActiveEventLoop) { + self.window = Some(event_loop.create_window(Window::default_attributes()).unwrap()); + } + fn window_event (&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) { + match event { + WindowEvent::CloseRequested => { + self.window.as_ref().unwrap().set_visible(false); + event_loop.exit(); + }, + WindowEvent::RedrawRequested => { + self.window.as_ref().unwrap().request_redraw(); + } + _ => (), + } + } +} + +#[cfg(feature = "lv2_gui")] +fn lv2_ui_instantiate (kind: &str) { + //let host = Suil +} diff --git a/tek/device/meter.rs b/device/meter.rs similarity index 53% rename from tek/device/meter.rs rename to device/meter.rs index 48f2c774..8a7d9a50 100644 --- a/tek/device/meter.rs +++ b/device/meter.rs @@ -9,19 +9,21 @@ pub enum MeteringMode { #[derive(Debug, Default, Clone)] pub struct Log10Meter(pub f32); -tui_layout!(|self: Log10Meter, to|to); -tui_draw!(|self: Log10Meter, to| { - let [x, y, w, h] = to.area(); - let signal = 100.0 - f32::max(0.0, f32::min(100.0, self.0.abs())); - let v = (signal * h as f32 / 100.0).ceil() as u16; - let y2 = y + h; - //to.blit(&format!("\r{v} {} {signal}", self.0), x * 20, y, None); - for y in y..(y + v) { - for x in x..(x + w) { - to.blit(&"โ–Œ", x, y2 - y, Some(Style::default().green())); +impl Layout for Log10Meter {} +impl Draw for Log10Meter { + fn draw (&self, to: &mut TuiOut) { + let [x, y, w, h] = to.area(); + let signal = 100.0 - f32::max(0.0, f32::min(100.0, self.0.abs())); + let v = (signal * h as f32 / 100.0).ceil() as u16; + let y2 = y + h; + //to.blit(&format!("\r{v} {} {signal}", self.0), x * 20, y, None); + for y in y..(y + v) { + for x in x..(x + w) { + to.blit(&"โ–Œ", x, y2 - y, Some(Style::default().green())); + } } } -}); +} pub fn to_log10 (samples: &[f32]) -> f32 { let total: f32 = samples.iter().map(|x|x.abs()).sum(); @@ -31,19 +33,21 @@ pub fn to_log10 (samples: &[f32]) -> f32 { #[derive(Debug, Default, Clone)] pub struct RmsMeter(pub f32); - -draw!(TuiOut: |self: RmsMeter, to| { - let [x, y, w, h] = to.area(); - let signal = f32::max(0.0, f32::min(100.0, self.0.abs())); - let v = (signal * h as f32).ceil() as u16; - let y2 = y + h; - //to.blit(&format!("\r{v} {} {signal}", self.0), x * 30, y, Some(Style::default())); - for y in y..(y + v) { - for x in x..(x + w) { - to.blit(&"โ–Œ", x, y2.saturating_sub(y), Some(Style::default().green())); +impl Layout for RmsMeter {} +impl Draw for RmsMeter { + fn draw (&self, to: &mut TuiOut) { + let [x, y, w, h] = to.area(); + let signal = f32::max(0.0, f32::min(100.0, self.0.abs())); + let v = (signal * h as f32).ceil() as u16; + let y2 = y + h; + //to.blit(&format!("\r{v} {} {signal}", self.0), x * 30, y, Some(Style::default())); + for y in y..(y + v) { + for x in x..(x + w) { + to.blit(&"โ–Œ", x, y2.saturating_sub(y), Some(Style::default().green())); + } } } -}); +} pub fn to_rms (samples: &[f32]) -> f32 { let sum = samples.iter() @@ -53,10 +57,10 @@ pub fn to_rms (samples: &[f32]) -> f32 { (sum / samples.len() as f32).sqrt() } -pub fn view_meter <'a> (label: &'a str, value: f32) -> impl Draw + 'a { +pub fn view_meter <'a> (label: &'a str, value: f32) -> impl Content + 'a { col!( FieldH(ItemTheme::G[128], label, format!("{:>+9.3}", value)), - Fixed::xy(if value >= 0.0 { 13 } + Fixed::XY(if value >= 0.0 { 13 } else if value >= -1.0 { 12 } else if value >= -2.0 { 11 } else if value >= -3.0 { 10 } @@ -74,7 +78,7 @@ pub fn view_meter <'a> (label: &'a str, value: f32) -> impl Draw + 'a { else { Green }, ()))) } -pub fn view_meters (values: &[f32;2]) -> impl Draw + use<'_> { +pub fn view_meters (values: &[f32;2]) -> impl Content + use<'_> { let left = format!("L/{:>+9.3}", values[0]); let right = format!("R/{:>+9.3}", values[1]); Bsp::s(left, right) diff --git a/tek/device/mixer.rs b/device/mixer.rs similarity index 97% rename from tek/device/mixer.rs rename to device/mixer.rs index d6c34432..99b2d74f 100644 --- a/tek/device/mixer.rs +++ b/device/mixer.rs @@ -1,3 +1,5 @@ +#[allow(unused)] use crate::*; + #[derive(Debug, Default)] pub enum MixingMode { #[default] diff --git a/device/pool.rs b/device/pool.rs new file mode 100644 index 00000000..42bef43c --- /dev/null +++ b/device/pool.rs @@ -0,0 +1,411 @@ +use crate::*; +#[derive(Debug)] +pub struct Pool { + pub visible: bool, + /// Selected clip + pub clip: AtomicUsize, + /// Mode switch + pub mode: Option, + /// Collection of clips + #[cfg(feature = "clip")] pub clips: Arc>>>>, + /// Embedded file browse + #[cfg(feature = "browse")] pub browse: Option, +} +impl Default for Pool { + fn default () -> Self { + //use PoolMode::*; + Self { + visible: true, + clip: 0.into(), + mode: None, + clips: Arc::from(RwLock::from(vec![])), + browse: None, + } + } +} +impl Pool { + pub fn clip_index (&self) -> usize { + self.clip.load(Relaxed) + } + pub fn set_clip_index (&self, value: usize) { + self.clip.store(value, Relaxed); + } + pub fn mode (&self) -> &Option { + &self.mode + } + pub fn mode_mut (&mut self) -> &mut Option { + &mut self.mode + } + pub fn begin_clip_length (&mut self) { + let length = self.clips()[self.clip_index()].read().unwrap().length; + *self.mode_mut() = Some(PoolMode::Length( + self.clip_index(), + length, + ClipLengthFocus::Bar + )); + } + pub fn begin_clip_rename (&mut self) { + let name = self.clips()[self.clip_index()].read().unwrap().name.clone(); + *self.mode_mut() = Some(PoolMode::Rename( + self.clip_index(), + name + )); + } + pub fn begin_import (&mut self) -> Usually<()> { + *self.mode_mut() = Some(PoolMode::Import( + self.clip_index(), + Browse::new(None)? + )); + Ok(()) + } + pub fn begin_export (&mut self) -> Usually<()> { + *self.mode_mut() = Some(PoolMode::Export( + self.clip_index(), + Browse::new(None)? + )); + Ok(()) + } + pub fn new_clip (&self) -> MidiClip { + MidiClip::new("Clip", true, 4 * PPQ, None, Some(ItemTheme::random())) + } + pub fn cloned_clip (&self) -> MidiClip { + let index = self.clip_index(); + let mut clip = self.clips()[index].read().unwrap().duplicate(); + clip.color = ItemTheme::random_near(clip.color, 0.25); + clip + } + pub fn add_new_clip (&self) -> (usize, Arc>) { + let clip = Arc::new(RwLock::new(self.new_clip())); + let index = { + let mut clips = self.clips.write().unwrap(); + clips.push(clip.clone()); + clips.len().saturating_sub(1) + }; + self.clip.store(index, Relaxed); + (index, clip) + } + pub fn delete_clip (&mut self, clip: &MidiClip) -> bool { + let index = self.clips.read().unwrap().iter().position(|x|*x.read().unwrap()==*clip); + if let Some(index) = index { + self.clips.write().unwrap().remove(index); + return true + } + false + } +} +/// Modes for clip pool +#[derive(Debug, Clone)] +pub enum PoolMode { + /// Renaming a pattern + Rename(usize, Arc), + /// Editing the length of a pattern + Length(usize, usize, ClipLengthFocus), + /// Load clip from disk + Import(usize, Browse), + /// Save clip to disk + Export(usize, Browse), +} +/// Focused field of `ClipLength` +#[derive(Copy, Clone, Debug)] +pub enum ClipLengthFocus { + /// Editing the number of bars + Bar, + /// Editing the number of beats + Beat, + /// Editing the number of ticks + Tick, +} +impl ClipLengthFocus { + pub fn next (&mut self) { + use ClipLengthFocus::*; + *self = match self { Bar => Beat, Beat => Tick, Tick => Bar, } + } + pub fn prev (&mut self) { + use ClipLengthFocus::*; + *self = match self { Bar => Tick, Beat => Bar, Tick => Beat, } + } +} +/// Displays and edits clip length. +#[derive(Clone)] +pub struct ClipLength { + /// Pulses per beat (quaver) + ppq: usize, + /// Beats per bar + bpb: usize, + /// Length of clip in pulses + pulses: usize, + /// Selected subdivision + pub focus: Option, +} +impl ClipLength { + pub fn _new (pulses: usize, focus: Option) -> Self { + Self { ppq: PPQ, bpb: 4, pulses, focus } + } + pub fn bars (&self) -> usize { + self.pulses / (self.bpb * self.ppq) + } + pub fn beats (&self) -> usize { + (self.pulses % (self.bpb * self.ppq)) / self.ppq + } + pub fn ticks (&self) -> usize { + self.pulses % self.ppq + } + pub fn bars_string (&self) -> Arc { + format!("{}", self.bars()).into() + } + pub fn beats_string (&self) -> Arc { + format!("{}", self.beats()).into() + } + pub fn ticks_string (&self) -> Arc { + format!("{:>02}", self.ticks()).into() + } +} +pub type ClipPool = Vec>>; +pub trait HasClips { + fn clips <'a> (&'a self) -> std::sync::RwLockReadGuard<'a, ClipPool>; + fn clips_mut <'a> (&'a self) -> std::sync::RwLockWriteGuard<'a, ClipPool>; + fn add_clip (&self) -> (usize, Arc>) { + let clip = Arc::new(RwLock::new(MidiClip::new("Clip", true, 384, None, None))); + self.clips_mut().push(clip.clone()); + (self.clips().len() - 1, clip) + } +} +#[macro_export] macro_rules! has_clips { + (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? { + fn clips <'a> (&'a $self) -> std::sync::RwLockReadGuard<'a, ClipPool> { + $cb.read().unwrap() + } + fn clips_mut <'a> (&'a $self) -> std::sync::RwLockWriteGuard<'a, ClipPool> { + $cb.write().unwrap() + } + } + } +} +has_clips!(|self: Pool|self.clips); +has_clip!(|self: Pool|self.clips().get(self.clip_index()).map(|c|c.clone())); +from!(|clip:&Arc>|Pool = { + let model = Self::default(); model.clips.write().unwrap().push(clip.clone()); model.clip.store(1, Relaxed); model +}); +impl Pool { + fn _todo_usize_ (&self) -> usize { todo!() } + fn _todo_bool_ (&self) -> bool { todo!() } + fn _todo_clip_ (&self) -> MidiClip { todo!() } + fn _todo_path_ (&self) -> PathBuf { todo!() } + fn _todo_color_ (&self) -> ItemColor { todo!() } + fn _todo_str_ (&self) -> Arc { todo!() } + fn clip_new (&self) -> MidiClip { self.new_clip() } + fn clip_cloned (&self) -> MidiClip { self.cloned_clip() } + fn clip_index_current (&self) -> usize { 0 } + fn clip_index_after (&self) -> usize { 0 } + fn clip_index_previous (&self) -> usize { 0 } + fn clip_index_next (&self) -> usize { 0 } + fn color_random (&self) -> ItemColor { ItemColor::random() } +} +def_command!(PoolCommand: |pool: Pool| { + // Toggle visibility of pool + Show { visible: bool } => { pool.visible = *visible; Ok(Some(Self::Show { visible: !visible })) }, + // Select a clip from the clip pool + Select { index: usize } => { pool.set_clip_index(*index); Ok(None) }, + // Update the contents of the clip pool + Clip { command: PoolClipCommand } => Ok(command.execute(pool)?.map(|command|Self::Clip{command})), + // Rename a clip + Rename { command: RenameCommand } => Ok(command.delegate(pool, |command|Self::Rename{command})?), + // Change the length of a clip + Length { command: CropCommand } => Ok(command.delegate(pool, |command|Self::Length{command})?), + // Import from file + Import { command: BrowseCommand } => Ok(if let Some(browse) = pool.browse.as_mut() { + command.delegate(browse, |command|Self::Import{command})? + } else { + None + }), + // Export to file + Export { command: BrowseCommand } => Ok(if let Some(browse) = pool.browse.as_mut() { + command.delegate(browse, |command|Self::Export{command})? + } else { + None + }), +}); +def_command!(PoolClipCommand: |pool: Pool| { + Delete { index: usize } => { + let index = *index; + let clip = pool.clips_mut().remove(index).read().unwrap().clone(); + Ok(Some(Self::Add { index, clip })) + }, + Swap { index: usize, other: usize } => { + let index = *index; + let other = *other; + pool.clips_mut().swap(index, other); + Ok(Some(Self::Swap { index, other })) + }, + Export { index: usize, path: PathBuf } => { + todo!("export clip to midi file"); + }, + Add { index: usize, clip: MidiClip } => { + let index = *index; + let mut index = index; + let clip = Arc::new(RwLock::new(clip.clone())); + let mut clips = pool.clips_mut(); + if index >= clips.len() { + index = clips.len(); + clips.push(clip) + } else { + clips.insert(index, clip); + } + Ok(Some(Self::Delete { index })) + }, + Import { index: usize, path: PathBuf } => { + let index = *index; + let bytes = std::fs::read(&path)?; + let smf = Smf::parse(bytes.as_slice())?; + let mut t = 0u32; + let mut events = vec![]; + for track in smf.tracks.iter() { + for event in track.iter() { + t += event.delta.as_int(); + if let TrackEventKind::Midi { channel, message } = event.kind { + events.push((t, channel.as_int(), message)); + } + } + } + let mut clip = MidiClip::new("imported", true, t as usize + 1, None, None); + for event in events.iter() { + clip.notes[event.0 as usize].push(event.2); + } + Ok(Self::Add { index, clip }.execute(pool)?) + }, + SetName { index: usize, name: Arc } => { + let index = *index; + let clip = &mut pool.clips_mut()[index]; + let old_name = clip.read().unwrap().name.clone(); + clip.write().unwrap().name = name.clone(); + Ok(Some(Self::SetName { index, name: old_name })) + }, + SetLength { index: usize, length: usize } => { + let index = *index; + let clip = &mut pool.clips_mut()[index]; + let old_len = clip.read().unwrap().length; + clip.write().unwrap().length = *length; + Ok(Some(Self::SetLength { index, length: old_len })) + }, + SetColor { index: usize, color: ItemColor } => { + let index = *index; + let mut color = ItemTheme::from(*color); + std::mem::swap(&mut color, &mut pool.clips()[index].write().unwrap().color); + Ok(Some(Self::SetColor { index, color: color.base })) + }, +}); + +def_command!(RenameCommand: |pool: Pool| { + Begin => unreachable!(), + Cancel => { + if let Some(PoolMode::Rename(clip, ref mut old_name)) = pool.mode_mut().clone() { + pool.clips()[clip].write().unwrap().name = old_name.clone().into(); + } + Ok(None) + }, + Confirm => { + if let Some(PoolMode::Rename(_clip, ref mut old_name)) = pool.mode_mut().clone() { + let old_name = old_name.clone(); *pool.mode_mut() = None; return Ok(Some(Self::Set { value: old_name })) + } + Ok(None) + }, + Set { value: Arc } => { + if let Some(PoolMode::Rename(clip, ref mut _old_name)) = pool.mode_mut().clone() { + pool.clips()[clip].write().unwrap().name = value.clone(); + } + Ok(None) + }, +}); +def_command!(CropCommand: |pool: Pool| { + Begin => unreachable!(), + Cancel => { if let Some(PoolMode::Length(..)) = pool.mode_mut().clone() { *pool.mode_mut() = None; } Ok(None) }, + Set { length: usize } => { + if let Some(PoolMode::Length(clip, ref mut length, ref mut _focus)) + = pool.mode_mut().clone() + { + let old_length; + { + let clip = pool.clips()[clip].clone();//.write().unwrap(); + old_length = Some(clip.read().unwrap().length); + clip.write().unwrap().length = *length; + } + *pool.mode_mut() = None; + return Ok(old_length.map(|length|Self::Set { length })) + } + Ok(None) + }, + Next => { + if let Some(PoolMode::Length(_clip, ref mut _length, ref mut focus)) = pool.mode_mut().clone() { focus.next() }; Ok(None) + }, + Prev => { + if let Some(PoolMode::Length(_clip, ref mut _length, ref mut focus)) = pool.mode_mut().clone() { focus.prev() }; Ok(None) + }, + Inc => { + if let Some(PoolMode::Length(_clip, ref mut length, ref mut focus)) = pool.mode_mut().clone() { + match focus { + ClipLengthFocus::Bar => { *length += 4 * PPQ }, + ClipLengthFocus::Beat => { *length += PPQ }, + ClipLengthFocus::Tick => { *length += 1 }, + } + } + Ok(None) + }, + Dec => { + if let Some(PoolMode::Length(_clip, ref mut length, ref mut focus)) = pool.mode_mut().clone() { + match focus { + ClipLengthFocus::Bar => { *length = length.saturating_sub(4 * PPQ) }, + ClipLengthFocus::Beat => { *length = length.saturating_sub(PPQ) }, + ClipLengthFocus::Tick => { *length = length.saturating_sub(1) }, + } + } + Ok(None) + } +}); +pub struct PoolView<'a>(pub &'a Pool); +impl<'a> HasContent for PoolView<'a> { + fn content (&self) -> impl Content { + let Self(pool) = self; + //let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||Tui::g(32).into()); + //let on_bg = |x|x;//Bsp::b(Repeat(" "), Tui::bg(color.darkest.rgb, x)); + //let border = |x|x;//Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)).enclose(x); + //let height = pool.clips.read().unwrap().len() as u16; + Fixed::X(20, Fill::Y(Align::n(Map::new( + ||pool.clips().clone().into_iter(), + move|clip: Arc>, i: usize|{ + let item_height = 1; + let item_offset = i as u16 * item_height; + let selected = i == pool.clip_index(); + let MidiClip { ref name, color, length, .. } = *clip.read().unwrap(); + let bg = if selected { color.light.rgb } else { color.base.rgb }; + let fg = color.lightest.rgb; + let name = if false { format!(" {i:>3}") } else { format!(" {i:>3} {name}") }; + let length = if false { String::default() } else { format!("{length} ") }; + Fixed::Y(1, map_south(item_offset, item_height, Tui::bg(bg, lay!( + Fill::X(Align::w(Tui::fg(fg, Tui::bold(selected, name)))), + Fill::X(Align::e(Tui::fg(fg, Tui::bold(selected, length)))), + Fill::X(Align::w(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "โ–ถ"))))), + Fill::X(Align::e(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "โ—€"))))), + )))) + })))) + } +} +impl HasContent for ClipLength { + fn content (&self) -> impl Content + '_ { + use ClipLengthFocus::*; + let bars = ||self.bars_string(); + let beats = ||self.beats_string(); + let ticks = ||self.ticks_string(); + match self.focus { + None => row!(" ", bars(), ".", beats(), ".", ticks()), + Some(Bar) => row!("[", bars(), "]", beats(), ".", ticks()), + Some(Beat) => row!(" ", bars(), "[", beats(), "]", ticks()), + Some(Tick) => row!(" ", bars(), ".", beats(), "[", ticks()), + } + } +} +//take!(BrowseCommand |state: Pool, iter|Ok(state.browse.as_ref() + //.map(|p|Take::take(p, iter)) + //.transpose()? + //.flatten())); + diff --git a/device/port.rs b/device/port.rs new file mode 100644 index 00000000..0485f7c8 --- /dev/null +++ b/device/port.rs @@ -0,0 +1,577 @@ +use crate::*; + +def_sizes_iter!(InputsSizes => MidiInput); +def_sizes_iter!(OutputsSizes => MidiOutput); +def_sizes_iter!(PortsSizes => Arc, [Connect]); + +pub(crate) use ConnectName::*; +pub(crate) use ConnectScope::*; +pub(crate) use ConnectStatus::*; + +#[derive(Debug)] pub struct AudioInput { + /// Handle to JACK client, for receiving reconnect events. + jack: Jack<'static>, + /// Port name + name: Arc, + /// Port handle. + port: Port, + /// List of ports to connect to. + pub connections: Vec, +} + +#[derive(Debug)] pub struct AudioOutput { + /// Handle to JACK client, for receiving reconnect events. + jack: Jack<'static>, + /// Port name + name: Arc, + /// Port handle. + port: Port, + /// List of ports to connect to. + pub connections: Vec, +} + +#[derive(Debug)] pub struct MidiInput { + /// Handle to JACK client, for receiving reconnect events. + jack: Jack<'static>, + /// Port name + name: Arc, + /// Port handle. + port: Port, + /// List of currently held notes. + held: Arc>, + /// List of ports to connect to. + pub connections: Vec, +} + +#[derive(Debug)] pub struct MidiOutput { + /// Handle to JACK client, for receiving reconnect events. + jack: Jack<'static>, + /// Port name + name: Arc, + /// Port handle. + port: Port, + /// List of ports to connect to. + pub connections: Vec, + /// List of currently held notes. + held: Arc>, + /// Buffer + note_buffer: Vec, + /// Buffer + output_buffer: Vec>>, +} + +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) + } +} +//take!(MidiInputCommand |state: Arrangement, iter|state.selected_midi_in().as_ref() + //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); +//take!(MidiOutputCommand |state: Arrangement, iter|state.selected_midi_out().as_ref() + //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); + +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() { + //panic!("{connect:?}"); + let status = match &connect.name { + Exact(name) => self.connect_exact(name), + RegExp(re) => self.connect_regexp(re, connect.scope), + }?; + *connect.status.write().unwrap() = status; + } + 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: ConnectScope + ) -> 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 == 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 + })) + } +} + +#[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, +} + +#[derive(Clone, Debug)] pub struct Connect { + pub name: ConnectName, + pub scope: ConnectScope, + 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 = Exact(name.as_ref().into()); + Self { name, scope: One, status: Arc::new(RwLock::new(vec![])), info } + } + pub fn regexp (name: impl AsRef) -> Self { + let info = format!("~:{}", name.as_ref()).into(); + let name = RegExp(name.as_ref().into()); + Self { name, scope: 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 = RegExp(name.as_ref().into()); + Self { name, scope: All, status: Arc::new(RwLock::new(vec![])), info } + } + pub fn info (&self) -> Arc { + let status = { + let status = self.status.read().unwrap(); + let mut ok = 0; + for (_, _, state) in status.iter() { + if *state == Connected { + ok += 1 + } + } + format!("{ok}/{}", status.len()) + }; + let scope = match self.scope { + One => " ", All => "*", + }; + let name = match &self.name { + Exact(name) => format!("= {name}"), RegExp(name) => format!("~ {name}"), + }; + format!(" ({}) {} {}", status, scope, name).into() + } +} + +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 HasJack<'static> for MidiInput { fn jack (&self) -> &Jack<'static> { &self.jack } } + +impl JackPort for MidiInput { + type Port = MidiIn; + type Pair = MidiOut; + fn port_name (&self) -> &Arc { + &self.name + } + fn port (&self) -> &Port { + &self.port + } + fn port_mut (&mut self) -> &mut Port { + &mut self.port + } + fn into_port (self) -> Port { + self.port + } + fn connections (&self) -> &[Connect] { + self.connections.as_slice() + } + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized + { + let port = Self { + port: Self::register(jack, name)?, + jack: jack.clone(), + name: name.as_ref().into(), + connections: connect.to_vec(), + held: Arc::new(RwLock::new([false;128])) + }; + port.connect_to_matching()?; + Ok(port) + } +} + +impl MidiInput { + pub fn parsed <'a> (&'a self, scope: &'a ProcessScope) -> impl Iterator, &'a [u8])> { + parse_midi_input(self.port().iter(scope)) + } +} + +impl>> HasMidiIns for T { + fn midi_ins (&self) -> &Vec { + self.get() + } + fn midi_ins_mut (&mut self) -> &mut Vec { + self.get_mut() + } +} + +/// 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 + }) + } +} + +pub type CollectedMidiInput<'a> = Vec, MidiError>)>>; + +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(()) + } +} + +/// May create new MIDI input ports. +pub trait AddMidiIn { + fn midi_in_add (&mut self) -> Usually<()>; +} + +impl HasJack<'static> for MidiOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } + +impl JackPort for MidiOutput { + type Port = MidiOut; + type Pair = MidiIn; + fn port_name (&self) -> &Arc { + &self.name + } + fn port (&self) -> &Port { + &self.port + } + fn port_mut (&mut self) -> &mut Port { + &mut self.port + } + fn into_port (self) -> Port { + self.port + } + fn connections (&self) -> &[Connect] { + self.connections.as_slice() + } + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized + { + let port = Self::register(jack, name)?; + let jack = jack.clone(); + let name = name.as_ref().into(); + let connections = connect.to_vec(); + let port = Self { + jack, + port, + name, + connections, + held: Arc::new([false;128].into()), + note_buffer: vec![0;8], + output_buffer: vec![vec![];65536], + }; + port.connect_to_matching()?; + Ok(port) + } +} + +impl MidiOutput { + /// Clear the section of the output buffer that we will be using, + /// emitting "all notes off" at start of buffer if requested. + pub fn buffer_clear (&mut self, scope: &ProcessScope, reset: bool) { + let n_frames = (scope.n_frames() as usize).min(self.output_buffer.len()); + for frame in &mut self.output_buffer[0..n_frames] { + frame.clear(); + } + if reset { + all_notes_off(&mut self.output_buffer); + } + } + /// Write a note to the output buffer + pub fn buffer_write <'a> ( + &'a mut self, + sample: usize, + event: LiveEvent, + ) { + self.note_buffer.fill(0); + event.write(&mut self.note_buffer).expect("failed to serialize MIDI event"); + self.output_buffer[sample].push(self.note_buffer.clone()); + // Update the list of currently held notes. + if let LiveEvent::Midi { ref message, .. } = event { + update_keys(&mut*self.held.write().unwrap(), message); + } + } + /// Write a chunk of MIDI data from the output buffer to the output port. + pub fn buffer_emit (&mut self, scope: &ProcessScope) { + let samples = scope.n_frames() as usize; + let mut writer = self.port.writer(scope); + for (time, events) in self.output_buffer.iter().enumerate().take(samples) { + for bytes in events.iter() { + writer.write(&RawMidi { time: time as u32, bytes }).unwrap_or_else(|_|{ + panic!("Failed to write MIDI data: {bytes:?}"); + }); + } + } + } +} + + +impl>> HasMidiOuts for T { + fn midi_outs (&self) -> &Vec { + self.get() + } + fn midi_outs_mut (&mut self) -> &mut Vec { + self.get_mut() + } +} + + +/// 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) + } + } +} + +/// 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 output ports. +pub trait AddMidiOut { + fn midi_out_add (&mut self) -> Usually<()>; +} + +impl HasJack<'static> for AudioInput { fn jack (&self) -> &Jack<'static> { &self.jack } } + +impl JackPort for AudioInput { + type Port = AudioIn; + type Pair = AudioOut; + fn port_name (&self) -> &Arc { + &self.name + } + fn port (&self) -> &Port { + &self.port + } + fn port_mut (&mut self) -> &mut Port { + &mut self.port + } + fn into_port (self) -> Port { + self.port + } + fn connections (&self) -> &[Connect] { + self.connections.as_slice() + } + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized + { + let port = Self { + port: Self::register(jack, name)?, + jack: jack.clone(), + name: name.as_ref().into(), + connections: connect.to_vec() + }; + port.connect_to_matching()?; + Ok(port) + } +} + +impl HasJack<'static> for AudioOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } + +impl JackPort for AudioOutput { + type Port = AudioOut; + type Pair = AudioIn; + fn port_name (&self) -> &Arc { + &self.name + } + fn port (&self) -> &Port { + &self.port + } + fn port_mut (&mut self) -> &mut Port { + &mut self.port + } + fn into_port (self) -> Port { + self.port + } + fn connections (&self) -> &[Connect] { + self.connections.as_slice() + } + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized + { + let port = Self { + port: Self::register(jack, name)?, + jack: jack.clone(), + name: name.as_ref().into(), + connections: connect.to_vec() + }; + port.connect_to_matching()?; + Ok(port) + } +} diff --git a/device/sampler.rs b/device/sampler.rs new file mode 100644 index 00000000..77aea1b2 --- /dev/null +++ b/device/sampler.rs @@ -0,0 +1,1116 @@ +//! ``` +//! let sample = Sample::new("test", 0, 0, vec![]); +//! ``` + +use crate::*; + +/// The sampler device plays sounds in response to MIDI notes. +#[derive(Debug)] +pub struct Sampler { + /// Name of sampler. + pub name: Arc, + /// Device color. + pub color: ItemTheme, + /// Audio input ports. Samples get recorded here. + #[cfg(feature = "port")] pub audio_ins: Vec, + /// Audio input meters. + #[cfg(feature = "meter")] pub input_meters: Vec, + /// Sample currently being recorded. + pub recording: Option<(usize, Option>>)>, + /// Recording buffer. + pub buffer: Vec>, + /// Samples mapped to MIDI notes. + pub mapped: [Option>>;128], + /// Samples that are not mapped to MIDI notes. + pub unmapped: Vec>>, + /// Sample currently being edited. + pub editing: Option>>, + /// MIDI input port. Triggers sample playback. + #[cfg(feature = "port")] pub midi_in: MidiInput, + /// Collection of currently playing instances of samples. + pub voices: Arc>>, + /// Audio output ports. Voices get played here. + #[cfg(feature = "port")] pub audio_outs: Vec, + /// Audio output meters. + #[cfg(feature = "meter")] pub output_meters: Vec, + /// How to mix the voices. + pub mixing_mode: MixingMode, + /// How to meter the inputs and outputs. + pub metering_mode: MeteringMode, + /// Fixed gain applied to all output. + pub output_gain: f32, + /// Currently active modal, if any. + pub mode: Option, + /// Size of rendered sampler. + pub size: Measure, + /// Lowest note displayed. + pub note_lo: AtomicUsize, + /// Currently selected note. + pub note_pt: AtomicUsize, + /// Selected note as row/col. + pub cursor: (AtomicUsize, AtomicUsize), +} + +impl Sampler { + pub fn new ( + jack: &Jack<'static>, + name: impl AsRef, + #[cfg(feature = "port")] midi_from: &[Connect], + #[cfg(feature = "port")] audio_from: &[&[Connect];2], + #[cfg(feature = "port")] audio_to: &[&[Connect];2], + ) -> Usually { + let name = name.as_ref(); + Ok(Self { + name: name.into(), + #[cfg(feature = "port")] midi_in: MidiInput::new(jack, &format!("M/{name}"), midi_from)?, + #[cfg(feature = "port")] audio_ins: vec![ + AudioInput::new(jack, &format!("L/{name}"), audio_from[0])?, + AudioInput::new(jack, &format!("R/{name}"), audio_from[1])?, + ], + #[cfg(feature = "port")] audio_outs: vec![ + AudioOutput::new(jack, &format!("{name}/L"), audio_to[0])?, + AudioOutput::new(jack, &format!("{name}/R"), audio_to[1])?, + ], + input_meters: vec![0.0;2], + output_meters: vec![0.0;2], + mapped: [const { None };128], + unmapped: vec![], + voices: Arc::new(RwLock::new(vec![])), + buffer: vec![vec![0.0;16384];2], + output_gain: 1., + recording: None, + mode: None, + editing: None, + size: Default::default(), + note_lo: 0.into(), + note_pt: 0.into(), + cursor: (0.into(), 0.into()), + color: Default::default(), + mixing_mode: Default::default(), + metering_mode: Default::default(), + }) + } + /// Value of cursor + pub fn cursor (&self) -> (usize, usize) { + (self.cursor.0.load(Relaxed), self.cursor.1.load(Relaxed)) + } +} + +impl NoteRange for Sampler { + fn note_lo (&self) -> &AtomicUsize { + &self.note_lo + } + fn note_axis (&self) -> &AtomicUsize { + &self.size.y + } +} + +impl NotePoint for Sampler { + fn note_len (&self) -> &AtomicUsize { + unreachable!(); + } + fn get_note_len (&self) -> usize { + 0 + } + fn set_note_len (&self, x: usize) -> usize { + 0 /*TODO?*/ + } + fn note_pos (&self) -> &AtomicUsize { + &self.note_pt + } + fn get_note_pos (&self) -> usize { + self.note_pt.load(Relaxed) + } + fn set_note_pos (&self, x: usize) -> usize { + let old = self.note_pt.swap(x, Relaxed); + self.cursor.0.store(x % 8, Relaxed); + self.cursor.1.store(x / 8, Relaxed); + old + } +} + +/// A sound sample. +#[derive(Default, Debug)] +pub struct Sample { + pub name: Arc, + pub start: usize, + pub end: usize, + pub channels: Vec>, + pub rate: Option, + pub gain: f32, + pub color: ItemTheme, +} + +impl Sample { + pub fn new (name: impl AsRef, start: usize, end: usize, channels: Vec>) -> Self { + Self { + name: name.as_ref().into(), + start, + end, + channels, + rate: None, + gain: 1.0, + color: ItemTheme::random(), + } + } + pub fn play (sample: &Arc>, after: usize, velocity: &u7) -> Voice { + Voice { + sample: sample.clone(), + after, + position: sample.read().unwrap().start, + velocity: velocity.as_int() as f32 / 127.0, + } + } +} + +/// A currently playing instance of a sample. +#[derive(Default, Debug, Clone)] +pub struct Voice { + pub sample: Arc>, + pub after: usize, + pub position: usize, + pub velocity: f32, +} + +#[derive(Debug)] +pub enum SamplerMode { + // Load sample from path + Import(usize, Browse), +} + +impl Sample { + + /// Read WAV from file + pub fn read_data (src: &str) -> Usually<(usize, Vec>)> { + let mut channels: Vec> = vec![]; + for channel in wavers::Wav::from_path(src)?.channels() { + channels.push(channel); + } + let mut end = 0; + let mut data: Vec> = vec![]; + for samples in channels.iter() { + let channel = Vec::from(samples.as_ref()); + end = end.max(channel.len()); + data.push(channel); + } + Ok((end, data)) + } + + pub fn from_file (path: &PathBuf) -> Usually { + let name = path.file_name().unwrap().to_string_lossy().into(); + let mut sample = Self { name, ..Default::default() }; + // Use file extension if present + let mut hint = Hint::new(); + if let Some(ext) = path.extension() { + hint.with_extension(&ext.to_string_lossy()); + } + let probed = symphonia::default::get_probe().format( + &hint, + MediaSourceStream::new( + Box::new(File::open(path)?), + Default::default(), + ), + &Default::default(), + &Default::default() + )?; + let mut format = probed.format; + let params = &format.tracks().iter() + .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) + .expect("no tracks found") + .codec_params; + let mut decoder = get_codecs().make(params, &Default::default())?; + loop { + match format.next_packet() { + Ok(packet) => sample.decode_packet(&mut decoder, packet)?, + Err(symphonia::core::errors::Error::IoError(_)) => break decoder.last_decoded(), + Err(err) => return Err(err.into()), + }; + }; + sample.end = sample.channels.iter().fold(0, |l, c|l + c.len()); + Ok(sample) + } + + fn decode_packet ( + &mut self, decoder: &mut Box, packet: Packet + ) -> Usually<()> { + // Decode a packet + let decoded = decoder + .decode(&packet) + .map_err(|e|Box::::from(e))?; + // Determine sample rate + let spec = *decoded.spec(); + if let Some(rate) = self.rate { + if rate != spec.rate as usize { + panic!("sample rate changed"); + } + } else { + self.rate = Some(spec.rate as usize); + } + // Determine channel count + while self.channels.len() < spec.channels.count() { + self.channels.push(vec![]); + } + // Load sample + let mut samples = SampleBuffer::new( + decoded.frames() as u64, + spec + ); + if samples.capacity() > 0 { + samples.copy_interleaved_ref(decoded); + for frame in samples.samples().chunks(spec.channels.count()) { + for (chan, frame) in frame.iter().enumerate() { + self.channels[chan].push(*frame) + } + } + } + Ok(()) + } + +} + +pub type MidiSample = (Option, Arc>); + +impl Sampler { + + /// Create [Voice]s from [Sample]s in response to MIDI input. + pub fn process_midi_in (&mut self, scope: &ProcessScope) { + let Sampler { midi_in, mapped, voices, .. } = self; + for RawMidi { time, bytes } in midi_in.port().iter(scope) { + if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() { + match message { + MidiMessage::NoteOn { ref key, ref vel } => { + if let Some(ref sample) = mapped[key.as_int() as usize] { + voices.write().unwrap().push(Sample::play(sample, time as usize, vel)); + } + }, + MidiMessage::Controller { controller: _, value: _ } => { + // TODO + } + _ => {} + } + } + } + } + +} + +impl Sample { + pub fn handle_cc (&mut self, controller: u7, value: u7) { + let percentage = value.as_int() as f64 / 127.; + match controller.as_int() { + 20 => { + self.start = (percentage * self.end as f64) as usize; + }, + 21 => { + let length = self.channels[0].len(); + self.end = length.min( + self.start + (percentage * (length as f64 - self.start as f64)) as usize + ); + }, + 22 => { /*attack*/ }, + 23 => { /*decay*/ }, + 24 => { + self.gain = percentage as f32 * 2.0; + }, + 26 => { /* pan */ } + 25 => { /* pitch */ } + _ => {} + } + } +} + +// TODO: +//for port in midi_in.iter() { + //for event in port.iter() { + //match event { + //(time, Ok(LiveEvent::Midi {message, ..})) => match message { + //MidiMessage::NoteOn {ref key, ..} if let Some(editor) = self.editor.as_ref() => { + //editor.set_note_pos(key.as_int() as usize); + //}, + //MidiMessage::Controller {controller, value} if let (Some(editor), Some(sampler)) = ( + //self.editor.as_ref(), + //self.sampler.as_ref(), + //) => { + //// TODO: give sampler its own cursor + //if let Some(sample) = &sampler.mapped[editor.note_pos()] { + //sample.write().unwrap().handle_cc(*controller, *value) + //} + //} + //_ =>{} + //}, + //_ =>{} + //} + //} +//} + +audio!(|self: Sampler, _client, scope|{ + self.process_midi_in(scope); + self.process_audio_out(scope); + self.process_audio_in(scope); + Control::Continue +}); + +impl Sampler { + + pub fn process_audio_in (&mut self, scope: &ProcessScope) { + self.reset_input_meters(); + if self.recording.is_some() { + self.record_into(scope); + } else { + self.update_input_meters(scope); + } + } + + /// Make sure that input meter count corresponds to input channel count + fn reset_input_meters (&mut self) { + let channels = self.audio_ins.len(); + if self.input_meters.len() != channels { + self.input_meters = vec![f32::MIN;channels]; + } + } + + /// Record from inputs to sample + fn record_into (&mut self, scope: &ProcessScope) { + if let Some(ref sample) = self.recording.as_ref().expect("no recording sample").1 { + let mut sample = sample.write().unwrap(); + if sample.channels.len() != self.audio_ins.len() { + panic!("channel count mismatch"); + } + let samples_with_meters = self.audio_ins.iter() + .zip(self.input_meters.iter_mut()) + .zip(sample.channels.iter_mut()); + let mut length = 0; + for ((input, meter), channel) in samples_with_meters { + let slice = input.port().as_slice(scope); + length = length.max(slice.len()); + *meter = to_rms(slice); + channel.extend_from_slice(slice); + } + sample.end += length; + } else { + panic!("tried to record into the void") + } + } + + /// Update input meters + fn update_input_meters (&mut self, scope: &ProcessScope) { + for (input, meter) in self.audio_ins.iter().zip(self.input_meters.iter_mut()) { + let slice = input.port().as_slice(scope); + *meter = to_rms(slice); + } + } + + /// Make sure that output meter count corresponds to input channel count + fn reset_output_meters (&mut self) { + let channels = self.audio_outs.len(); + if self.output_meters.len() != channels { + self.output_meters = vec![f32::MIN;channels]; + } + } + + /// Mix all currently playing samples into the output. + pub fn process_audio_out (&mut self, scope: &ProcessScope) { + self.clear_output_buffer(); + self.populate_output_buffer(scope.n_frames() as usize); + self.write_output_buffer(scope); + } + + /// Zero the output buffer. + fn clear_output_buffer (&mut self) { + for buffer in self.buffer.iter_mut() { + buffer.fill(0.0); + } + } + + /// Write playing voices to output buffer + fn populate_output_buffer (&mut self, frames: usize) { + let Sampler { buffer, voices, output_gain, mixing_mode, .. } = self; + let channel_count = buffer.len(); + match mixing_mode { + MixingMode::Summing => voices.write().unwrap().retain_mut(|voice|{ + mix_summing(buffer.as_mut_slice(), *output_gain, frames, ||voice.next()) + }), + MixingMode::Average => voices.write().unwrap().retain_mut(|voice|{ + mix_average(buffer.as_mut_slice(), *output_gain, frames, ||voice.next()) + }), + } + } + + /// Write output buffer to output ports. + fn write_output_buffer (&mut self, scope: &ProcessScope) { + let Sampler { audio_outs, buffer, .. } = self; + for (i, port) in audio_outs.iter_mut().enumerate() { + let buffer = &buffer[i]; + for (i, value) in port.port_mut().as_mut_slice(scope).iter_mut().enumerate() { + *value = *buffer.get(i).unwrap_or(&0.0); + } + } + } + +} + +impl Iterator for Voice { + type Item = [f32;2]; + fn next (&mut self) -> Option { + if self.after > 0 { + self.after -= 1; + return Some([0.0, 0.0]) + } + let sample = self.sample.read().unwrap(); + if self.position < sample.end { + let position = self.position; + self.position += 1; + return sample.channels[0].get(position).map(|_amplitude|[ + sample.channels[0][position] * self.velocity * sample.gain, + sample.channels[0][position] * self.velocity * sample.gain, + ]) + } + None + } +} + +def_command!(SamplerCommand: |sampler: Sampler| { + RecordToggle { slot: usize } => { + let slot = *slot; + let recording = sampler.recording.as_ref().map(|x|x.0); + let _ = Self::RecordFinish.execute(sampler)?; + // autoslice: continue recording at next slot + if recording != Some(slot) { + Self::RecordBegin { slot }.execute(sampler) + } else { + Ok(None) + } + }, + RecordBegin { slot: usize } => { + let slot = *slot; + sampler.recording = Some(( + slot, + Some(Arc::new(RwLock::new(Sample::new( + "Sample", 0, 0, vec![vec![];sampler.audio_ins.len()] + )))) + )); + Ok(None) + }, + RecordFinish => { + let _prev_sample = sampler.recording.as_mut().map(|(index, sample)|{ + std::mem::swap(sample, &mut sampler.mapped[*index]); + sample + }); // TODO: undo + Ok(None) + }, + RecordCancel => { + sampler.recording = None; + Ok(None) + }, + PlaySample { slot: usize } => { + let slot = *slot; + if let Some(ref sample) = sampler.mapped[slot] { + sampler.voices.write().unwrap().push(Sample::play(sample, 0, &u7::from(128))); + } + Ok(None) + }, + StopSample { slot: usize } => { + let slot = *slot; + todo!(); + Ok(None) + }, +}); + +def_command!(FileBrowserCommand: |sampler: Sampler|{ + //("begin" [] Some(Self::Begin)) + //("cancel" [] Some(Self::Cancel)) + //("confirm" [] Some(Self::Confirm)) + //("select" [i: usize] Some(Self::Select(i.expect("no index")))) + //("chdir" [p: PathBuf] Some(Self::Chdir(p.expect("no path")))) + //("filter" [f: Arc] Some(Self::Filter(f.expect("no filter"))))) +}); + +impl Sampler { + fn sample_selected (&self) -> usize { + (self.get_note_pos() as u8).into() + } + fn sample_selected_pitch (&self) -> u7 { + (self.get_note_pos() as u8).into() + } +} + +pub struct AddSampleModal { + exited: bool, + dir: PathBuf, + subdirs: Vec, + files: Vec, + cursor: usize, + offset: usize, + sample: Arc>, + voices: Arc>>, + _search: Option, +} + +impl AddSampleModal { + fn exited (&self) -> bool { + self.exited + } + fn exit (&mut self) { + self.exited = true + } +} + +impl AddSampleModal { + pub fn new ( + sample: &Arc>, + voices: &Arc>> + ) -> Usually { + let dir = std::env::current_dir()?; + let (subdirs, files) = scan(&dir)?; + Ok(Self { + exited: false, + dir, + subdirs, + files, + cursor: 0, + offset: 0, + sample: sample.clone(), + voices: voices.clone(), + _search: None + }) + } + fn rescan (&mut self) -> Usually<()> { + scan(&self.dir).map(|(subdirs, files)|{ + self.subdirs = subdirs; + self.files = files; + }) + } + fn prev (&mut self) { + self.cursor = self.cursor.saturating_sub(1); + } + fn next (&mut self) { + self.cursor = self.cursor + 1; + } + fn try_preview (&mut self) -> Usually<()> { + if let Some(path) = self.cursor_file() { + if let Ok(sample) = Sample::from_file(&path) { + *self.sample.write().unwrap() = sample; + self.voices.write().unwrap().push( + Sample::play(&self.sample, 0, &u7::from(100u8)) + ); + } + //load_sample(&path)?; + //let src = std::fs::File::open(&path)?; + //let mss = MediaSourceStream::new(Box::new(src), Default::default()); + //let mut hint = Hint::new(); + //if let Some(ext) = path.extension() { + //hint.with_extension(&ext.to_string_lossy()); + //} + //let meta_opts: MetadataOptions = Default::default(); + //let fmt_opts: FormatOptions = Default::default(); + //if let Ok(mut probed) = symphonia::default::get_probe() + //.format(&hint, mss, &fmt_opts, &meta_opts) + //{ + //panic!("{:?}", probed.format.metadata()); + //}; + } + Ok(()) + } + fn cursor_dir (&self) -> Option { + if self.cursor < self.subdirs.len() { + Some(self.dir.join(&self.subdirs[self.cursor])) + } else { + None + } + } + fn cursor_file (&self) -> Option { + if self.cursor < self.subdirs.len() { + return None + } + let index = self.cursor.saturating_sub(self.subdirs.len()); + if index < self.files.len() { + Some(self.dir.join(&self.files[index])) + } else { + None + } + } + fn pick (&mut self) -> Usually { + if self.cursor == 0 { + if let Some(parent) = self.dir.parent() { + self.dir = parent.into(); + self.rescan()?; + self.cursor = 0; + return Ok(false) + } + } + if let Some(dir) = self.cursor_dir() { + self.dir = dir; + self.rescan()?; + self.cursor = 0; + return Ok(false) + } + if let Some(path) = self.cursor_file() { + let (end, channels) = read_sample_data(&path.to_string_lossy())?; + let mut sample = self.sample.write().unwrap(); + sample.name = path.file_name().unwrap().to_string_lossy().into(); + sample.end = end; + sample.channels = channels; + return Ok(true) + } + return Ok(false) + } +} + +fn read_sample_data (_: &str) -> Usually<(usize, Vec>)> { + todo!(); +} + +fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { + let (mut subdirs, mut files) = std::fs::read_dir(dir)? + .fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{ + let entry = entry.expect("failed to read drectory entry"); + let meta = entry.metadata().expect("failed to read entry metadata"); + if meta.is_file() { + files.push(entry.file_name()); + } else if meta.is_dir() { + subdirs.push(entry.file_name()); + } + (subdirs, files) + }); + subdirs.sort(); + files.sort(); + Ok((subdirs, files)) +} + +impl Draw for AddSampleModal { + fn draw (&self, _to: &mut TuiOut) { + todo!() + //let area = to.area(); + //to.make_dim(); + //let area = center_box( + //area, + //64.max(area.w().saturating_sub(8)), + //20.max(area.w().saturating_sub(8)), + //); + //to.fill_fg(area, Color::Reset); + //to.fill_bg(area, Nord::bg_lo(true, true)); + //to.fill_char(area, ' '); + //to.blit(&format!("{}", &self.dir.to_string_lossy()), area.x()+2, area.y()+1, Some(Style::default().bold()))?; + //to.blit(&"Select sample:", area.x()+2, area.y()+2, Some(Style::default().bold()))?; + //for (i, (is_dir, name)) in self.subdirs.iter() + //.map(|path|(true, path)) + //.chain(self.files.iter().map(|path|(false, path))) + //.enumerate() + //.skip(self.offset) + //{ + //if i >= area.h() as usize - 4 { + //break + //} + //let t = if is_dir { "๎—พ" } else { "๏’ฅ" }; + //let line = format!("{t} {}", name.to_string_lossy()); + //let line = &line[..line.len().min(area.w() as usize - 4)]; + //to.blit(&line, area.x() + 2, area.y() + 3 + i as u16, Some(if i == self.cursor { + //Style::default().green() + //} else { + //Style::default().white() + //}))?; + //} + //Lozenge(Style::default()).draw(to) + } +} + +impl Sampler { + + pub fn view_grid (&self) -> impl Content + use<'_> { + //let cells_x = 8u16; + //let cells_y = 8u16; + //let cell_width = 10u16; + //let cell_height = 2u16; + //let width = cells_x * cell_width; + //let height = cells_y * cell_height; + //let cols = Map::east( + //cell_width, + //move||0..cells_x, + //move|x, _|Map::south( + //cell_height, + //move||0..cells_y, + //move|y, _|self.view_grid_cell("........", x, y, cell_width, cell_height) + //) + //); + //cols + //Thunk::new(|to: &mut TuiOut|{ + //}) + "TODO" + } + + pub fn view_grid_cell <'a> ( + &'a self, name: &'a str, x: u16, y: u16, w: u16, h: u16 + ) -> impl Content + use<'a> { + let cursor = self.cursor(); + let hi_fg = Color::Rgb(64, 64, 64); + let hi_bg = if y == 0 { Color::Reset } else { Color::Rgb(64, 64, 64) /*prev*/ }; + let tx_fg = if let Some((index, _)) = self.recording + && index % 8 == x as usize + && index / 8 == y as usize + { + Color::Rgb(255, 64, 0) + } else { + Color::Rgb(255, 255, 255) + }; + let tx_bg = if x as usize == cursor.0 && y as usize == cursor.1 { + Color::Rgb(96, 96, 96) + } else { + Color::Rgb(64, 64, 64) + }; + let lo_fg = Color::Rgb(64, 64, 64); + let lo_bg = if y == 7 { Color::Reset } else { tx_bg }; + Fixed::XY(w, h, Bsp::s( + Fixed::Y(1, Tui::fg_bg(hi_fg, hi_bg, RepeatH(Phat::<()>::LO))), + Bsp::n( + Fixed::Y(1, Tui::fg_bg(lo_fg, lo_bg, RepeatH(Phat::<()>::HI))), + Fill::X(Fixed::Y(1, Tui::fg_bg(tx_fg, tx_bg, name))), + ), + )) + } + + const _EMPTY: &[(f64, f64)] = &[(0., 0.), (1., 1.), (2., 2.), (0., 2.), (2., 0.)]; + + pub fn view_list <'a, T: NotePoint + NoteRange> ( + &'a self, compact: bool, editor: &T + ) -> impl Content + 'a { + let note_lo = editor.get_note_lo(); + let note_pt = editor.get_note_pos(); + let note_hi = editor.get_note_hi(); + Fixed::X(if compact { 4 } else { 12 }, Map::south( + 1, + move||(note_lo..=note_hi).rev(), + move|note, _index| { + //let offset = |a|Push::y(i as u16, Align::n(Fixed::Y(1, Fill::X(a)))); + let mut bg = if note == note_pt { Tui::g(64) } else { Color::Reset }; + let mut fg = Tui::g(160); + if let Some(mapped) = &self.mapped[note] { + let sample = mapped.read().unwrap(); + fg = if note == note_pt { + sample.color.lightest.rgb + } else { + Tui::g(224) + }; + bg = if note == note_pt { + sample.color.light.rgb + } else { + sample.color.base.rgb + }; + } + if let Some((index, _)) = self.recording { + if note == index { + bg = if note == note_pt { Color::Rgb(96,24,0) } else { Color::Rgb(64,16,0) }; + fg = Color::Rgb(224,64,32) + } + } + Tui::fg_bg(fg, bg, format!("{note:3} {}", self.view_list_item(note, compact))) + })) + } + + pub fn view_list_item (&self, note: usize, compact: bool) -> String { + if compact { + String::default() + } else { + draw_list_item(&self.mapped[note]) + } + } + + pub fn view_sample (&self, note_pt: usize) -> impl Content + use<'_> { + Outer(true, Style::default().fg(Tui::g(96))) + .enclose(Fill::XY(draw_viewer(if let Some((_, Some(sample))) = &self.recording { + Some(sample) + } else if let Some(sample) = &self.mapped[note_pt] { + Some(sample) + } else { + None + }))) + } + + pub fn view_sample_info (&self, note_pt: usize) -> impl Content + use<'_> { + Fill::X(Fixed::Y(1, draw_info(if let Some((_, Some(sample))) = &self.recording { + Some(sample) + } else if let Some(sample) = &self.mapped[note_pt] { + Some(sample) + } else { + None + }))) + } + + pub fn view_sample_status (&self, note_pt: usize) -> impl Content + use<'_> { + Fixed::X(20, draw_info_v(if let Some((_, Some(sample))) = &self.recording { + Some(sample) + } else if let Some(sample) = &self.mapped[note_pt] { + Some(sample) + } else { + None + })) + } + + pub fn view_status (&self, index: usize) -> impl Content { + draw_status(self.mapped[index].as_ref()) + } + + pub fn view_meters_input (&self) -> impl Content + use<'_> { + draw_meters(&self.input_meters) + } + + pub fn view_meters_output (&self) -> impl Content + use<'_> { + draw_meters(&self.output_meters) + } +} + +fn draw_meters (meters: &[f32]) -> impl Content + use<'_> { + Tui::bg(Black, Fixed::X(2, Map::east(1, ||meters.iter(), |value, _index|{ + Fill::Y(RmsMeter(*value)) + }))) +} + +fn draw_list_item (sample: &Option>>) -> String { + if let Some(sample) = sample { + let sample = sample.read().unwrap(); + format!("{:8}", sample.name) + //format!("{:8} {:3} {:6}-{:6}/{:6}", + //sample.name, + //sample.gain, + //sample.start, + //sample.end, + //sample.channels[0].len() + //) + } else { + String::from("........") + } +} + +fn draw_viewer (sample: Option<&Arc>>) -> impl Content + use<'_> { + let min_db = -64.0; + Thunk::new(move|to: &mut TuiOut|{ + let [x, y, width, height] = to.area(); + let area = Rect { x, y, width, height }; + if let Some(sample) = &sample { + let sample = sample.read().unwrap(); + let start = sample.start as f64; + let end = sample.end as f64; + let length = end - start; + let step = length / width as f64; + let mut t = start; + let mut lines = vec![]; + while t < end { + let chunk = &sample.channels[0][t as usize..((t + step) as usize).min(sample.end)]; + let total: f32 = chunk.iter().map(|x|x.abs()).sum(); + let count = chunk.len() as f32; + let meter = 10. * (total / count).log10(); + let x = t as f64; + let y = meter as f64; + lines.push(Line::new(x, min_db, x, y, Color::Green)); + t += step / 2.; + } + Canvas::default() + .x_bounds([sample.start as f64, sample.end as f64]) + .y_bounds([min_db, 0.]) + .paint(|ctx| { + for line in lines.iter() { + ctx.draw(line); + } + //FIXME: proportions + //let text = "press record to finish sampling"; + //ctx.print( + //(width - text.len() as u16) as f64 / 2.0, + //height as f64 / 2.0, + //text.red() + //); + }).render(area, &mut to.buffer); + } else { + Canvas::default() + .x_bounds([0.0, width as f64]) + .y_bounds([0.0, height as f64]) + .paint(|_ctx| { + //let text = "press record to begin sampling"; + //ctx.print( + //(width - text.len() as u16) as f64 / 2.0, + //height as f64 / 2.0, + //text.red() + //); + }) + .render(area, &mut to.buffer); + } + }) +} + +fn draw_info (sample: Option<&Arc>>) -> impl Content + use<'_> { + When::new(sample.is_some(), Thunk::new(move|to: &mut TuiOut|{ + let sample = sample.unwrap().read().unwrap(); + let theme = sample.color; + to.place(&row!( + FieldH(theme, "Name", format!("{:<10}", sample.name.clone())), + FieldH(theme, "Length", format!("{:<8}", sample.channels[0].len())), + FieldH(theme, "Start", format!("{:<8}", sample.start)), + FieldH(theme, "End", format!("{:<8}", sample.end)), + FieldH(theme, "Trans", "0"), + FieldH(theme, "Gain", format!("{}", sample.gain)), + )) + })) +} + +fn draw_info_v (sample: Option<&Arc>>) -> impl Content + use<'_> { + Either::new(sample.is_some(), Thunk::new(move|to: &mut TuiOut|{ + let sample = sample.unwrap().read().unwrap(); + let theme = sample.color; + to.place(&Fixed::X(20, col!( + Fill::X(Align::w(FieldH(theme, "Name ", format!("{:<10}", sample.name.clone())))), + Fill::X(Align::w(FieldH(theme, "Length", format!("{:<8}", sample.channels[0].len())))), + Fill::X(Align::w(FieldH(theme, "Start ", format!("{:<8}", sample.start)))), + Fill::X(Align::w(FieldH(theme, "End ", format!("{:<8}", sample.end)))), + Fill::X(Align::w(FieldH(theme, "Trans ", "0"))), + Fill::X(Align::w(FieldH(theme, "Gain ", format!("{}", sample.gain)))), + ))) + }), Thunk::new(|to: &mut TuiOut|to.place(&Tui::fg(Red, col!( + Tui::bold(true, "ร— No sample."), + "[r] record", + "[Shift-F9] import", + ))))) +} + +fn draw_status (sample: Option<&Arc>>) -> impl Content { + Tui::bold(true, Tui::fg(Tui::g(224), sample + .map(|sample|{ + let sample = sample.read().unwrap(); + format!("Sample {}-{}", sample.start, sample.end) + }) + .unwrap_or_else(||"No sample".to_string()))) +} + +fn draw_sample ( + to: &mut TuiOut, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool +) -> Usually { + let style = if focus { Style::default().green() } else { Style::default() }; + if focus { + to.blit(&"๐Ÿฌด", x+1, y, Some(style.bold())); + } + let label1 = format!("{:3} {:12}", + note.map(|n|n.to_string()).unwrap_or(String::default()), + sample.name); + let label2 = format!("{:>6} {:>6} +0.0", + sample.start, + sample.end); + to.blit(&label1, x+2, y, Some(style.bold())); + to.blit(&label2, x+3+label1.len()as u16, y, Some(style)); + Ok(label1.len() + label2.len() + 4) +} + //fn file_browser_filter (&self) -> Arc { + //todo!() + //} + //fn file_browser_path (&self) -> PathBuf { + //todo!(); + //} + ///// Immutable reference to sample at cursor. + //fn sample_selected (&self) -> Option>> { + //for (i, sample) in self.mapped.iter().enumerate() { + //if i == self.cursor().0 { + //return sample.as_ref() + //} + //} + //for (i, sample) in self.unmapped.iter().enumerate() { + //if i + self.mapped.len() == self.cursor().0 { + //return Some(sample) + //} + //} + //None + //} + //fn sample_gain (&self) -> f32 { + //todo!() + //} + //fn sample_above () -> usize { + //self.note_pos().min(119) + 8 + //} + //fn sample_below () -> usize { + //self.note_pos().max(8) - 8 + //} + //fn sample_to_left () -> usize { + //self.note_pos().min(126) + 1 + //} + //fn sample_to_right () -> usize { + //self.note_pos().max(1) - 1 + //} + //fn selected_pitch () -> u7 { + //(self.note_pos() as u8).into() // TODO + //} + + //select (&self, state: &mut Sampler, i: usize) -> Option { + //Self::Select(state.set_note_pos(i)) + //} + ///// Assign sample to slot + //set (&self, slot: u7, sample: Option>>) -> Option { + //let i = slot.as_int() as usize; + //let old = self.mapped[i].clone(); + //self.mapped[i] = sample; + //Some(Self::Set(old)) + //} + //set_start (&self, state: &mut Sampler, slot: u7, frame: usize) -> Option { + //todo!() + //} + //set_gain (&self, state: &mut Sampler, slot: u7, g: f32) -> Option { + //todo!() + //} + //note_on (&self, state: &mut Sampler, slot: u7, v: u7) -> Option { + //todo!() + //} + //note_off (&self, state: &mut Sampler, slot: u7) -> Option { + //todo!() + //} + //set_sample (&self, state: &mut Sampler, slot: u7, s: Option>>) -> Option { + //Some(Self::SetSample(p, state.set_sample(p, s))) + //} + //import (&self, state: &mut Sampler, c: FileBrowserCommand) -> Option { + //match c { + //FileBrowserCommand::Begin => { + ////let voices = &state.state.voices; + ////let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); + //state.mode = Some(SamplerMode::Import(0, FileBrowser::new(None)?)); + //None + //}, + //_ => { + //println!("\n\rtodo: import: filebrowser: {c:?}"); + //None + //} + //} + //} + ////(Select [i: usize] Some(Self::Select(state.set_note_pos(i)))) + ////(RecordBegin [p: u7] cmd!(state.begin_recording(p.as_int() as usize))) + ////(RecordCancel [] cmd!(state.cancel_recording())) + ////(RecordFinish [] cmd!(state.finish_recording())) + ////(SetStart [p: u7, frame: usize] cmd_todo!("\n\rtodo: {self:?}")) + ////(SetGain [p: u7, gain: f32] cmd_todo!("\n\rtodo: {self:?}")) + ////(NoteOn [p: u7, velocity: u7] cmd_todo!("\n\rtodo: {self:?}")) + ////(NoteOff [p: u7] cmd_todo!("\n\rtodo: {self:?}")) + ////(SetSample [p: u7, s: Option>>] Some(Self::SetSample(p, state.set_sample(p, s)))) + ////(Import [c: FileBrowserCommand] match c { + ////FileBrowserCommand::Begin => { + //////let voices = &state.state.voices; + //////let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); + ////state.mode = Some(SamplerMode::Import(0, FileBrowser::new(None)?)); + ////None + ////}, + ////_ => { + ////println!("\n\rtodo: import: filebrowser: {c:?}"); + ////None + ////} + ////}))); + ////("import" [,..a] + ////FileBrowserCommand::try_from_expr(state, a).map(Self::Import)) + ////("select" [i: usize] + ////Some(Self::Select(i.expect("no index")))) + ////("record/begin" [i: u7] + ////Some(Self::RecordBegin(i.expect("no index")))) + ////("record/cancel" [] + ////Some(Self::RecordCancel)) + ////("record/finish" [] + ////Some(Self::RecordFinish)) + ////("set/sample" [i: u7, s: Option>>] + ////Some(Self::SetSample(i.expect("no index"), s.expect("no sampler")))) + ////("set/start" [i: u7, s: usize] + ////Some(Self::SetStart(i.expect("no index"), s.expect("no start")))) + ////("set/gain" [i: u7, g: f32] + ////Some(Self::SetGain(i.expect("no index"), g.expect("no gain")))) + ////("note/on" [p: u7, v: u7] + ////Some(Self::NoteOn(p.expect("no slot"), v.expect("no velocity")))) + ////("note/off" [p: u7] + ////Some(Self::NoteOff(p.expect("no slot")))))); diff --git a/tek/device/arranger/arranger_scenes.rs b/device/scene.rs similarity index 65% rename from tek/device/arranger/arranger_scenes.rs rename to device/scene.rs index d8383fa3..3db432bd 100644 --- a/tek/device/arranger/arranger_scenes.rs +++ b/device/scene.rs @@ -1,5 +1,7 @@ use crate::*; +def_sizes_iter!(ScenesSizes => Scene); + impl> + Send + Sync> HasScenes for T {} pub trait HasScenes: Has> + Send + Sync { @@ -83,6 +85,64 @@ pub trait HasScene: Has> + Send + Sync { } } +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() { + let data = (s, scene, y, y + height); + y += height; + Some(data) + } else { + None + } + }) + } + fn view_scenes_names (&self) -> impl Content { + Fixed::x(20, Thunk::new(|to: &mut TuiOut|for (index, scene, ..) in self.scenes_with_sizes() { + to.place(&self.view_scene_name(index, scene)); + })) + } + fn view_scene_name <'a> (&'a self, index: usize, scene: &'a Scene) -> impl Content + 'a { + let h = if self.selection().scene() == Some(index) && let Some(_editor) = self.editor() { + 7 + } else { + Self::H_SCENE as u16 + }; + let bg = if self.selection().scene() == Some(index) { + scene.color.light.rgb + } else { + scene.color.base.rgb + }; + let a = Fill::X(Align::w(Bsp::e(format!("ยทs{index:02} "), + Tui::fg(Tui::g(255), Tui::bold(true, &scene.name))))); + let b = When::new(self.selection().scene() == Some(index) && self.is_editing(), + Fill::XY(Align::nw(Bsp::s( + self.editor().as_ref().map(|e|e.clip_status()), + self.editor().as_ref().map(|e|e.edit_status()))))); + Fixed::XY(20, h, Tui::bg(bg, Align::nw(Bsp::s(a, b)))) + } +} + #[derive(Debug, Default)] pub struct Scene { /// Name of scene @@ -122,8 +182,10 @@ impl Scene { match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None } } } - //scene_scroll: Fill::y(Fixed::x(1, ScrollbarV { + //scene_scroll: Fill::Y(Fixed::x(1, ScrollbarV { //offset: arrangement.scene_scroll, //length: h_scenes_area as usize, //total: h_scenes as usize, //})), +//take!(SceneCommand |state: Arrangement, iter|state.selected_scene().as_ref() + //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); diff --git a/tek/device/arranger/arranger_select.rs b/device/select.rs similarity index 65% rename from tek/device/arranger/arranger_select.rs rename to device/select.rs index 665acb42..4868692b 100644 --- a/tek/device/arranger/arranger_select.rs +++ b/device/select.rs @@ -1,14 +1,9 @@ use crate::*; impl> HasSelection for T {} - pub trait HasSelection: Has { - fn selection (&self) -> &Selection { - self.get() - } - fn selection_mut (&mut self) -> &mut Selection { - self.get_mut() - } + fn selection (&self) -> &Selection { self.get() } + fn selection_mut (&mut self) -> &mut Selection { self.get_mut() } } /// Represents the current user selection in the arranger @@ -24,30 +19,27 @@ pub enum Selection { /// A MIDI output is selected. Output(usize), /// A scene is selected. - Scene(usize), + #[cfg(feature = "scene")] Scene(usize), /// A track is selected. - Track(usize), + #[cfg(feature = "track")] Track(usize), /// A clip (track ร— scene) is selected. - TrackClip { track: usize, scene: usize }, + #[cfg(feature = "track")] TrackClip { track: usize, scene: usize }, /// A track's MIDI input connection is selected. - TrackInput { track: usize, port: usize }, + #[cfg(feature = "track")] TrackInput { track: usize, port: usize }, /// A track's MIDI output connection is selected. - TrackOutput { track: usize, port: usize }, + #[cfg(feature = "track")] TrackOutput { track: usize, port: usize }, /// A track device slot is selected. - TrackDevice { track: usize, device: usize }, + #[cfg(feature = "track")] TrackDevice { track: usize, device: usize }, } -/// Focus identification methods +#[cfg(feature = "track")] impl Selection { pub fn track (&self) -> Option { use Selection::*; - match self { - Track(track) - | TrackClip { track, .. } - | TrackInput { track, .. } - | TrackOutput { track, .. } - | TrackDevice { track, .. } => Some(*track), - _ => None + if let Track(track)|TrackClip{track,..}|TrackInput{track,..}|TrackOutput{track,..}|TrackDevice{track,..} = self { + Some(*track) + } else { + None } } pub fn select_track (&self, track_count: usize) -> Self { @@ -65,16 +57,8 @@ impl Selection { match self { Mix => Track(0), Scene(s) => TrackClip { track: 0, scene: *s }, - Track(t) => if t + 1 < len { - Track(t + 1) - } else { - Mix - }, - TrackClip {track, scene} => if track + 1 < len { - TrackClip { track: track + 1, scene: *scene } - } else { - Scene(*scene) - }, + Track(t) => if t + 1 < len { Track(t + 1) } else { Mix }, + TrackClip {track, scene} => if track + 1 < len { TrackClip { track: track + 1, scene: *scene } } else { Scene(*scene) }, _ => todo!() } } @@ -90,12 +74,13 @@ impl Selection { _ => todo!() } } +} + +#[cfg(feature = "scene")] +impl Selection { pub fn scene (&self) -> Option { use Selection::*; - match self { - Scene(scene) | TrackClip { scene, .. } => Some(*scene), - _ => None - } + match self { Scene(scene) | TrackClip { scene, .. } => Some(*scene), _ => None } } pub fn select_scene (&self, scene_count: usize) -> Self { use Selection::*; @@ -111,16 +96,8 @@ impl Selection { match self { Mix => Scene(0), Track(t) => TrackClip { track: *t, scene: 0 }, - Scene(s) => if s + 1 < len { - Scene(s + 1) - } else { - Mix - }, - TrackClip { track, scene } => if scene + 1 < len { - TrackClip { track: *track, scene: scene + 1 } - } else { - Track(*track) - }, + Scene(s) => if s + 1 < len { Scene(s + 1) } else { Mix }, + TrackClip { track, scene } => if scene + 1 < len { TrackClip { track: *track, scene: scene + 1 } } else { Track(*track) }, _ => todo!() } } @@ -135,14 +112,21 @@ impl Selection { _ => todo!() } } - pub fn describe (&self, tracks: &[Track], scenes: &[Scene]) -> Arc { +} + +impl Selection { + pub fn describe ( + &self, + #[cfg(feature = "track")] tracks: &[Track], + #[cfg(feature = "scene")] scenes: &[Scene], + ) -> Arc { use Selection::*; format!("{}", match self { Mix => "Everything".to_string(), - Scene(s) => scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name)) - .unwrap_or_else(||"S??".into()), - Track(t) => tracks.get(*t).map(|track|format!("T{t}: {}", &track.name)) - .unwrap_or_else(||"T??".into()), + #[cfg(feature = "scene")] Scene(s) => + scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name)).unwrap_or_else(||"S??".into()), + #[cfg(feature = "track")] Track(t) => + tracks.get(*t).map(|track|format!("T{t}: {}", &track.name)).unwrap_or_else(||"T??".into()), TrackClip { track, scene } => match (tracks.get(*track), scenes.get(*scene)) { (Some(_), Some(s)) => match s.clip(*track) { Some(clip) => format!("T{track} S{scene} C{}", &clip.read().unwrap().name), diff --git a/device/sequencer.rs b/device/sequencer.rs new file mode 100644 index 00000000..46c93cd3 --- /dev/null +++ b/device/sequencer.rs @@ -0,0 +1,542 @@ +//! MIDI sequencer +//! ``` +//! use crate::*; +//! +//! let clip = MidiClip::default(); +//! println!("Empty clip: {clip:?}"); +//! +//! let clip = MidiClip::stop_all(); +//! println!("Panic clip: {clip:?}"); +//! +//! let mut clip = MidiClip::new("clip", true, 1, None, None); +//! clip.set_length(96); +//! clip.toggle_loop(); +//! clip.record_event(12, MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }); +//! assert!(clip.contains_note_on(36.into(), 6, 18)); +//! assert_eq!(&clip.notes, &clip.duplicate().notes); +//! +//! let clip = std::sync::Arc::new(clip); +//! assert_eq!(clip.clone(), clip); +//! +//! let sequencer = Sequencer::default(); +//! println!("{sequencer:?}"); +//! ``` + +use crate::*; + +impl> HasSequencer for T { + fn sequencer (&self) -> &Sequencer { + self.get() + } + fn sequencer_mut (&mut self) -> &mut Sequencer { + self.get_mut() + } +} + +pub trait HasSequencer { + fn sequencer (&self) -> &Sequencer; + fn sequencer_mut (&mut self) -> &mut Sequencer; +} + +/// Contains state for playing a clip +pub struct Sequencer { + /// State of clock and playhead + #[cfg(feature = "clock")] pub clock: Clock, + /// Start time and clip being played + #[cfg(feature = "clip")] pub play_clip: Option<(Moment, Option>>)>, + /// Start time and next clip + #[cfg(feature = "clip")] pub next_clip: Option<(Moment, Option>>)>, + /// Record from MIDI ports to current sequence. + #[cfg(feature = "port")] pub midi_ins: Vec, + /// Play from current sequence to MIDI ports + #[cfg(feature = "port")] pub midi_outs: Vec, + /// Play input through output. + pub monitoring: bool, + /// Write input to sequence. + pub recording: bool, + /// Overdub input to sequence. + pub overdub: bool, + /// Send all notes off + pub reset: bool, // TODO?: after Some(nframes) + /// Notes currently held at input + pub notes_in: Arc>, + /// Notes currently held at output + pub notes_out: Arc>, + /// MIDI output buffer + pub note_buf: Vec, + /// MIDI output buffer + pub midi_buf: Vec>>, +} + +impl Default for Sequencer { + fn default () -> Self { + Self { + #[cfg(feature = "clock")] clock: Clock::default(), + #[cfg(feature = "clip")] play_clip: None, + #[cfg(feature = "clip")] next_clip: None, + #[cfg(feature = "port")] midi_ins: vec![], + #[cfg(feature = "port")] midi_outs: vec![], + + recording: false, + monitoring: true, + overdub: false, + notes_in: RwLock::new([false;128]).into(), + notes_out: RwLock::new([false;128]).into(), + note_buf: vec![0;8], + midi_buf: vec![], + reset: true, + } + } +} + +impl Sequencer { + pub fn new ( + name: impl AsRef, + jack: &Jack<'static>, + #[cfg(feature = "clock")] clock: Option<&Clock>, + #[cfg(feature = "clip")] clip: Option<&Arc>>, + #[cfg(feature = "port")] midi_from: &[Connect], + #[cfg(feature = "port")] midi_to: &[Connect], + ) -> Usually { + let _name = name.as_ref(); + #[cfg(feature = "clock")] let clock = clock.cloned().unwrap_or_default(); + Ok(Self { + reset: true, + notes_in: RwLock::new([false;128]).into(), + notes_out: RwLock::new([false;128]).into(), + #[cfg(feature = "port")] midi_ins: vec![MidiInput::new(jack, &format!("M/{}", name.as_ref()), midi_from)?,], + #[cfg(feature = "port")] midi_outs: vec![MidiOutput::new(jack, &format!("{}/M", name.as_ref()), midi_to)?, ], + #[cfg(feature = "clip")] play_clip: clip.map(|clip|(Moment::zero(&clock.timebase), Some(clip.clone()))), + #[cfg(feature = "clock")] clock, + ..Default::default() + }) + } +} + +impl std::fmt::Debug for Sequencer { + fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("Sequencer") + .field("clock", &self.clock) + .field("play_clip", &self.play_clip) + .field("next_clip", &self.next_clip) + .finish() + } +} + +#[cfg(feature = "clock")] has!(Clock: |self:Sequencer|self.clock); +#[cfg(feature = "port")] has!(Vec: |self:Sequencer|self.midi_ins); +#[cfg(feature = "port")] has!(Vec: |self:Sequencer|self.midi_outs); + +impl MidiMonitor for Sequencer { + fn notes_in (&self) -> &Arc> { + &self.notes_in + } + fn monitoring (&self) -> bool { + self.monitoring + } + fn monitoring_mut (&mut self) -> &mut bool { + &mut self.monitoring + } +} + +impl MidiRecord for Sequencer { + fn recording (&self) -> bool { + self.recording + } + fn recording_mut (&mut self) -> &mut bool { + &mut self.recording + } + fn overdub (&self) -> bool { + self.overdub + } + fn overdub_mut (&mut self) -> &mut bool { + &mut self.overdub + } +} + +#[cfg(feature="clip")] impl HasPlayClip for Sequencer { + fn reset (&self) -> bool { + self.reset + } + fn reset_mut (&mut self) -> &mut bool { + &mut self.reset + } + fn play_clip (&self) -> &Option<(Moment, Option>>)> { + &self.play_clip + } + fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)> { + &mut self.play_clip + } + fn next_clip (&self) -> &Option<(Moment, Option>>)> { + &self.next_clip + } + fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)> { + &mut self.next_clip + } +} + +/// JACK process callback for a sequencer's clip sequencer/recorder. +impl Audio for Sequencer { + fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { + if self.clock().is_rolling() { + self.process_rolling(scope) + } else { + self.process_stopped(scope) + } + } +} + +impl Sequencer { + fn process_rolling (&mut self, scope: &ProcessScope) -> Control { + self.process_clear(scope, false); + // Write chunk of clip to output, handle switchover + if self.process_playback(scope) { + self.process_switchover(scope); + } + // Monitor input to output + self.process_monitoring(scope); + // Record and/or monitor input + self.process_recording(scope); + // Emit contents of MIDI buffers to JACK MIDI output ports. + self.midi_outs_emit(scope); + Control::Continue + } + fn process_stopped (&mut self, scope: &ProcessScope) -> Control { + if self.monitoring() && self.midi_ins().len() > 0 && self.midi_outs().len() > 0 { + self.process_monitoring(scope) + } + Control::Continue + } + fn process_monitoring (&mut self, scope: &ProcessScope) { + let notes_in = self.notes_in().clone(); // For highlighting keys and note repeat + let monitoring = self.monitoring(); + for input in self.midi_ins.iter() { + for (sample, event, bytes) in input.parsed(scope) { + if let LiveEvent::Midi { message, .. } = event { + if monitoring { + self.midi_buf[sample].push(bytes.to_vec()); + } + // FIXME: don't lock on every event! + update_keys(&mut notes_in.write().unwrap(), &message); + } + } + } + } + /// Clear the section of the output buffer that we will be using, + /// emitting "all notes off" at start of buffer if requested. + fn process_clear (&mut self, scope: &ProcessScope, reset: bool) { + let n_frames = (scope.n_frames() as usize).min(self.midi_buf_mut().len()); + for frame in &mut self.midi_buf_mut()[0..n_frames] { + frame.clear(); + } + if reset { + all_notes_off(self.midi_buf_mut()); + } + for port in self.midi_outs_mut().iter_mut() { + // Clear output buffer(s) + port.buffer_clear(scope, false); + } + } + fn process_recording (&mut self, scope: &ProcessScope) { + if self.monitoring() { + self.monitor(scope); + } + if let Some((started, ref clip)) = self.play_clip.clone() { + self.record_clip(scope, started, clip); + } + if let Some((_start_at, _clip)) = &self.next_clip() { + self.record_next(); + } + } + fn process_playback (&mut self, scope: &ProcessScope) -> bool { + // If a clip is playing, write a chunk of MIDI events from it to the output buffer. + // If no clip is playing, prepare for switchover immediately. + if let Some((started, clip)) = &self.play_clip { + // Length of clip, to repeat or stop on end. + let length = clip.as_ref().map_or(0, |p|p.read().unwrap().length); + // Index of first sample to populate. + let offset = self.clock().get_sample_offset(scope, &started); + // Write MIDI events from clip at sample offsets corresponding to pulses. + for (sample, pulse) in self.clock().get_pulses(scope, offset) { + // If a next clip is enqueued, and we're past the end of the current one, + // break the loop here (FIXME count pulse correctly) + let past_end = if clip.is_some() { pulse >= length } else { true }; + // Is it time for switchover? + if self.next_clip().is_some() && past_end { + return true + } + // If there's a currently playing clip, output notes from it to buffer: + if let Some(clip) = clip { + // Source clip from which the MIDI events will be taken. + let clip = clip.read().unwrap(); + // Clip with zero length is not processed + if clip.length > 0 { + // Current pulse index in source clip + let pulse = pulse % clip.length; + // Output each MIDI event from clip at appropriate frames of output buffer: + for message in clip.notes[pulse].iter() { + for port in self.midi_outs.iter_mut() { + port.buffer_write(sample, LiveEvent::Midi { + channel: 0.into(), /* TODO */ + message: *message + }); + } + } + } + } + } + false + } else { + true + } + } + /// Handle switchover from current to next playing clip. + fn process_switchover (&mut self, scope: &ProcessScope) { + let midi_buf = self.midi_buf_mut(); + let sample0 = scope.last_frame_time() as usize; + //let samples = scope.n_frames() as usize; + if let Some((start_at, clip)) = &self.next_clip() { + let start = start_at.sample.get() as usize; + let sample = self.clock().started.read().unwrap() + .as_ref().unwrap().sample.get() as usize; + // If it's time to switch to the next clip: + if start <= sample0.saturating_sub(sample) { + // Samples elapsed since clip was supposed to start + let _skipped = sample0 - start; + // Switch over to enqueued clip + let started = Moment::from_sample(self.clock().timebase(), start as f64); + // Launch enqueued clip + *self.play_clip_mut() = Some((started, clip.clone())); + // Unset enqueuement (TODO: where to implement looping?) + *self.next_clip_mut() = None; + // Fill in remaining ticks of chunk from next clip. + self.process_playback(scope); + } + } + } +} + +pub trait HasMidiBuffers { + fn note_buf_mut (&mut self) -> &mut Vec; + fn midi_buf_mut (&mut self) -> &mut Vec>>; +} + +impl HasMidiBuffers for Sequencer { + fn note_buf_mut (&mut self) -> &mut Vec { + &mut self.note_buf + } + fn midi_buf_mut (&mut self) -> &mut Vec>> { + &mut self.midi_buf + } +} + +pub trait MidiMonitor: HasMidiIns + HasMidiBuffers { + fn notes_in (&self) -> &Arc>; + fn monitoring (&self) -> bool; + fn monitoring_mut (&mut self) -> &mut bool; + fn toggle_monitor (&mut self) { + *self.monitoring_mut() = !self.monitoring(); + } + fn monitor (&mut self, scope: &ProcessScope) { + } +} + +pub trait MidiRecord: MidiMonitor + HasClock + HasPlayClip { + fn recording (&self) -> bool; + fn recording_mut (&mut self) -> &mut bool; + fn toggle_record (&mut self) { + *self.recording_mut() = !self.recording(); + } + + fn overdub (&self) -> bool; + fn overdub_mut (&mut self) -> &mut bool; + fn toggle_overdub (&mut self) { + *self.overdub_mut() = !self.overdub(); + } + + fn record_clip ( + &mut self, + scope: &ProcessScope, + started: Moment, + clip: &Option>>, + ) { + if let Some(clip) = clip { + let sample0 = scope.last_frame_time() as usize; + let start = started.sample.get() as usize; + let _recording = self.recording(); + let timebase = self.clock().timebase().clone(); + let quant = self.clock().quant.get(); + let mut clip = clip.write().unwrap(); + let length = clip.length; + for input in self.midi_ins_mut().iter() { + for (sample, event, _bytes) in parse_midi_input(input.port().iter(scope)) { + if let LiveEvent::Midi { message, .. } = event { + clip.record_event({ + let sample = (sample0 + sample - start) as f64; + let pulse = timebase.samples_to_pulse(sample); + let quantized = (pulse / quant).round() * quant; + quantized as usize % length + }, message); + } + } + } + } + } + + fn record_next (&mut self) { + // TODO switch to next clip and record into it + } +} + +pub trait MidiViewer: HasSize + MidiRange + MidiPoint + Debug + Send + Sync { + fn buffer_size (&self, clip: &MidiClip) -> (usize, usize); + fn redraw (&self); + fn clip (&self) -> &Option>>; + fn clip_mut (&mut self) -> &mut Option>>; + fn set_clip (&mut self, clip: Option<&Arc>>) { + *self.clip_mut() = clip.cloned(); + self.redraw(); + } + /// Make sure cursor is within note range + fn autoscroll (&self) { + let note_pos = self.get_note_pos().min(127); + let note_lo = self.get_note_lo(); + let note_hi = self.get_note_hi(); + if note_pos < note_lo { + self.note_lo().set(note_pos); + } else if note_pos > note_hi { + self.note_lo().set((note_lo + note_pos).saturating_sub(note_hi)); + } + } + /// Make sure time range is within display + fn autozoom (&self) { + if self.time_lock().get() { + let time_len = self.get_time_len(); + let time_axis = self.get_time_axis(); + let time_zoom = self.get_time_zoom(); + loop { + let time_zoom = self.time_zoom().get(); + let time_area = time_axis * time_zoom; + if time_area > time_len { + let next_time_zoom = NoteDuration::prev(time_zoom); + if next_time_zoom <= 1 { + break + } + let next_time_area = time_axis * next_time_zoom; + if next_time_area >= time_len { + self.time_zoom().set(next_time_zoom); + } else { + break + } + } else if time_area < time_len { + let prev_time_zoom = NoteDuration::next(time_zoom); + if prev_time_zoom > 384 { + break + } + let prev_time_area = time_axis * prev_time_zoom; + if prev_time_area <= time_len { + self.time_zoom().set(prev_time_zoom); + } else { + break + } + } + } + if time_zoom != self.time_zoom().get() { + self.redraw() + } + } + //while time_len.div_ceil(time_zoom) > time_axis { + //println!("\r{time_len} {time_zoom} {time_axis}"); + //time_zoom = Note::next(time_zoom); + //} + //self.time_zoom().set(time_zoom); + } +} + +pub trait HasPlayClip: HasClock { + + fn reset (&self) -> bool; + + fn reset_mut (&mut self) -> &mut bool; + + fn play_clip (&self) -> &Option<(Moment, Option>>)>; + + fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)>; + + fn next_clip (&self) -> &Option<(Moment, Option>>)>; + + fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)>; + + fn pulses_since_start (&self) -> Option { + if let Some((started, Some(_))) = self.play_clip().as_ref() { + let elapsed = self.clock().playhead.pulse.get() - started.pulse.get(); + return Some(elapsed) + } + None + } + + fn pulses_since_start_looped (&self) -> Option<(f64, f64)> { + if let Some((started, Some(clip))) = self.play_clip().as_ref() { + let elapsed = self.clock().playhead.pulse.get() - started.pulse.get(); + let length = clip.read().unwrap().length.max(1); // prevent div0 on empty clip + let times = (elapsed as usize / length) as f64; + let elapsed = (elapsed as usize % length) as f64; + return Some((times, elapsed)) + } + None + } + + fn enqueue_next (&mut self, clip: Option<&Arc>>) { + *self.next_clip_mut() = Some((self.clock().next_launch_instant(), clip.cloned())); + *self.reset_mut() = true; + } + + fn play_status (&self) -> impl Content { + let (name, color): (Arc, ItemTheme) = if let Some((_, Some(clip))) = self.play_clip() { + let MidiClip { ref name, color, .. } = *clip.read().unwrap(); + (name.clone(), color) + } else { + ("".into(), Tui::g(64).into()) + }; + let time: String = self.pulses_since_start_looped() + .map(|(times, time)|format!("{:>3}x {:>}", times+1.0, self.clock().timebase.format_beats_1(time))) + .unwrap_or_else(||String::from(" ")).into(); + FieldV(color, "Now:", format!("{} {}", time, name)) + } + + fn next_status (&self) -> impl Content { + let mut time: Arc = String::from("--.-.--").into(); + let mut name: Arc = String::from("").into(); + let mut color = ItemTheme::G[64]; + let clock = self.clock(); + if let Some((t, Some(clip))) = self.next_clip() { + let clip = clip.read().unwrap(); + name = clip.name.clone(); + color = clip.color.clone(); + time = { + let target = t.pulse.get(); + let current = clock.playhead.pulse.get(); + if target > current { + let remaining = target - current; + format!("-{:>}", clock.timebase.format_beats_1(remaining)) + } else { + String::new() + } + }.into() + } else if let Some((t, Some(clip))) = self.play_clip() { + let clip = clip.read().unwrap(); + if clip.looped { + name = clip.name.clone(); + color = clip.color.clone(); + let target = t.pulse.get() + clip.length as f64; + let current = clock.playhead.pulse.get(); + if target > current { + time = format!("-{:>}", clock.timebase.format_beats_0(target - current)).into() + } + } else { + name = "Stop".to_string().into(); + } + }; + FieldV(color, "Next:", format!("{} {}", time, name)) + } +} diff --git a/tek/device/sf2.rs b/device/sf2.rs similarity index 100% rename from tek/device/sf2.rs rename to device/sf2.rs diff --git a/device/track.rs b/device/track.rs new file mode 100644 index 00000000..438b84a5 --- /dev/null +++ b/device/track.rs @@ -0,0 +1,347 @@ +use crate::*; + +def_sizes_iter!(TracksSizes => Track); + +impl> + Send + Sync> HasTracks for T {} + +pub trait HasTracks: Has> + Send + Sync { + fn tracks (&self) -> &Vec { Has::>::get(self) } + fn tracks_mut (&mut self) -> &mut Vec { Has::>::get_mut(self) } + /// 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 HasTrackScroll: HasTracks { fn track_scroll (&self) -> usize; } +impl HasTrackScroll for Arrangement { fn track_scroll (&self) -> usize { self.track_scroll } } +impl> HasTrack for T { + fn track (&self) -> Option<&Track> { self.get() } + fn track_mut (&mut self) -> Option<&mut Track> { self.get_mut() } +} +impl Track { + 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!() } +} +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 }), +}); + +#[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, +} + +has!(Clock: |self: Track|self.sequencer.clock); +has!(Sequencer: |self: Track|self.sequencer); + +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() + }) + } +} +#[cfg(feature = "sampler")] +impl Track { + /// Create a new track connecting the [Sequencer] to a [Sampler]. + pub fn new_with_sampler ( + name: &impl AsRef, + color: Option, + jack: &Jack<'static>, + clock: Option<&Clock>, + clip: Option<&Arc>>, + midi_from: &[Connect], + midi_to: &[Connect], + audio_from: &[&[Connect];2], + audio_to: &[&[Connect];2], + ) -> Usually { + let mut track = Self::new(name, color, jack, clock, clip, midi_from, midi_to)?; + let client_name = jack.with_client(|c|c.name().to_string()); + let port_name = track.sequencer.midi_outs[0].port_name(); + let connect = [Connect::exact(format!("{client_name}:{}", port_name))]; + track.devices.push(Device::Sampler(Sampler::new( + jack, &format!("{}/sampler", name.as_ref()), &connect, audio_from, audio_to + )?)); + Ok(track) + } + pub fn sampler (&self, mut nth: usize) -> Option<&Sampler> { + for device in self.devices.iter() { + match device { + Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; }, + _ => {} + } + } + None + } + pub fn sampler_mut (&mut self, mut nth: usize) -> Option<&mut Sampler> { + for device in self.devices.iter_mut() { + match device { + Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; }, + _ => {} + } + } + None + } +} + +pub trait HasTrack { + fn track (&self) -> Option<&Track>; + fn track_mut (&mut self) -> Option<&mut Track>; + #[cfg(feature = "port")] fn view_midi_ins_status <'a> (&'a self, theme: ItemTheme) -> impl Content + '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 Content + '_ { + 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 Content { + 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 Content { + self.track().map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs())) + } +} + +impl ClipsView for T {} + +pub trait TracksView: + ScenesView + + HasMidiIns + + HasMidiOuts + + HasSize + + HasTrackScroll + + HasSelection + + HasEditor + + HasClipsSize +{ + fn tracks_width_available (&self) -> u16 { + (self.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.width()); + 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() { + let data = (index, track, x, x + width); + x += width + Self::TRACK_SPACING; + Some(data) + } else { + None + } + }) + } + fn view_track_names (&self, theme: ItemTheme) -> impl Content { + let track_count = self.tracks().len(); + let scene_count = self.scenes().len(); + let selected = self.selection(); + let button = Bsp::s( + 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 = Bsp::s( + button_2("T", "+", false), + button_2("S", "+", false)); + view_track_row_section(theme, button, button_2, Tui::bg(theme.darker.rgb, + Fixed::Y(2, Thunk::new(|to: &mut TuiOut|{ + for (index, track, x1, _x2) in self.tracks_with_sizes() { + to.place(&Push::X(x1 as u16, Fixed::X(track_width(index, track), + Tui::bg(if selected.track() == Some(index) { + track.color.light.rgb + } else { + track.color.base.rgb + }, Bsp::s(Fill::X(Align::nw(Bsp::e( + format!("ยทt{index:02} "), + Tui::fg(Rgb(255, 255, 255), Tui::bold(true, &track.name)) + ))), ""))) ));}})))) + } + fn view_track_outputs <'a> (&'a self, theme: ItemTheme, _h: u16) -> impl Content { + view_track_row_section(theme, + Bsp::s(Fill::X(Align::w(button_2("o", "utput", false))), + Thunk::new(|to: &mut TuiOut|for port in self.midi_outs().iter() { + to.place(&Fill::X(Align::w(port.port_name()))); + })), + button_2("O", "+", false), + Tui::bg(theme.darker.rgb, Align::w(Thunk::new(|to: &mut TuiOut|{ + for (index, track, _x1, _x2) in self.tracks_with_sizes() { + to.place(&Fixed::X(track_width(index, track), + Align::nw(Fill::Y(Map::south(1, ||track.sequencer.midi_outs.iter(), + |port, index|Tui::fg(Rgb(255, 255, 255), + Fixed::Y(1, Tui::bg(track.color.dark.rgb, Fill::X(Align::w( + format!("ยทo{index:02} {}", port.port_name())))))))))));}})))) + } + fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Content { + let mut h = 0u16; + for track in self.tracks().iter() { + h = h.max(track.sequencer.midi_ins.len() as u16); + } + let content = Thunk::new(move|to: &mut TuiOut|for (index, track, _x1, _x2) in self.tracks_with_sizes() { + to.place(&Fixed::XY(track_width(index, track), h + 1, + Align::nw(Bsp::s( + Tui::bg(track.color.base.rgb, + Fill::X(Align::w(row!( + Either::new(track.sequencer.monitoring, Tui::fg(Green, "โ—mon "), "ยทmon "), + Either::new(track.sequencer.recording, Tui::fg(Red, "โ—rec "), "ยทrec "), + Either::new(track.sequencer.overdub, Tui::fg(Yellow, "โ—dub "), "ยทdub "), + )))), + Map::south(1, ||track.sequencer.midi_ins.iter(), + |port, index|Tui::fg_bg(Rgb(255, 255, 255), track.color.dark.rgb, + Fill::X(Align::w(format!("ยทi{index:02} {}", port.port_name()))))))))); + }); + view_track_row_section(theme, button_2("i", "nput", false), button_2("I", "+", false), + Tui::bg(theme.darker.rgb, Align::w(content))) + } +} + +pub(crate) fn track_width (_index: usize, track: &Track) -> u16 { + track.width as u16 +} + +fn view_track_header (theme: ItemTheme, content: impl Content) -> impl Content { + Fixed::X(12, Tui::bg(theme.darker.rgb, Fill::X(Align::e(content)))) +} + +fn view_ports_status (theme: ItemTheme, title: &str, ports: &[T]) { + let ins = ports.len() as u16; + let frame = Outer(true, Style::default().fg(Tui::g(96))); + let iter = move||ports.iter(); + let names = Map::south(1, iter, |port, index|Fill::Y(Align::w(format!(" {index} {}", port.port_name())))); + let field = FieldV(theme, title, names); + Fixed::XY(20, 1 + ins, frame.enclose(Fixed::XY(20, 1 + ins, field.content()))) +} + +impl Track { + pub fn per <'a, T: Content + 'a, U: TracksSizes<'a>> ( + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a + ) -> impl Content + 'a { + Map::new(tracks, + move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ + let width = (x2 - x1) as u16; + map_east(x1 as u16, width, Fixed::X(width, Tui::fg_bg( + track.color.lightest.rgb, + track.color.base.rgb, + callback(index, track))))}) + } +} + +pub(crate) fn per_track_top <'a, T: Content + 'a, U: TracksSizes<'a>> ( + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a +) -> impl Content + 'a { + Align::x(Tui::bg(Reset, Map::new(tracks, + move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ + let width = (x2 - x1) as u16; + map_east(x1 as u16, width, Fixed::X(width, Tui::fg_bg( + track.color.lightest.rgb, + track.color.base.rgb, + callback(index, track))))}))) +} + +pub(crate) fn per_track <'a, T: Content + 'a, U: TracksSizes<'a>> ( + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a +) -> impl Content + 'a { + per_track_top(tracks, move|index, track|Fill::Y(Align::y(callback(index, track)))) +} + +pub(crate) fn io_ports <'a, T: PortsSizes<'a>> ( + fg: Color, bg: Color, iter: impl Fn()->T + Send + Sync + 'a +) -> impl Content + 'a { + Map::new(iter, move|( + _index, name, connections, y, y2 + ): (usize, &'a Arc, &'a [Connect], usize, usize), _| + map_south(y as u16, (y2-y) as u16, Bsp::s( + Fill::Y(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(Bsp::e(&" ๓ฐฃฒ ", name))))), + Map::new(||connections.iter(), move|connect: &'a Connect, index|map_south(index as u16, 1, + Fill::Y(Align::w(Tui::bold(false, Tui::fg_bg(fg, bg, + &connect.info))))))))) +} + +//pub(crate) fn io_conns <'a, T: PortsSizes<'a>> ( + //fg: Color, bg: Color, iter: &mut impl Iterator, &'a [Connect], usize, usize)> +//) -> impl Content + 'a { + //Fill::XY(Thunk::new(move|to: &mut TuiOut|for (_, _, connections, y, y2) in &mut *iter { + //to.place(&map_south(y as u16, (y2-y) as u16, Bsp::s( + //Fill::Y(Tui::bold(true, wrap(bg, fg, Fill::Y(Align::w(&"โ–žโ–žโ–žโ–ž โ–žโ–žโ–žโ–ž"))))), + //Thunk::new(|to: &mut TuiOut|for (index, _connection) in connections.iter().enumerate() { + //to.place(&map_south(index as u16, 1, Fill::Y(Align::w(Tui::bold(false, + //wrap(bg, fg, Fill::Y(&""))))))) + //}) + //))) + //})) +//} + //track_scroll: Fill::Y(Fixed::Y(1, ScrollbarH { + //offset: arrangement.track_scroll, + //length: h_tracks_area as usize, + //total: h_scenes as usize, + //})), +//take!(TrackCommand |state: Arrangement, iter|state.selected_track().as_ref() + //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); diff --git a/tek/device/vst2.rs b/device/vst2.rs similarity index 100% rename from tek/device/vst2.rs rename to device/vst2.rs diff --git a/tek/device/vst3.rs b/device/vst3.rs similarity index 100% rename from tek/device/vst3.rs rename to device/vst3.rs diff --git a/engine/Cargo.toml b/engine/Cargo.toml new file mode 100644 index 00000000..23988f80 --- /dev/null +++ b/engine/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "tek_engine" +edition = { workspace = true } +version = { workspace = true } + +[lib] +path = "engine.rs" + +[target.'cfg(target_os = "linux")'] +rustflags = ["-C", "link-arg=-fuse-ld=mold"] + +[dependencies] +atomic_float = { workspace = true } +jack = { workspace = true } +midly = { workspace = true } +tengri = { workspace = true } + +[dev-dependencies] +proptest = { workspace = true } +proptest-derive = { workspace = true } + +[features] diff --git a/tek/engine.rs b/engine/engine.rs similarity index 99% rename from tek/engine.rs rename to engine/engine.rs index 460b33b5..f7beba37 100644 --- a/tek/engine.rs +++ b/engine/engine.rs @@ -1,5 +1,4 @@ -use crate::*; - +mod engine_deps; pub use self::engine_deps::*; mod time; pub use self::time::*; mod note; pub use self::note::*; mod jack; pub use self::jack::*; diff --git a/engine/engine_deps.rs b/engine/engine_deps.rs new file mode 100644 index 00000000..1284b0f2 --- /dev/null +++ b/engine/engine_deps.rs @@ -0,0 +1,11 @@ +pub use ::tengri; +pub(crate) use ::{ + tengri::{Usually, from}, + atomic_float::AtomicF64, + std::{ + cmp::Ord, + fmt::Debug, + ops::{Add, Sub, Mul, Div, Rem}, + sync::{Arc, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}}, + }, +}; diff --git a/tek/engine/jack.rs b/engine/jack.rs similarity index 100% rename from tek/engine/jack.rs rename to engine/jack.rs diff --git a/tek/engine/midi.rs b/engine/midi.rs similarity index 100% rename from tek/engine/midi.rs rename to engine/midi.rs diff --git a/tek/engine/note.rs b/engine/note.rs similarity index 100% rename from tek/engine/note.rs rename to engine/note.rs diff --git a/tek/engine/note/note_pitch.rs b/engine/note/note_pitch.rs similarity index 100% rename from tek/engine/note/note_pitch.rs rename to engine/note/note_pitch.rs diff --git a/tek/engine/note/note_point.rs b/engine/note/note_point.rs similarity index 100% rename from tek/engine/note/note_point.rs rename to engine/note/note_point.rs diff --git a/tek/engine/note/note_range.rs b/engine/note/note_range.rs similarity index 100% rename from tek/engine/note/note_range.rs rename to engine/note/note_range.rs diff --git a/tek/engine/time.rs b/engine/time.rs similarity index 100% rename from tek/engine/time.rs rename to engine/time.rs diff --git a/tek/engine/time/time_moment.rs b/engine/time/time_moment.rs similarity index 100% rename from tek/engine/time/time_moment.rs rename to engine/time/time_moment.rs diff --git a/tek/engine/time/time_note.rs b/engine/time/time_note.rs similarity index 100% rename from tek/engine/time/time_note.rs rename to engine/time/time_note.rs diff --git a/tek/engine/time/time_perf.rs b/engine/time/time_perf.rs similarity index 100% rename from tek/engine/time/time_perf.rs rename to engine/time/time_perf.rs diff --git a/tek/engine/time/time_pulse.rs b/engine/time/time_pulse.rs similarity index 100% rename from tek/engine/time/time_pulse.rs rename to engine/time/time_pulse.rs diff --git a/tek/engine/time/time_sample_count.rs b/engine/time/time_sample_count.rs similarity index 100% rename from tek/engine/time/time_sample_count.rs rename to engine/time/time_sample_count.rs diff --git a/tek/engine/time/time_sample_rate.rs b/engine/time/time_sample_rate.rs similarity index 100% rename from tek/engine/time/time_sample_rate.rs rename to engine/time/time_sample_rate.rs diff --git a/tek/engine/time/time_timebase.rs b/engine/time/time_timebase.rs similarity index 100% rename from tek/engine/time/time_timebase.rs rename to engine/time/time_timebase.rs diff --git a/tek/engine/time/time_unit.rs b/engine/time/time_unit.rs similarity index 100% rename from tek/engine/time/time_unit.rs rename to engine/time/time_unit.rs diff --git a/tek/engine/time/time_usec.rs b/engine/time/time_usec.rs similarity index 100% rename from tek/engine/time/time_usec.rs rename to engine/time/time_usec.rs diff --git a/tek/config/bind.rs b/tek/config/bind.rs deleted file mode 100644 index 0795f799..00000000 --- a/tek/config/bind.rs +++ /dev/null @@ -1,106 +0,0 @@ -use super::*; - -pub type Binds = Arc, EventMap>>>>; - -/// A collection of input bindings. -#[derive(Debug)] -pub struct EventMap( - /// Map of each event (e.g. key combination) to - /// all command expressions bound to it by - /// all loaded input layers. - pub BTreeMap>> -); - -/// An input binding. -#[derive(Debug, Clone)] -pub struct Binding { - pub commands: Arc<[C]>, - pub condition: Option, - pub description: Option>, - pub source: Option>, -} - -/// Input bindings are only returned if this evaluates to true -#[derive(Clone)] -pub struct Condition(Arcbool + Send + Sync>>); - -/// Default is always empty map regardless if `E` and `C` implement [Default]. -impl Default for EventMap { - fn default () -> Self { Self(Default::default()) } -} - -impl EventMap { - /// Create a new event map - pub fn new () -> Self { - Default::default() - } - /// Add a binding to an owned event map. - pub fn def (mut self, event: E, binding: Binding) -> Self { - self.add(event, binding); - self - } - /// Add a binding to an event map. - pub fn add (&mut self, event: E, binding: Binding) -> &mut Self { - if !self.0.contains_key(&event) { - self.0.insert(event.clone(), Default::default()); - } - self.0.get_mut(&event).unwrap().push(binding); - self - } - /// Return the binding(s) that correspond to an event. - pub fn query (&self, event: &E) -> Option<&[Binding]> { - self.0.get(event).map(|x|x.as_slice()) - } - /// Return the first binding that corresponds to an event, considering conditions. - pub fn dispatch (&self, event: &E) -> Option<&Binding> { - self.query(event) - .map(|bb|bb.iter().filter(|b|b.condition.as_ref().map(|c|(c.0)()).unwrap_or(true)).next()) - .flatten() - } -} - -impl EventMap> { - pub fn load_into (binds: &Binds, name: &impl AsRef, body: &impl Dsl) -> Usually<()> { - println!("EventMap::load_into: {}: {body:?}", name.as_ref()); - let mut map = Self::new(); - body.each(|item|if item.expr().head() == Ok(Some("see")) { - // TODO - Ok(()) - } else if let Ok(Some(_word)) = item.expr().head().word() { - if let Some(key) = TuiEvent::from_dsl(item.expr()?.head()?)? { - map.add(key, Binding { - commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(), - condition: None, - description: None, - source: None - }); - Ok(()) - } else if Some(":char") == item.expr()?.head()? { - // TODO - return Ok(()) - } else { - return Err(format!("Config::load_bind: invalid key: {:?}", item.expr()?.head()?).into()) - } - } else { - return Err(format!("Config::load_bind: unexpected: {item:?}").into()) - })?; - binds.write().unwrap().insert(name.as_ref().into(), map); - Ok(()) - } -} - -impl Binding { - pub fn from_dsl (dsl: impl Dsl) -> Usually { - let command: Option = None; - let condition: Option = None; - let description: Option> = None; - let source: Option> = None; - if let Some(command) = command { - Ok(Self { commands: [command].into(), condition, description, source }) - } else { - Err(format!("no command in {dsl:?}").into()) - } - } -} - -impl_debug!(Condition |self, w| { write!(w, "*") }); diff --git a/tek/config/view.rs b/tek/config/view.rs deleted file mode 100644 index d8cb5e2f..00000000 --- a/tek/config/view.rs +++ /dev/null @@ -1,3 +0,0 @@ -use super::*; - -pub type Views = Arc, Arc>>>; diff --git a/tek/device/arranger.rs b/tek/device/arranger.rs deleted file mode 100644 index dd277a25..00000000 --- a/tek/device/arranger.rs +++ /dev/null @@ -1,198 +0,0 @@ -use crate::*; - -mod arranger_api; pub use self::arranger_api::*; -mod arranger_clip; pub use self::arranger_clip::*; -mod arranger_scenes; pub use self::arranger_scenes::*; -mod arranger_select; pub use self::arranger_select::*; -mod arranger_tracks; pub use self::arranger_tracks::*; -mod arranger_view; pub use self::arranger_view::*; - -def_sizes_iter!(ScenesSizes => Scene); -def_sizes_iter!(TracksSizes => Track); -def_sizes_iter!(InputsSizes => MidiInput); -def_sizes_iter!(OutputsSizes => MidiOutput); -def_sizes_iter!(PortsSizes => Arc, [Connect]); - -#[derive(Default, Debug)] pub struct Arrangement { - /// Project name. - pub name: Arc, - /// Base color. - pub color: ItemTheme, - /// Jack client handle - pub jack: Jack<'static>, - /// Source of time - pub clock: Clock, - /// Allows one MIDI clip to be edited - pub editor: Option, - /// List of global midi inputs - pub midi_ins: Vec, - /// List of global midi outputs - pub midi_outs: Vec, - /// List of global audio inputs - pub audio_ins: Vec, - /// List of global audio outputs - pub audio_outs: Vec, - /// Last track number (to avoid duplicate port names) - pub track_last: usize, - /// List of tracks - pub tracks: Vec, - /// Scroll offset of tracks - pub track_scroll: usize, - /// List of scenes - pub scenes: Vec, - /// Scroll offset of scenes - pub scene_scroll: usize, - /// Selected UI element - pub selection: Selection, - /// Contains a render of the project arrangement, redrawn on update. - /// TODO rename to "render_cache" or smth - pub arranger: Arc>, - /// Display size - pub size: Measure, - /// Display size of clips area - pub inner_size: Measure, -} -impl HasJack<'static> for Arrangement { - fn jack (&self) -> &Jack<'static> { - &self.jack - } -} -has!(Jack<'static>: |self: Arrangement|self.jack); -has!(Clock: |self: Arrangement|self.clock); -has!(Selection: |self: Arrangement|self.selection); -has!(Vec: |self: Arrangement|self.midi_ins); -has!(Vec: |self: Arrangement|self.midi_outs); -has!(Vec: |self: Arrangement|self.scenes); -has!(Vec: |self: Arrangement|self.tracks); -has!(Measure: |self: Arrangement|self.size); -has!(Option: |self: Arrangement|self.editor); -maybe_has!(Track: |self: Arrangement| - { Has::::get(self).track().map(|index|Has::>::get(self).get(index)).flatten() }; - { Has::::get(self).track().map(|index|Has::>::get_mut(self).get_mut(index)).flatten() }); -maybe_has!(Scene: |self: Arrangement| - { Has::::get(self).track().map(|index|Has::>::get(self).get(index)).flatten() }; - { Has::::get(self).track().map(|index|Has::>::get_mut(self).get_mut(index)).flatten() }); -//take!(MidiInputCommand |state: Arrangement, iter|state.selected_midi_in().as_ref() - //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); -//take!(MidiOutputCommand |state: Arrangement, iter|state.selected_midi_out().as_ref() - //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); -//take!(DeviceCommand|state: Arrangement, iter|state.selected_device().as_ref() - //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); -//take!(TrackCommand |state: Arrangement, iter|state.selected_track().as_ref() - //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); -//take!(SceneCommand |state: Arrangement, iter|state.selected_scene().as_ref() - //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); -//take!(ClipCommand |state: Arrangement, iter|state.selected_clip().as_ref() - //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); -impl Arrangement { - fn selected_midi_in (&self) -> Option { todo!() } - fn selected_midi_out (&self) -> Option { todo!() } - fn selected_device (&self) -> Option { todo!() } - fn selected_track (&self) -> Option { todo!() } - fn selected_scene (&self) -> Option { todo!() } - fn selected_clip (&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!() } - fn _todo_opt_item_theme_stub (&self) -> Option { todo!() } - fn select_nothing (&self) -> Selection { - Selection::Nothing - } -} -impl Arrangement { - /// 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 active track - pub fn get_track (&self) -> Option<&Track> { - let index = self.selection().track()?; - Has::>::get(self).get(index) - } - /// Get a mutable reference to the active track - pub fn get_track_mut (&mut self) -> Option<&mut Track> { - let index = self.selection().track()?; - Has::>::get_mut(self).get_mut(index) - } - /// Get the active scene - pub fn get_scene (&self) -> Option<&Scene> { - let index = self.selection().scene()?; - Has::>::get(self).get(index) - } - /// Get a mutable reference to the active scene - pub fn get_scene_mut (&mut self) -> Option<&mut Scene> { - let index = self.selection().scene()?; - Has::>::get_mut(self).get_mut(index) - } - /// Get the active clip - pub fn get_clip (&self) -> Option>> { - self.get_scene()?.clips.get(self.selection().track()?)?.clone() - } - /// Put a clip in a slot - pub fn clip_put ( - &mut self, track: usize, scene: usize, clip: Option>> - ) -> Option>> { - let old = self.scenes[scene].clips[track].clone(); - self.scenes[scene].clips[track] = clip; - old - } - /// Change the color of a clip, returning the previous one - pub fn clip_set_color (&self, track: usize, scene: usize, color: ItemTheme) - -> Option - { - self.scenes[scene].clips[track].as_ref().map(|clip|{ - let mut clip = clip.write().unwrap(); - let old = clip.color.clone(); - clip.color = color.clone(); - panic!("{color:?} {old:?}"); - old - }) - } - /// Toggle looping for the active clip - pub fn toggle_loop (&mut self) { - if let Some(clip) = self.get_clip() { - clip.write().unwrap().toggle_loop() - } - } -} - -#[cfg(feature = "sampler")] -impl Arrangement { - /// Get the first sampler of the active track - pub fn sampler (&self) -> Option<&Sampler> { - self.get_track()?.sampler(0) - } - /// Get the first sampler of the active track - pub fn sampler_mut (&mut self) -> Option<&mut Sampler> { - self.get_track_mut()?.sampler_mut(0) - } -} - -impl ScenesView for Arrangement { - fn h_scenes (&self) -> u16 { - (self.height() as u16).saturating_sub(20) - } - fn w_side (&self) -> u16 { - (self.width() as u16 * 2 / 10).max(20) - } - fn w_mid (&self) -> u16 { - (self.width() as u16).saturating_sub(2 * self.w_side()).max(40) - } -} diff --git a/tek/device/arranger/arranger_api.rs b/tek/device/arranger/arranger_api.rs deleted file mode 100644 index 75a1d91d..00000000 --- a/tek/device/arranger/arranger_api.rs +++ /dev/null @@ -1,258 +0,0 @@ -use crate::*; - -def_command!(ArrangementCommand: |arranger: Arrangement| { - - Home => { arranger.editor = None; Ok(None) }, - - Edit => { - let selection = arranger.selection().clone(); - arranger.editor = if arranger.editor.is_some() { - None - } else { - match selection { - Selection::TrackClip { track, scene } => { - let clip = &mut arranger.scenes_mut()[scene].clips[track]; - if clip.is_none() { - //app.clip_auto_create(); - *clip = Some(Arc::new(RwLock::new(MidiClip::new( - &format!("t{track:02}s{scene:02}"), - false, 384, None, Some(ItemTheme::random()) - )))); - } - clip.as_ref().map(|c|c.into()) - } - _ => { - None - } - } - }; - if let Some(editor) = arranger.editor.as_mut() { - if let Some(clip) = editor.clip() { - let length = clip.read().unwrap().length.max(1); - let width = arranger.inner_size.w().saturating_sub(20).max(1); - editor.set_time_zoom(length / width); - editor.redraw(); - } - } - Ok(None) - }, - - //// Set the selection - Select { selection: Selection } => { *arranger.selection_mut() = *selection; Ok(None) }, - - //// Launch the selected clip or scene - Launch => { - match *arranger.selection() { - Selection::Track(t) => { - arranger.tracks[t].sequencer.enqueue_next(None) - }, - Selection::TrackClip { track, scene } => { - arranger.tracks[track].sequencer.enqueue_next(arranger.scenes[scene].clips[track].as_ref()) - }, - Selection::Scene(s) => { - for t in 0..arranger.tracks.len() { - arranger.tracks[t].sequencer.enqueue_next(arranger.scenes[s].clips[t].as_ref()) - } - }, - _ => {} - }; - Ok(None) - }, - - //// Set the color of the selected entity - SetColor { palette: Option } => { - let mut palette = palette.unwrap_or_else(||ItemTheme::random()); - let selection = *arranger.selection(); - Ok(Some(Self::SetColor { palette: Some(match selection { - Selection::Mix => { - std::mem::swap(&mut palette, &mut arranger.color); - palette - }, - Selection::Scene(s) => { - std::mem::swap(&mut palette, &mut arranger.scenes[s].color); - palette - } - Selection::Track(t) => { - std::mem::swap(&mut palette, &mut arranger.tracks[t].color); - palette - } - Selection::TrackClip { track, scene } => { - if let Some(ref clip) = arranger.scenes[scene].clips[track] { - let mut clip = clip.write().unwrap(); - std::mem::swap(&mut palette, &mut clip.color); - palette - } else { - return Ok(None) - } - }, - _ => todo!() - }) })) - }, - - Track { track: TrackCommand } => { todo!("delegate") }, - - TrackAdd => { - let index = arranger.track_add(None, None, &[], &[])?.0; - *arranger.selection_mut() = match arranger.selection() { - Selection::Track(_) => Selection::Track(index), - Selection::TrackClip { track: _, scene } => Selection::TrackClip { - track: index, scene: *scene - }, - _ => *arranger.selection() - }; - Ok(Some(Self::TrackDelete { index })) - }, - - TrackSwap { index: usize, other: usize } => { - let index = *index; - let other = *other; - Ok(Some(Self::TrackSwap { index, other })) - }, - - TrackDelete { index: usize } => { - let index = *index; - let exists = arranger.tracks().get(index).is_some(); - if exists { - let track = arranger.tracks_mut().remove(index); - let Track { sequencer: Sequencer { midi_ins, midi_outs, .. }, .. } = track; - for port in midi_ins.into_iter() { - port.close()?; - } - for port in midi_outs.into_iter() { - port.close()?; - } - for scene in arranger.scenes_mut().iter_mut() { - scene.clips.remove(index); - } - } - Ok(None) - //TODO:Ok(Some(Self::TrackAdd ( index, track: Some(deleted_track) }) - }, - - MidiIn { input: MidiInputCommand } => { - todo!("delegate"); Ok(None) - }, - - MidiInAdd => { - arranger.midi_in_add()?; - Ok(None) - }, - - MidiOut { output: MidiOutputCommand } => { - todo!("delegate"); - Ok(None) - }, - - MidiOutAdd => { - arranger.midi_out_add()?; - Ok(None) - }, - - Device { command: DeviceCommand } => { - todo!("delegate"); - Ok(None) - }, - - DeviceAdd { index: usize } => { - todo!("delegate"); - Ok(None) - }, - - Scene { scene: SceneCommand } => { - todo!("delegate"); - Ok(None) - }, - - OutputAdd => { - arranger.midi_outs.push(MidiOutput::new( - arranger.jack(), - &format!("/M{}", arranger.midi_outs.len() + 1), - &[] - )?); - Ok(None) - }, - - InputAdd => { - arranger.midi_ins.push(MidiInput::new( - arranger.jack(), - &format!("M{}/", arranger.midi_ins.len() + 1), - &[] - )?); - Ok(None) - }, - - SceneAdd => { - let index = arranger.scene_add(None, None)?.0; - *arranger.selection_mut() = match arranger.selection() { - Selection::Scene(_) => Selection::Scene(index), - Selection::TrackClip { track, scene } => Selection::TrackClip { - track: *track, - scene: index - }, - _ => *arranger.selection() - }; - Ok(None) // TODO - }, - - SceneSwap { index: usize, other: usize } => { - let index = *index; - let other = *other; - Ok(Some(Self::SceneSwap { index, other })) - }, - - SceneDelete { index: usize } => { - let index = *index; - let scenes = arranger.scenes_mut(); - Ok(if scenes.get(index).is_some() { - let _scene = scenes.remove(index); - None - } else { - None - }) - }, - - SceneLaunch { index: usize } => { - let index = *index; - for track in 0..arranger.tracks.len() { - let clip = arranger.scenes[index].clips[track].as_ref(); - arranger.tracks[track].sequencer.enqueue_next(clip); - } - Ok(None) - }, - - Clip { scene: ClipCommand } => { - todo!("delegate") - }, - - ClipGet { a: usize, b: usize } => { - //(Get [a: usize, b: usize] cmd_todo!("\n\rtodo: clip: get: {a} {b}")) - //("get" [a: usize, b: usize] Some(Self::Get(a.unwrap(), b.unwrap()))) - todo!() - }, - - ClipPut { a: usize, b: usize } => { - //(Put [t: usize, s: usize, c: MaybeClip] - //Some(Self::Put(t, s, arranger.clip_put(t, s, c)))) - //("put" [a: usize, b: usize, c: MaybeClip] Some(Self::Put(a.unwrap(), b.unwrap(), c.unwrap()))) - todo!() - }, - - ClipDel { a: usize, b: usize } => { - //("delete" [a: usize, b: usize] Some(Self::Put(a.unwrap(), b.unwrap(), None)))) - todo!() - }, - - ClipEnqueue { a: usize, b: usize } => { - //(Enqueue [t: usize, s: usize] - //cmd!(arranger.tracks[t].sequencer.enqueue_next(arranger.scenes[s].clips[t].as_ref()))) - //("enqueue" [a: usize, b: usize] Some(Self::Enqueue(a.unwrap(), b.unwrap()))) - todo!() - }, - - ClipSwap { a: usize, b: usize }=> { - //(Edit [clip: MaybeClip] cmd_todo!("\n\rtodo: clip: edit: {clip:?}")) - //("edit" [a: MaybeClip] Some(Self::Edit(a.unwrap()))) - todo!() - }, - -}); diff --git a/tek/device/arranger/arranger_clip.rs b/tek/device/arranger/arranger_clip.rs deleted file mode 100644 index 8ddcc485..00000000 --- a/tek/device/arranger/arranger_clip.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::*; - -impl MidiClip { - fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } - fn _todo_bool_stub_ (&self) -> bool { todo!() } - fn _todo_usize_stub_ (&self) -> usize { todo!() } - fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } - fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } - fn _todo_opt_item_theme_stub (&self) -> Option { todo!() } -} - -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/tek/device/arranger/arranger_tracks.rs b/tek/device/arranger/arranger_tracks.rs deleted file mode 100644 index 8a293c7a..00000000 --- a/tek/device/arranger/arranger_tracks.rs +++ /dev/null @@ -1,346 +0,0 @@ -use crate::*; - -impl Arrangement { - /// Add multiple tracks - pub fn tracks_add ( - &mut self, - count: usize, width: Option, - mins: &[Connect], mouts: &[Connect], - ) -> Usually<()> { - let jack = self.jack().clone(); - 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 mut track = self.track_add(None, Some(color), mins, mouts)?.1; - if let Some(width) = width { - track.width = width; - } - } - Ok(()) - } - - /// Add a track - pub fn track_add ( - &mut self, - name: Option<&str>, color: Option, - mins: &[Connect], mouts: &[Connect], - ) -> Usually<(usize, &mut Track)> { - let name: Arc = name.map_or_else( - ||format!("trk{:02}", self.track_last).into(), - |x|x.to_string().into() - ); - self.track_last += 1; - let mut 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])) - } -} - -impl> + Send + Sync> HasTracks for T {} - -pub trait HasTracks: Has> + Send + Sync { - fn tracks (&self) -> &Vec { - Has::>::get(self) - } - fn tracks_mut (&mut self) -> &mut Vec { - Has::>::get_mut(self) - } - /// 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 HasTrackScroll: HasTracks { - fn track_scroll (&self) -> usize; -} -impl HasTrackScroll for Arrangement { - fn track_scroll (&self) -> usize { self.track_scroll } -} - -impl> HasTrack for T { - fn track (&self) -> Option<&Track> { - self.get() - } - fn track_mut (&mut self) -> Option<&mut Track> { - self.get_mut() - } -} - -impl Track { - 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!() } -} - -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 }), -}); - -#[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, -} - -has!(Clock: |self: Track|self.sequencer.clock); -has!(Sequencer: |self: Track|self.sequencer); - -impl Track { - /// Create a new track with only the default [Sequencer]. - pub fn new ( - name: &impl AsRef, - color: Option, - jack: &Jack<'static>, - clock: Option<&Clock>, - clip: Option<&Arc>>, - midi_from: &[Connect], - midi_to: &[Connect], - ) -> Usually { - Ok(Self { - name: name.as_ref().into(), - color: color.unwrap_or_default(), - sequencer: Sequencer::new( - format!("{}/sequencer", name.as_ref()), - jack, - clock, - clip, - midi_from, - midi_to - )?, - ..Default::default() - }) - } - /// Create a new track connecting the [Sequencer] to a [Sampler]. - pub fn new_with_sampler ( - name: &impl AsRef, - color: Option, - jack: &Jack<'static>, - clock: Option<&Clock>, - clip: Option<&Arc>>, - midi_from: &[Connect], - midi_to: &[Connect], - audio_from: &[&[Connect];2], - audio_to: &[&[Connect];2], - ) -> Usually { - let mut track = Self::new(name, color, jack, clock, clip, midi_from, midi_to)?; - let client_name = jack.with_client(|c|c.name().to_string()); - let port_name = track.sequencer.midi_outs[0].port_name(); - let connect = [Connect::exact(format!("{client_name}:{}", port_name))]; - track.devices.push(Device::Sampler(Sampler::new( - jack, &format!("{}/sampler", name.as_ref()), &connect, audio_from, audio_to - )?)); - Ok(track) - } - #[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 trait HasTrack { - fn track (&self) -> Option<&Track>; - fn track_mut (&mut self) -> Option<&mut Track>; - fn view_midi_ins_status (&self, theme: ItemTheme) -> impl Draw { - self.track().map(|track|{ - let ins = track.sequencer.midi_ins.len() as u16; - let border = Outer(true, Style::default().fg(Tui::g(96))); - Fixed::xy(20, 1 + ins, border.enclose( - Fixed::xy(20, 1 + ins, FieldV(theme, format!("MIDI ins: "), - Map::south(1, ||track.sequencer.midi_ins.iter(), - |port, index|Fill::x(Align::w(format!(" {index} {}", port.port_name()))))))))}) - } - fn view_midi_outs_status (&self, theme: ItemTheme) -> impl Draw { - self.track().map(|track|{ - let outs = track.sequencer.midi_outs.len() as u16; - let border = Outer(true, Style::default().fg(Tui::g(96))); - Fixed::xy(20, 1 + outs, border.enclose( - Fixed::xy(20, 1 + outs, FieldV(theme, format!("MIDI outs: "), - Map::south(1, ||track.sequencer.midi_outs.iter(), - |port, index|Fill::x(Align::w(format!(" {index} {}", port.port_name()))))))))}) - } - fn view_audio_ins_status (&self, theme: ItemTheme) -> impl Draw { - self.track().and_then(|track|track.devices.get(0)).map(|device|{ - let ins = device.audio_ins().len() as u16; - let border = Outer(true, Style::default().fg(Tui::g(96))); - Fixed::xy(20, 1 + ins, border.enclose( - Fixed::xy(20, 1 + ins, FieldV(theme, format!("Audio ins: "), - Map::south(1, ||device.audio_ins().iter(), - |port, index|Fill::x(Align::w(format!(" {index} {}", port.port_name()))))))))}) - } - fn view_audio_outs_status (&self, theme: ItemTheme) -> impl Draw { - self.track().and_then(|track|track.devices.last()).map(|device|{ - let outs = device.audio_outs().len() as u16; - let border = Outer(true, Style::default().fg(Tui::g(96))); - Fixed::xy(20, 1 + outs, border.enclose( - Fixed::xy(20, 1 + outs, FieldV(theme, format!("Audio outs: "), - Map::south(1, ||device.audio_outs().iter(), - |port, index|Fill::x(Align::w(format!(" {index} {}", port.port_name()))))))))}) - } -} - -impl Track { - pub fn per <'a, T: Draw + Layout + 'a, U: TracksSizes<'a>> ( - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a - ) -> impl Draw + Layout + 'a { - Map::new(tracks, - move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ - let width = (x2 - x1) as u16; - map_east(x1 as u16, width, Fixed::x(width, Tui::fg_bg( - track.color.lightest.rgb, - track.color.base.rgb, - callback(index, track))))}) - } -} - -pub(crate) fn per_track_top <'a, T: Draw + Layout + 'a, U: TracksSizes<'a>> ( - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a -) -> impl Draw + Layout + 'a { - Align::x(Tui::bg(Reset, Map::new(tracks, - move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ - let width = (x2 - x1) as u16; - map_east(x1 as u16, width, Fixed::x(width, Tui::fg_bg( - track.color.lightest.rgb, - track.color.base.rgb, - callback(index, track))))}))) -} - -pub(crate) fn per_track <'a, T: Draw + Layout + '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|Fill::y(Align::y(callback(index, track)))) -} - -pub(crate) fn io_ports <'a, T: PortsSizes<'a>> ( - fg: Color, bg: Color, iter: impl Fn()->T + Send + Sync + 'a -) -> impl Draw + Layout + 'a { - Map::new(iter, move|( - _index, name, connections, y, y2 - ): (usize, &'a Arc, &'a [Connect], usize, usize), _| - map_south(y as u16, (y2-y) as u16, Bsp::s( - Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(Bsp::e(&" ๓ฐฃฒ ", name))))), - Map::new(||connections.iter(), move|connect: &'a Connect, index|map_south(index as u16, 1, - Fill::x(Align::w(Tui::bold(false, Tui::fg_bg(fg, bg, - &connect.info))))))))) -} - -//pub(crate) fn io_conns <'a, T: PortsSizes<'a>> ( - //fg: Color, bg: Color, iter: &mut impl Iterator, &'a [Connect], usize, usize)> -//) -> impl Draw + Layout + 'a { - //Fill::xy(Thunk::new(move|to: &mut TuiOut|for (_, _, connections, y, y2) in &mut *iter { - //to.place(&map_south(y as u16, (y2-y) as u16, Bsp::s( - //Fill::x(Tui::bold(true, wrap(bg, fg, Fill::x(Align::w(&"โ–žโ–žโ–žโ–ž โ–žโ–žโ–žโ–ž"))))), - //Thunk::new(|to: &mut TuiOut|for (index, _connection) in connections.iter().enumerate() { - //to.place(&map_south(index as u16, 1, Fill::x(Align::w(Tui::bold(false, - //wrap(bg, fg, Fill::x(&""))))))) - //}) - //))) - //})) -//} - //track_scroll: Fill::x(Fixed::y(1, ScrollbarH { - //offset: arrangement.track_scroll, - //length: h_tracks_area as usize, - //total: h_scenes as usize, - //})), diff --git a/tek/device/arranger/arranger_view.rs b/tek/device/arranger/arranger_view.rs deleted file mode 100644 index 10114eb2..00000000 --- a/tek/device/arranger/arranger_view.rs +++ /dev/null @@ -1,391 +0,0 @@ -use crate::*; - -pub(crate) fn wrap ( - bg: Color, fg: Color, content: impl Draw + Layout -) -> impl Draw + Layout { - let left = Tui::fg_bg(bg, Reset, Fixed::x(1, RepeatV("โ–"))); - let right = Tui::fg_bg(bg, Reset, Fixed::x(1, RepeatV("โ–Œ"))); - Bsp::e(left, Bsp::w(right, Tui::fg_bg(fg, bg, content))) -} - -type Add<'a> = &'a dyn FnMut(&dyn Draw); - -impl Arrangement { - - pub fn view_inputs (&self, _theme: ItemTheme) -> impl Draw + Layout + '_ { - Bsp::s(self.view_inputs_header(), Thunk::new(|to: &mut TuiOut|{ - for (index, port) in self.midi_ins().iter().enumerate() { - to.place(&Push::x(index as u16 * 10, self.view_inputs_row(port))) - } - })) - } - - fn view_inputs_header (&self) -> impl Draw + Layout + '_ { - Fixed::y(1, Bsp::e( - Fixed::x(20, Align::w( - button_3("i", "nput ", format!("{}", self.midi_ins.len()), false))), - Bsp::w(Fixed::x(4, button_2("I", "+", false)), - Thunk::new(|to: &mut TuiOut|for (_index, track, x1, _x2) in self.tracks_with_sizes() { - to.place(&Push::x(x1 as u16, Tui::bg(track.color.dark.rgb, Align::w(Fixed::x(track.width as u16, row!( - Either::new(track.sequencer.monitoring, Tui::fg(Green, "mon "), "mon "), - Either::new(track.sequencer.recording, Tui::fg(Red, "rec "), "rec "), - Either::new(track.sequencer.overdub, Tui::fg(Yellow, "dub "), "dub "), - )))))) - })))) - } - - fn view_inputs_row (&self, port: &MidiInput) -> impl Draw + Layout { - Fixed::y(1, Bsp::e( - Fixed::x(20, Align::w( - Bsp::e(" โ— ", Tui::bold(true, Tui::fg(Rgb(255,255,255), port.port_name()))))), - Bsp::w(Fixed::x(4, ()), Thunk::new(|to: &mut TuiOut|{ - for (_index, track, _x1, _x2) in self.tracks_with_sizes() { - to.place(&Tui::bg(track.color.darker.rgb, Align::w(Fixed::x(track.width as u16, row!( - Either::new(track.sequencer.monitoring, Tui::fg(Green, " โ— "), " ยท "), - Either::new(track.sequencer.recording, Tui::fg(Red, " โ— "), " ยท "), - Either::new(track.sequencer.overdub, Tui::fg(Yellow, " โ— "), " ยท "), - ))))) - } - })))) - } - - pub fn view_outputs (&self, theme: ItemTheme) -> impl Draw + Layout { - let mut h = 1; - for output in self.midi_outs().iter() { - h += 1 + output.connections.len(); - } - let h = h as u16; - let list = Bsp::s( - Fixed::y(1, Fill::x(Align::w(button_3("o", "utput", format!("{}", self.midi_outs.len()), false)))), - Fixed::y(h - 1, Fill::xy(Align::nw(Thunk::new(|to: &mut TuiOut|{ - for (_index, port) in self.midi_outs().iter().enumerate() { - to.place(&Fixed::y(1,Fill::x(Bsp::e( - Align::w(Bsp::e(" โ— ", Tui::fg(Rgb(255,255,255),Tui::bold(true, port.port_name())))), - Fill::x(Align::e(format!("{}/{} ", - port.port().get_connections().len(), - port.connections.len()))))))); - for (index, conn) in port.connections.iter().enumerate() { - to.place(&Fixed::y(1, Fill::x(Align::w(format!(" c{index:02}{}", conn.info()))))); - } - } - }))))); - Fixed::y(h, view_track_row_section(theme, list, button_2("O", "+", false), - Tui::bg(theme.darker.rgb, Align::w(Fill::x( - Thunk::new(|to: &mut TuiOut|{ - for (index, track, _x1, _x2) in self.tracks_with_sizes() { - to.place(&Fixed::x(self.track_width(index, track), - Thunk::new(|to: &mut TuiOut|{ - to.place(&Fixed::y(1, Align::w(Bsp::e( - Either::new(true, Tui::fg(Green, "play "), "play "), - Either::new(false, Tui::fg(Yellow, "solo "), "solo "), - )))); - for (_index, port) in self.midi_outs().iter().enumerate() { - to.place(&Fixed::y(1, Align::w(Bsp::e( - Either::new(true, Tui::fg(Green, " โ— "), " ยท "), - Either::new(false, Tui::fg(Yellow, " โ— "), " ยท "), - )))); - for (_index, _conn) in port.connections.iter().enumerate() { - to.place(&Fixed::y(1, Fill::x(""))); - } - }})))}})))))) - } - - pub fn view_track_devices (&self, theme: ItemTheme) -> impl Draw + Layout { - let mut h = 2u16; - for track in self.tracks().iter() { - h = h.max(track.devices.len() as u16 * 2); - } - view_track_row_section(theme, - button_3("d", "evice", format!("{}", self.track().map(|t|t.devices.len()).unwrap_or(0)), false), - button_2("D", "+", false), - Thunk::new(move|to: &mut TuiOut|for (index, track, _x1, _x2) in self.tracks_with_sizes() { - to.place(&Fixed::xy(self.track_width(index, track), h + 1, - Tui::bg(track.color.dark.rgb, Align::nw(Map::south(2, move||0..h, - |_, _index|Fixed::xy(track.width as u16, 2, - Tui::fg_bg( - ItemTheme::G[32].lightest.rgb, - ItemTheme::G[32].dark.rgb, - Align::nw(format!(" ยท {}", "--"))))))))); - })) - } - -} - -fn view_track_row_section ( - _theme: ItemTheme, - button: impl Draw + Layout, - button_add: impl Draw + Layout, - content: impl Draw + Layout, -) -> impl Draw + Layout { - Bsp::w(Fill::y(Fixed::x(4, Align::nw(button_add))), - Bsp::e(Fixed::x(20, Fill::y(Align::nw(button))), Fill::xy(Align::c(content)))) -} - -pub trait HasClipsSize { - fn clips_size (&self) -> &Measure; -} -impl HasClipsSize for Arrangement { - fn clips_size (&self) -> &Measure { &self.inner_size } -} - -impl TracksView for Arrangement {} - -impl ClipsView for T {} - -pub trait TracksView: - ScenesView + - HasMidiIns + - HasMidiOuts + - HasSize + - HasTrackScroll + - HasSelection + - HasEditor + - HasClipsSize -{ - fn tracks_width_available (&self) -> u16 { - (self.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.width()); - 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() { - let data = (index, track, x, x + width); - x += width + Self::TRACK_SPACING; - Some(data) - } else { - None - } - }) - } - fn view_track_header <'a> ( - &'a self, theme: ItemTheme, content: impl Draw + Layout - ) -> impl Draw + Layout { - Fixed::x(12, Tui::bg(theme.darker.rgb, Fill::x(Align::e(content)))) - } - fn view_track_names (&self, theme: ItemTheme) -> impl Draw + Layout { - let track_count = self.tracks().len(); - let scene_count = self.scenes().len(); - let selected = self.selection(); - let button = Bsp::s( - 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 = Bsp::s( - button_2("T", "+", false), - button_2("S", "+", false)); - view_track_row_section(theme, button, button_2, Tui::bg(theme.darker.rgb, - Fixed::y(2, Thunk::new(|to: &mut TuiOut|{ - for (index, track, x1, _x2) in self.tracks_with_sizes() { - to.place(&Push::x(x1 as u16, Fixed::x(self.track_width(index, track), - Tui::bg(if selected.track() == Some(index) { - track.color.light.rgb - } else { - track.color.base.rgb - }, Bsp::s(Fill::x(Align::nw(Bsp::e( - format!("ยทt{index:02} "), - Tui::fg(Rgb(255, 255, 255), Tui::bold(true, &track.name)) - ))), ""))) ));}})))) - } - fn view_track_outputs <'a> (&'a self, theme: ItemTheme, _h: u16) -> impl Draw + Layout { - view_track_row_section(theme, - Bsp::s(Fill::x(Align::w(button_2("o", "utput", false))), - Thunk::new(|to: &mut TuiOut|for port in self.midi_outs().iter() { - to.place(&Fill::x(Align::w(port.port_name()))); - })), - button_2("O", "+", false), - Tui::bg(theme.darker.rgb, Align::w(Thunk::new(|to: &mut TuiOut|{ - for (index, track, _x1, _x2) in self.tracks_with_sizes() { - to.place(&Fixed::x(self.track_width(index, track), - Align::nw(Fill::y(Map::south(1, ||track.sequencer.midi_outs.iter(), - |port, index|Tui::fg(Rgb(255, 255, 255), - Fixed::y(1, Tui::bg(track.color.dark.rgb, Fill::x(Align::w( - format!("ยทo{index:02} {}", port.port_name())))))))))));}})))) - } - fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Draw + Layout { - let mut h = 0u16; - for track in self.tracks().iter() { - h = h.max(track.sequencer.midi_ins.len() as u16); - } - view_track_row_section(theme, - button_2("i", "nput", false), - button_2("I", "+", false), - Tui::bg(theme.darker.rgb, Align::w(Thunk::new(move|to: &mut TuiOut|{ - for (index, track, _x1, _x2) in self.tracks_with_sizes() { - to.place(&Fixed::xy(self.track_width(index, track), h + 1, - Align::nw(Bsp::s( - Tui::bg(track.color.base.rgb, - Fill::x(Align::w(row!( - Either::new(track.sequencer.monitoring, - Tui::fg(Green, "โ—mon "), "ยทmon "), - Either::new(track.sequencer.recording, - Tui::fg(Red, "โ—rec "), "ยทrec "), - Either::new(track.sequencer.overdub, - Tui::fg(Yellow, "โ—dub "), "ยทdub "), - )))), - Map::south(1, ||track.sequencer.midi_ins.iter(), - |port, index|Tui::fg_bg(Rgb(255, 255, 255), track.color.dark.rgb, - Fill::x(Align::w(format!("ยทi{index:02} {}", port.port_name()))))))))); - } - })))) - } - fn track_width (&self, index: usize, track: &Track) -> u16 { - track.width as u16 - } -} - -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() { - let data = (s, scene, y, y + height); - y += height; - Some(data) - } else { - None - } - }) - } - fn view_scenes_names (&self) -> impl Draw + Layout { - Fixed::x(20, Thunk::new(|to: &mut TuiOut|for (index, scene, ..) in self.scenes_with_sizes() { - to.place(&self.view_scene_name(index, scene)); - })) - } - fn view_scene_name <'a> (&'a self, index: usize, scene: &'a Scene) -> impl Draw + Layout + 'a { - let h = if self.selection().scene() == Some(index) && let Some(_editor) = self.editor() { - 7 - } else { - Self::H_SCENE as u16 - }; - let bg = if self.selection().scene() == Some(index) { - scene.color.light.rgb - } else { - scene.color.base.rgb - }; - let a = Fill::x(Align::w(Bsp::e(format!("ยทs{index:02} "), - Tui::fg(Tui::g(255), Tui::bold(true, &scene.name))))); - let b = When::new(self.selection().scene() == Some(index) && self.is_editing(), - Fill::xy(Align::nw(Bsp::s( - self.editor().as_ref().map(|e|e.clip_status()), - self.editor().as_ref().map(|e|e.edit_status()))))); - Fixed::xy(20, h, Tui::bg(bg, Align::nw(Bsp::s(a, b)))) - } -} - -pub trait ClipsView: - TracksView + - ScenesView + - HasClipsSize + - Send + - Sync -{ - fn view_scenes_clips <'a> (&'a self) - -> impl Draw + 'a - { - self.clips_size().of(Fill::xy(Bsp::a( - Fill::xy(Align::se(Tui::fg(Green, format!("{}x{}", self.clips_size().w(), self.clips_size().h())))), - Thunk::new(|to: &mut TuiOut|for ( - track_index, track, _, _ - ) in self.tracks_with_sizes() { - to.place(&Fixed::x(track.width as u16, - Fill::y(self.view_track_clips(track_index, track)))) - })))) - } - - fn view_track_clips <'a> (&'a self, track_index: usize, track: &'a Track) -> impl Draw + 'a { - Thunk::new(move|to: &mut TuiOut|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.rgb; - let mut outline = theme.base.rgb; - let bg = if self.selection().track() == Some(track_index) - && self.selection().scene() == Some(scene_index) - { - outline = theme.lighter.rgb; - theme.light.rgb - } else if self.selection().track() == Some(track_index) - || self.selection().scene() == Some(scene_index) - { - outline = theme.darkest.rgb; - theme.base.rgb - } else { - theme.dark.rgb - }; - let w = if self.selection().track() == Some(track_index) - && let Some(editor) = self.editor () - { - editor.width().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.height().max(12) - } else { - Self::H_SCENE - } as u16; - - to.place(&Fixed::xy(w, y, Bsp::b( - Fill::xy(Outer(true, Style::default().fg(outline))), - Fill::xy(Bsp::b( - Bsp::b( - Tui::fg_bg(outline, bg, Fill::xy("")), - Fill::xy(Align::nw(Tui::fg_bg(fg, bg, Tui::bold(true, name)))), - ), - Fill::xy(When::new(self.selection().track() == Some(track_index) - && self.selection().scene() == Some(scene_index) - && self.is_editing(), self.editor()))))))); - }) - } -} - -pub trait HasWidth { - const MIN_WIDTH: usize; - /// Increment track width. - fn width_inc (&mut self); - /// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH]. - fn width_dec (&mut self); -} - -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; - } - } -} diff --git a/tek/device/browse.rs b/tek/device/browse.rs deleted file mode 100644 index c9ecd55f..00000000 --- a/tek/device/browse.rs +++ /dev/null @@ -1,95 +0,0 @@ -use crate::*; -use std::path::PathBuf; -use std::ffi::OsString; -mod browse_api; pub use self::browse_api::*; -mod browse_view; //pub use self::browse_view::*; - -#[derive(Clone, Debug)] -pub enum BrowseTarget { - SaveProject, - LoadProject, - ImportSample(Arc>>), - ExportSample(Arc>>), - ImportClip(Arc>>), - ExportClip(Arc>>), -} - -impl PartialEq for BrowseTarget { - fn eq (&self, other: &Self) -> bool { - match self { - Self::ImportSample(_) => false, - Self::ExportSample(_) => false, - Self::ImportClip(_) => false, - Self::ExportClip(_) => false, - t => matches!(other, t) - } - } -} - -/// Browses for phrase to import/export -#[derive(Debug, Clone, Default, PartialEq)] -pub struct Browse { - pub cwd: PathBuf, - pub dirs: Vec<(OsString, String)>, - pub files: Vec<(OsString, String)>, - pub filter: String, - pub index: usize, - pub scroll: usize, - pub size: Measure, -} - -impl Browse { - - pub fn new (cwd: Option) -> Usually { - let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? }; - let mut dirs = vec![]; - let mut files = vec![]; - for entry in std::fs::read_dir(&cwd)? { - let entry = entry?; - let name = entry.file_name(); - let decoded = name.clone().into_string().unwrap_or_else(|_|"".to_string()); - let meta = entry.metadata()?; - if meta.is_dir() { - dirs.push((name, format!("๐Ÿ“ {decoded}"))); - } else if meta.is_file() { - files.push((name, format!("๐Ÿ“„ {decoded}"))); - } - } - Ok(Self { - cwd, - dirs, - files, - filter: "".to_string(), - index: 0, - scroll: 0, - size: Measure::new(), - }) - } - - pub fn len (&self) -> usize { - self.dirs.len() + self.files.len() - } - - pub fn is_dir (&self) -> bool { - self.index < self.dirs.len() - } - - pub fn is_file (&self) -> bool { - self.index >= self.dirs.len() - } - - pub fn path (&self) -> PathBuf { - self.cwd.join(if self.is_dir() { - &self.dirs[self.index].0 - } else if self.is_file() { - &self.files[self.index - self.dirs.len()].0 - } else { - unreachable!() - }) - } - - pub fn chdir (&self) -> Usually { - Self::new(Some(self.path())) - } - -} diff --git a/tek/device/browse/browse_api.rs b/tek/device/browse/browse_api.rs deleted file mode 100644 index ddbb04d5..00000000 --- a/tek/device/browse/browse_api.rs +++ /dev/null @@ -1,92 +0,0 @@ -use crate::*; - -impl Browse { - fn _todo_stub_path_buf (&self) -> PathBuf { - todo!() - } - fn _todo_stub_usize (&self) -> usize { - todo!() - } - fn _todo_stub_arc_str (&self) -> Arc { - todo!() - } -} - -def_command!(BrowseCommand: |browse: Browse| { - SetVisible => Ok(None), - SetPath { address: PathBuf } => Ok(None), - SetSearch { filter: Arc } => Ok(None), - SetCursor { cursor: usize } => Ok(None), -}); - -// Commands supported by [Browse] -//#[derive(Debug, Clone, PartialEq)] -//pub enum BrowseCommand { - //Begin, - //Cancel, - //Confirm, - //Select(usize), - //Chdir(PathBuf), - //Filter(Arc), -//} - //fn begin (browse: &mut Browse) => { - //unreachable!(); - //} - //fn cancel (browse: &mut Browse) => { - //todo!() - ////browse.mode = None; - ////Ok(None) - //} - //fn confirm (browse: &mut Browse) => { - //todo!() - ////Ok(match browse.mode { - ////Some(PoolMode::Import(index, ref mut browse)) => { - ////if browse.is_file() { - ////let path = browse.path(); - ////browse.mode = None; - ////let _undo = PoolClipCommand::import(browse, index, path)?; - ////None - ////} else if browse.is_dir() { - ////browse.mode = Some(PoolMode::Import(index, browse.chdir()?)); - ////None - ////} else { - ////None - ////} - ////}, - ////Some(PoolMode::Export(index, ref mut browse)) => { - ////todo!() - ////}, - ////_ => unreachable!(), - ////}) - //} - //fn select (browse: &mut Browse, index: usize) => { - //todo!() - ////Ok(match browse.mode { - ////Some(PoolMode::Import(index, ref mut browse)) => { - ////browse.index = index; - ////None - ////}, - ////Some(PoolMode::Export(index, ref mut browse)) => { - ////browse.index = index; - ////None - ////}, - ////_ => unreachable!(), - ////}) - //} - //fn chdir (browse: &mut Browse, dir: PathBuf) => { - //todo!() - ////Ok(match browse.mode { - ////Some(PoolMode::Import(index, ref mut browse)) => { - ////browse.mode = Some(PoolMode::Import(index, Browse::new(Some(dir))?)); - ////None - ////}, - ////Some(PoolMode::Export(index, ref mut browse)) => { - ////browse.mode = Some(PoolMode::Export(index, Browse::new(Some(dir))?)); - ////None - ////}, - ////_ => unreachable!(), - ////}) - //} - //fn filter (browse: &mut Browse, filter: Arc) => { - //todo!() - //} diff --git a/tek/device/browse/browse_view.rs b/tek/device/browse/browse_view.rs deleted file mode 100644 index 4d73b0bd..00000000 --- a/tek/device/browse/browse_view.rs +++ /dev/null @@ -1,33 +0,0 @@ -use crate::*; - -content!(TuiOut: |self: Browse|Map::south(1, ||EntriesIterator { - offset: 0, - index: 0, - length: self.dirs.len() + self.files.len(), - browser: self, -}, |entry, _index|Fill::x(Align::w(entry)))); - -struct EntriesIterator<'a> { - browser: &'a Browse, - offset: usize, - length: usize, - index: usize, -} - -impl<'a> Iterator for EntriesIterator<'a> { - type Item = Modify<&'a str>; - fn next (&mut self) -> Option { - let dirs = self.browser.dirs.len(); - let files = self.browser.files.len(); - let index = self.index; - if self.index < dirs { - self.index += 1; - Some(Tui::bold(true, self.browser.dirs[index].1.as_str())) - } else if self.index < dirs + files { - self.index += 1; - Some(Tui::bold(false, self.browser.files[index - dirs].1.as_str())) - } else { - None - } - } -} diff --git a/tek/device/clock.rs b/tek/device/clock.rs deleted file mode 100644 index f16baafe..00000000 --- a/tek/device/clock.rs +++ /dev/null @@ -1,19 +0,0 @@ -use crate::*; - -mod clock_api; pub use self::clock_api::*; -mod clock_model; pub use self::clock_model::*; -mod clock_view; pub use self::clock_view::*; - -pub trait HasClock: Send + Sync { - fn clock (&self) -> &Clock; - fn clock_mut (&mut self) -> &mut Clock; -} - -impl> HasClock for T { - fn clock (&self) -> &Clock { - self.get() - } - fn clock_mut (&mut self) -> &mut Clock { - self.get_mut() - } -} diff --git a/tek/device/clock/clock_api.rs b/tek/device/clock/clock_api.rs deleted file mode 100644 index faf93ace..00000000 --- a/tek/device/clock/clock_api.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::*; - -impl Clock { - fn _todo_provide_u32 (&self) -> u32 { - todo!() - } - fn _todo_provide_opt_u32 (&self) -> Option { - todo!() - } - fn _todo_provide_f64 (&self) -> f64 { - todo!() - } -} - -impl Command for ClockCommand { - fn execute (&self, state: &mut T) -> Perhaps { - self.execute(state.clock_mut()) // awesome - } -} - -def_command!(ClockCommand: |clock: Clock| { - SeekUsec { usec: f64 } => { - clock.playhead.update_from_usec(*usec); Ok(None) }, - SeekSample { sample: f64 } => { - clock.playhead.update_from_sample(*sample); Ok(None) }, - SeekPulse { pulse: f64 } => { - clock.playhead.update_from_pulse(*pulse); Ok(None) }, - SetBpm { bpm: f64 } => Ok(Some( - Self::SetBpm { bpm: clock.timebase().bpm.set(*bpm) })), - SetQuant { quant: f64 } => Ok(Some( - Self::SetQuant { quant: clock.quant.set(*quant) })), - SetSync { sync: f64 } => Ok(Some( - Self::SetSync { sync: clock.sync.set(*sync) })), - - Play { position: Option } => { - clock.play_from(*position)?; Ok(None) /* TODO Some(Pause(previousPosition)) */ }, - Pause { position: Option } => { - clock.pause_at(*position)?; Ok(None) }, - - TogglePlayback { position: u32 } => Ok(if clock.is_rolling() { - clock.pause_at(Some(*position))?; None - } else { - clock.play_from(Some(*position))?; None - }), -}); diff --git a/tek/device/clock/clock_model.rs b/tek/device/clock/clock_model.rs deleted file mode 100644 index 8496f93a..00000000 --- a/tek/device/clock/clock_model.rs +++ /dev/null @@ -1,204 +0,0 @@ -use crate::*; - -#[derive(Clone, Default)] -pub struct Clock { - /// JACK transport handle. - pub transport: Arc>, - /// Global temporal resolution (shared by [Moment] fields) - pub timebase: Arc, - /// Current global sample and usec (monotonic from JACK clock) - pub global: Arc, - /// Global sample and usec at which playback started - pub started: Arc>>, - /// Playback offset (when playing not from start) - pub offset: Arc, - /// Current playhead position - pub playhead: Arc, - /// Note quantization factor - pub quant: Arc, - /// Launch quantization factor - pub sync: Arc, - /// Size of buffer in samples - pub chunk: Arc, - /// For syncing the clock to an external source - pub midi_in: Arc>>, - /// For syncing other devices to this clock - pub midi_out: Arc>>, - /// For emitting a metronome - pub click_out: Arc>>, - // Cache of formatted strings - pub view_cache: Arc>, -} - -impl std::fmt::Debug for Clock { - fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("Clock") - .field("timebase", &self.timebase) - .field("chunk", &self.chunk) - .field("quant", &self.quant) - .field("sync", &self.sync) - .field("global", &self.global) - .field("playhead", &self.playhead) - .field("started", &self.started) - .finish() - } -} - -impl Clock { - pub fn new (jack: &Jack<'static>, bpm: Option) -> Usually { - let (chunk, transport) = jack.with_client(|c|(c.buffer_size(), c.transport())); - let timebase = Arc::new(Timebase::default()); - let clock = Self { - quant: Arc::new(24.into()), - sync: Arc::new(384.into()), - transport: Arc::new(Some(transport)), - chunk: Arc::new((chunk as usize).into()), - global: Arc::new(Moment::zero(&timebase)), - playhead: Arc::new(Moment::zero(&timebase)), - offset: Arc::new(Moment::zero(&timebase)), - started: RwLock::new(None).into(), - timebase, - midi_in: Arc::new(RwLock::new(Some(MidiInput::new(jack, &"M/clock", &[])?))), - midi_out: Arc::new(RwLock::new(Some(MidiOutput::new(jack, &"clock/M", &[])?))), - click_out: Arc::new(RwLock::new(Some(AudioOutput::new(jack, &"click", &[])?))), - ..Default::default() - }; - if let Some(bpm) = bpm { - clock.timebase.bpm.set(bpm); - } - Ok(clock) - } - pub fn timebase (&self) -> &Arc { - &self.timebase - } - /// Current sample rate - pub fn sr (&self) -> &SampleRate { - &self.timebase.sr - } - /// Current tempo - pub fn bpm (&self) -> &BeatsPerMinute { - &self.timebase.bpm - } - /// Current MIDI resolution - pub fn ppq (&self) -> &PulsesPerQuaver { - &self.timebase.ppq - } - /// Next pulse that matches launch sync (for phrase switchover) - pub fn next_launch_pulse (&self) -> usize { - let sync = self.sync.get() as usize; - let pulse = self.playhead.pulse.get() as usize; - if pulse % sync == 0 { - pulse - } else { - (pulse / sync + 1) * sync - } - } - /// Start playing, optionally seeking to a given location beforehand - pub fn play_from (&self, start: Option) -> Usually<()> { - if let Some(transport) = self.transport.as_ref() { - if let Some(start) = start { - transport.locate(start)?; - } - transport.start()?; - } - Ok(()) - } - /// Pause, optionally seeking to a given location afterwards - pub fn pause_at (&self, pause: Option) -> Usually<()> { - if let Some(transport) = self.transport.as_ref() { - transport.stop()?; - if let Some(pause) = pause { - transport.locate(pause)?; - } - } - Ok(()) - } - /// Is currently paused? - pub fn is_stopped (&self) -> bool { - self.started.read().unwrap().is_none() - } - /// Is currently playing? - pub fn is_rolling (&self) -> bool { - self.started.read().unwrap().is_some() - } - /// Update chunk size - pub fn set_chunk (&self, n_frames: usize) { - self.chunk.store(n_frames, Relaxed); - } - pub fn update_from_scope (&self, scope: &ProcessScope) -> Usually<()> { - // Store buffer length - self.set_chunk(scope.n_frames() as usize); - - // Store reported global frame and usec - let CycleTimes { current_frames, current_usecs, .. } = scope.cycle_times()?; - self.global.sample.set(current_frames as f64); - self.global.usec.set(current_usecs as f64); - - let mut started = self.started.write().unwrap(); - - // If transport has just started or just stopped, - // update starting point: - if let Some(transport) = self.transport.as_ref() { - match (transport.query_state()?, started.as_ref()) { - (TransportState::Rolling, None) => { - let moment = Moment::zero(&self.timebase); - moment.sample.set(current_frames as f64); - moment.usec.set(current_usecs as f64); - *started = Some(moment); - }, - (TransportState::Stopped, Some(_)) => { - *started = None; - }, - _ => {} - }; - } - - self.playhead.update_from_sample(started.as_ref() - .map(|started|current_frames as f64 - started.sample.get()) - .unwrap_or(0.)); - - Ok(()) - } - - pub fn bbt (&self) -> PositionBBT { - let pulse = self.playhead.pulse.get() as i32; - let ppq = self.timebase.ppq.get() as i32; - let bpm = self.timebase.bpm.get(); - let bar = (pulse / ppq) / 4; - PositionBBT { - bar: 1 + bar, - beat: 1 + (pulse / ppq) % 4, - tick: (pulse % ppq), - bar_start_tick: (bar * 4 * ppq) as f64, - beat_type: 4., - beats_per_bar: 4., - beats_per_minute: bpm, - ticks_per_beat: ppq as f64 - } - } - - pub fn next_launch_instant (&self) -> Moment { - Moment::from_pulse(self.timebase(), self.next_launch_pulse() as f64) - } - - /// Get index of first sample to populate. - /// - /// Greater than 0 means that the first pulse of the clip - /// falls somewhere in the middle of the chunk. - pub fn get_sample_offset (&self, scope: &ProcessScope, started: &Moment) -> usize{ - (scope.last_frame_time() as usize).saturating_sub( - started.sample.get() as usize + - self.started.read().unwrap().as_ref().unwrap().sample.get() as usize - ) - } - - // Get iterator that emits sample paired with pulse. - // - // * Sample: index into output buffer at which to write MIDI event - // * Pulse: index into clip from which to take the MIDI event - // - // Emitted for each sample of the output buffer that corresponds to a MIDI pulse. - pub fn get_pulses (&self, scope: &ProcessScope, offset: usize) -> TicksIterator { - self.timebase().pulses_between_samples(offset, offset + scope.n_frames() as usize) - } -} diff --git a/tek/device/clock/clock_view.rs b/tek/device/clock/clock_view.rs deleted file mode 100644 index 287eec9f..00000000 --- a/tek/device/clock/clock_view.rs +++ /dev/null @@ -1,164 +0,0 @@ -use crate::*; -use std::fmt::Write; - -pub fn view_transport ( - play: bool, - bpm: Arc>, - beat: Arc>, - time: Arc>, -) -> impl Draw + Layout { - let theme = ItemTheme::G[96]; - Tui::bg(Black, row!(Bsp::a( - Fill::xy(Align::w(button_play_pause(play))), - Fill::xy(Align::e(row!( - FieldH(theme, "BPM", bpm), - FieldH(theme, "Beat", beat), - FieldH(theme, "Time", time), - ))) - ))) -} - -pub fn view_status ( - sel: Option>, - sr: Arc>, - buf: Arc>, - lat: Arc>, -) -> impl Draw + Layout { - let theme = ItemTheme::G[96]; - Tui::bg(Black, row!(Bsp::a( - Fill::xy(Align::w(sel.map(|sel|FieldH(theme, "Selected", sel)))), - Fill::xy(Align::e(row!( - FieldH(theme, "SR", sr), - FieldH(theme, "Buf", buf), - FieldH(theme, "Lat", lat), - ))) - ))) -} - -pub(crate) fn button_play_pause (playing: bool) -> impl Draw + Layout { - let compact = true;//self.is_editing(); - Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, - Either::new(compact, - Thunk::new(move|to: &mut TuiOut|to.place(&Fixed::x(9, Either::new(playing, - Tui::fg(Rgb(0, 255, 0), " PLAYING "), - Tui::fg(Rgb(255, 128, 0), " STOPPED "))) - )), - Thunk::new(move|to: &mut TuiOut|to.place(&Fixed::x(5, Either::new(playing, - Tui::fg(Rgb(0, 255, 0), Bsp::s(" ๐Ÿญ๐Ÿญ‘๐Ÿฌฝ ", " ๐Ÿญž๐Ÿญœ๐Ÿญ˜ ",)), - Tui::fg(Rgb(255, 128, 0), Bsp::s(" โ–—โ–„โ–– ", " โ–โ–€โ–˜ ",)))) - )) - ) - ) -} - -#[derive(Debug)] pub struct ViewCache { - pub sr: Memo, String>, - pub buf: Memo, String>, - pub lat: Memo, String>, - pub bpm: Memo, String>, - pub beat: Memo, String>, - pub time: Memo, String>, -} - -impl Default for ViewCache { - fn default () -> Self { - let mut beat = String::with_capacity(16); - let _ = write!(beat, "{}", Self::BEAT_EMPTY); - let mut time = String::with_capacity(16); - let _ = write!(time, "{}", Self::TIME_EMPTY); - let mut bpm = String::with_capacity(16); - let _ = write!(bpm, "{}", Self::BPM_EMPTY); - Self { - beat: Memo::new(None, beat), - time: Memo::new(None, time), - bpm: Memo::new(None, bpm), - sr: Memo::new(None, String::with_capacity(16)), - buf: Memo::new(None, String::with_capacity(16)), - lat: Memo::new(None, String::with_capacity(16)), - } - } -} - -impl ViewCache { - pub const BEAT_EMPTY: &'static str = "-.-.--"; - pub const TIME_EMPTY: &'static str = "-.---s"; - pub const BPM_EMPTY: &'static str = "---.---"; - - //pub fn track_counter (cache: &Arc>, track: usize, tracks: usize) - //-> Arc> - //{ - //let data = (track, tracks); - //cache.write().unwrap().trks.update(Some(data), rewrite!(buf, "{}/{}", data.0, data.1)); - //cache.read().unwrap().trks.view.clone() - //} - - //pub fn scene_add (cache: &Arc>, scene: usize, scenes: usize, is_editing: bool) - //-> impl Draw - //{ - //let data = (scene, scenes); - //cache.write().unwrap().scns.update(Some(data), rewrite!(buf, "({}/{})", data.0, data.1)); - //button_3("S", "add scene", cache.read().unwrap().scns.view.clone(), is_editing) - //} - - pub fn update_clock (cache: &Arc>, clock: &Clock, compact: bool) { - let rate = clock.timebase.sr.get(); - let chunk = clock.chunk.load(Relaxed) as f64; - let lat = chunk / rate * 1000.; - let delta = |start: &Moment|clock.global.usec.get() - start.usec.get(); - let mut cache = cache.write().unwrap(); - cache.buf.update(Some(chunk), rewrite!(buf, "{chunk}")); - cache.lat.update(Some(lat), rewrite!(buf, "{lat:.1}ms")); - cache.sr.update(Some((compact, rate)), |buf,_,_|{ - buf.clear(); - if compact { - write!(buf, "{:.1}kHz", rate / 1000.) - } else { - write!(buf, "{:.0}Hz", rate) - } - }); - if let Some(now) = clock.started.read().unwrap().as_ref().map(delta) { - let pulse = clock.timebase.usecs_to_pulse(now); - let time = now/1000000.; - let bpm = clock.timebase.bpm.get(); - cache.beat.update(Some(pulse), |buf, _, _|{ - buf.clear(); - clock.timebase.format_beats_1_to(buf, pulse) - }); - cache.time.update(Some(time), rewrite!(buf, "{:.3}s", time)); - cache.bpm.update(Some(bpm), rewrite!(buf, "{:.3}", bpm)); - } else { - cache.beat.update(None, rewrite!(buf, "{}", ViewCache::BEAT_EMPTY)); - cache.time.update(None, rewrite!(buf, "{}", ViewCache::TIME_EMPTY)); - cache.bpm.update(None, rewrite!(buf, "{}", ViewCache::BPM_EMPTY)); - } - } - - //pub fn view_h2 (&self) -> impl Draw { - //let cache = self.project.clock.view_cache.clone(); - //let cache = cache.read().unwrap(); - //add(&Fixed::x(15, Align::w(Bsp::s( - //FieldH(theme, "Beat", cache.beat.view.clone()), - //FieldH(theme, "Time", cache.time.view.clone()), - //)))); - //add(&Fixed::x(13, Align::w(Bsp::s( - //Fill::x(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))), - //Fill::x(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))), - //)))); - //add(&Fixed::x(12, Align::w(Bsp::s( - //Fill::x(Align::w(FieldH(theme, "Buf", cache.buf.view.clone()))), - //Fill::x(Align::w(FieldH(theme, "Lat", cache.lat.view.clone()))), - //)))); - //add(&Bsp::s( - //Fill::x(Align::w(FieldH(theme, "Selected", Align::w(self.selection().describe( - //self.tracks(), - //self.scenes() - //))))), - //Fill::x(Align::w(FieldH(theme, format!("History ({})", self.history.len()), - //self.history.last().map(|last|Fill::x(Align::w(format!("{:?}", last.0))))))) - //)); - ////if let Some(last) = self.history.last() { - ////add(&FieldV(theme, format!("History ({})", self.history.len()), - ////Fill::x(Align::w(format!("{:?}", last.0))))); - ////} - //} -} diff --git a/tek/device/editor.rs b/tek/device/editor.rs deleted file mode 100644 index 5ce20dd0..00000000 --- a/tek/device/editor.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod editor_api; pub use self::editor_api::*; -mod editor_model; pub use self::editor_model::*; -mod editor_view; //pub use self::editor_view::*; -mod editor_view_h; pub use self::editor_view_h::*; -mod editor_view_v; pub use self::editor_view_v::*; diff --git a/tek/device/editor/editor_api.rs b/tek/device/editor/editor_api.rs deleted file mode 100644 index 2da18f81..00000000 --- a/tek/device/editor/editor_api.rs +++ /dev/null @@ -1,117 +0,0 @@ -use crate::*; - -def_command!(MidiEditCommand: |editor: MidiEditor| { - Show { clip: Option>> } => { - editor.set_clip(clip.as_ref()); editor.redraw(); Ok(None) }, - DeleteNote => { - editor.redraw(); todo!() }, - AppendNote { advance: bool } => { - editor.put_note(*advance); editor.redraw(); Ok(None) }, - SetNotePos { pos: usize } => { - editor.set_note_pos((*pos).min(127)); editor.redraw(); Ok(None) }, - SetNoteLen { len: usize } => { - editor.set_note_len(*len); editor.redraw(); Ok(None) }, - SetNoteScroll { scroll: usize } => { - editor.set_note_lo((*scroll).min(127)); editor.redraw(); Ok(None) }, - SetTimePos { pos: usize } => { - editor.set_time_pos(*pos); editor.redraw(); Ok(None) }, - SetTimeScroll { scroll: usize } => { - editor.set_time_start(*scroll); editor.redraw(); Ok(None) }, - SetTimeZoom { zoom: usize } => { - editor.set_time_zoom(*zoom); editor.redraw(); Ok(None) }, - SetTimeLock { lock: bool } => { - editor.set_time_lock(*lock); editor.redraw(); Ok(None) }, - // TODO: 1-9 seek markers that by default start every 8th of the clip -}); - -impl MidiEditor { - fn _todo_opt_clip_stub (&self) -> Option>> { - todo!() - } - - fn clip_length (&self) -> usize { - self.clip().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1) - } - - fn note_length (&self) -> usize { - self.get_note_len() - } - - fn note_pos (&self) -> usize { - self.get_note_pos() - } - fn note_pos_next (&self) -> usize { - self.get_note_pos() + 1 - } - fn note_pos_next_octave (&self) -> usize { - self.get_note_pos() + 12 - } - fn note_pos_prev (&self) -> usize { - self.get_note_pos().saturating_sub(1) - } - fn note_pos_prev_octave (&self) -> usize { - self.get_note_pos().saturating_sub(12) - } - - fn note_len (&self) -> usize { - self.get_note_len() - } - fn note_len_next (&self) -> usize { - self.get_note_len() + 1 - } - fn note_len_prev (&self) -> usize { - self.get_note_len().saturating_sub(1) - } - - fn note_range (&self) -> usize { - self.get_note_axis() - } - fn note_range_next (&self) -> usize { - self.get_note_axis() + 1 - } - fn note_range_prev (&self) -> usize { - self.get_note_axis().saturating_sub(1) - } - - fn time_pos (&self) -> usize { - self.get_time_pos() - } - fn time_pos_next (&self) -> usize { - (self.get_time_pos() + self.get_note_len()) % self.clip_length() - } - fn time_pos_next_fine (&self) -> usize { - (self.get_time_pos() + 1) % self.clip_length() - } - fn time_pos_prev (&self) -> usize { - let step = self.get_note_len(); - self.get_time_pos().overflowing_sub(step) - .0.min(self.clip_length().saturating_sub(step)) - } - fn time_pos_prev_fine (&self) -> usize { - self.get_time_pos().overflowing_sub(1) - .0.min(self.clip_length().saturating_sub(1)) - } - - fn time_zoom (&self) -> usize { - self.get_time_zoom() - } - fn time_zoom_next (&self) -> usize { - self.get_time_zoom() + 1 - } - fn time_zoom_next_fine (&self) -> usize { - self.get_time_zoom() + 1 - } - fn time_zoom_prev (&self) -> usize { - self.get_time_zoom().saturating_sub(1).max(1) - } - fn time_zoom_prev_fine (&self) -> usize { - self.get_time_zoom().saturating_sub(1).max(1) - } - - fn time_lock (&self) -> bool { - self.get_time_lock() - } - fn time_lock_toggled (&self) -> bool { - !self.get_time_lock() - } -} diff --git a/tek/device/editor/editor_model.rs b/tek/device/editor/editor_model.rs deleted file mode 100644 index 0082725c..00000000 --- a/tek/device/editor/editor_model.rs +++ /dev/null @@ -1,142 +0,0 @@ -use crate::*; - -/// Contains state for viewing and editing a clip -pub struct MidiEditor { - /// Size of editor on screen - pub size: Measure, - /// View mode and state of editor - pub mode: PianoHorizontal, -} - -has!(Measure: |self: MidiEditor|self.size); - -impl std::fmt::Debug for MidiEditor { - fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("MidiEditor") - .field("mode", &self.mode) - .finish() - } -} - -impl Default for MidiEditor { - fn default () -> Self { - Self { - size: Measure::new(), - mode: PianoHorizontal::new(None), - } - } -} - -from!(|clip: &Arc>|MidiEditor = { - let model = Self::from(Some(clip.clone())); - model.redraw(); - model -}); - -from!(|clip: Option>>|MidiEditor = { - let mut model = Self::default(); - *model.clip_mut() = clip; - model.redraw(); - model -}); - -impl MidiEditor { - /// Put note at current position - pub fn put_note (&mut self, advance: bool) { - let mut redraw = false; - if let Some(clip) = self.clip() { - let mut clip = clip.write().unwrap(); - let note_start = self.get_time_pos(); - let note_pos = self.get_note_pos(); - let note_len = self.get_note_len(); - let note_end = note_start + (note_len.saturating_sub(1)); - let key: u7 = u7::from(note_pos as u8); - let vel: u7 = 100.into(); - let length = clip.length; - let note_end = note_end % length; - let note_on = MidiMessage::NoteOn { key, vel }; - if !clip.notes[note_start].iter().any(|msg|*msg == note_on) { - clip.notes[note_start].push(note_on); - } - let note_off = MidiMessage::NoteOff { key, vel }; - if !clip.notes[note_end].iter().any(|msg|*msg == note_off) { - clip.notes[note_end].push(note_off); - } - if advance { - self.set_time_pos((note_end + 1) % clip.length); - } - redraw = true; - } - if redraw { - self.mode.redraw(); - } - } -} - -impl TimeRange for MidiEditor { - fn time_len (&self) -> &AtomicUsize { self.mode.time_len() } - fn time_zoom (&self) -> &AtomicUsize { self.mode.time_zoom() } - fn time_lock (&self) -> &AtomicBool { self.mode.time_lock() } - fn time_start (&self) -> &AtomicUsize { self.mode.time_start() } - fn time_axis (&self) -> &AtomicUsize { self.mode.time_axis() } -} - -impl NoteRange for MidiEditor { - fn note_lo (&self) -> &AtomicUsize { self.mode.note_lo() } - fn note_axis (&self) -> &AtomicUsize { self.mode.note_axis() } -} - -impl NotePoint for MidiEditor { - fn note_len (&self) -> &AtomicUsize { self.mode.note_len() } - fn note_pos (&self) -> &AtomicUsize { self.mode.note_pos() } -} - -impl TimePoint for MidiEditor { - fn time_pos (&self) -> &AtomicUsize { self.mode.time_pos() } -} - -impl MidiViewer for MidiEditor { - fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { self.mode.buffer_size(clip) } - fn redraw (&self) { self.mode.redraw() } - fn clip (&self) -> &Option>> { self.mode.clip() } - fn clip_mut (&mut self) -> &mut Option>> { self.mode.clip_mut() } - fn set_clip (&mut self, p: Option<&Arc>>) { self.mode.set_clip(p) } -} - -impl>> HasEditor for T {} - -pub trait HasEditor: Has> { - fn editor (&self) -> Option<&MidiEditor> { - self.get().as_ref() - } - fn editor_mut (&mut self) -> Option<&mut MidiEditor> { - self.get_mut().as_mut() - } - fn is_editing (&self) -> bool { - self.editor().is_some() - } - fn editor_w (&self) -> usize { - self.editor().map(|e|e.size.w()).unwrap_or(0) - } - fn editor_h (&self) -> usize { - self.editor().map(|e|e.size.h()).unwrap_or(0) - } -} - -#[macro_export] macro_rules! has_editor { - (|$self:ident: $Struct:ident|{ - editor = $e0:expr; - editor_w = $e1:expr; - editor_h = $e2:expr; - is_editing = $e3:expr; - }) => { - impl HasEditor for $Struct { - fn editor (&$self) -> Option<&MidiEditor> { $e0.as_ref() } - fn editor_mut (&mut $self) -> Option<&mut MidiEditor> { $e0.as_mut() } - fn editor_w (&$self) -> usize { $e1 } - fn editor_h (&$self) -> usize { $e2 } - fn is_editing (&$self) -> bool { $e3 } - } - }; -} - diff --git a/tek/device/editor/editor_view.rs b/tek/device/editor/editor_view.rs deleted file mode 100644 index afcf904e..00000000 --- a/tek/device/editor/editor_view.rs +++ /dev/null @@ -1,57 +0,0 @@ -use crate::*; - -tui_layout!(|self: MidiEditor, to|self.content().layout(to)); -tui_draw!(|self: MidiEditor, to|self.content().draw(to)); -content!(TuiOut: |self: MidiEditor| { - self.autoscroll(); - //self.autozoom(); - self.size.of(&self.mode) -}); - -impl MidiEditor { - - pub fn clip_status (&self) -> impl Draw + Layout + '_ { - let (_color, name, length, looped) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { - (clip.color, clip.name.clone(), clip.length, clip.looped) - } else { (ItemTheme::G[64], String::new().into(), 0, false) }; - Fixed::x(20, col!( - Fill::x(Align::w(Bsp::e( - button_2("f2", "name ", false), - Fill::x(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{name} "))))))), - Fill::x(Align::w(Bsp::e( - button_2("l", "ength ", false), - Fill::x(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{length} "))))))), - Fill::x(Align::w(Bsp::e( - button_2("r", "epeat ", false), - Fill::x(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{looped} "))))))), - )) - } - - pub fn edit_status (&self) -> impl Draw + Layout + '_ { - let (_color, length) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { - (clip.color, clip.length) - } else { (ItemTheme::G[64], 0) }; - let time_pos = self.get_time_pos(); - let time_zoom = self.get_time_zoom(); - let time_lock = if self.get_time_lock() { "[lock]" } else { " " }; - let note_pos = self.get_note_pos(); - let note_name = format!("{:4}", Note::pitch_to_name(note_pos)); - let note_pos = format!("{:>3}", note_pos); - let note_len = format!("{:>4}", self.get_note_len()); - Fixed::x(20, col!( - Fill::x(Align::w(Bsp::e( - button_2("t", "ime ", false), - Fill::x(Align::e(Tui::fg(Rgb(255, 255, 255), - format!("{length} /{time_zoom} +{time_pos} "))))))), - Fill::x(Align::w(Bsp::e( - button_2("z", "lock ", false), - Fill::x(Align::e(Tui::fg(Rgb(255, 255, 255), - format!("{time_lock}"))))))), - Fill::x(Align::w(Bsp::e( - button_2("x", "note ", false), - Fill::x(Align::e(Tui::fg(Rgb(255, 255, 255), - format!("{note_name} {note_pos} {note_len}"))))))), - )) - } - -} diff --git a/tek/device/editor/editor_view_h.rs b/tek/device/editor/editor_view_h.rs deleted file mode 100644 index e461e738..00000000 --- a/tek/device/editor/editor_view_h.rs +++ /dev/null @@ -1,328 +0,0 @@ -use crate::*; - -/// A clip, rendered as a horizontal piano roll. -#[derive(Clone)] -pub struct PianoHorizontal { - pub clip: Option>>, - /// Buffer where the whole clip is rerendered on change - pub buffer: Arc>, - /// Size of actual notes area - pub size: Measure, - /// The display window - pub range: MidiRangeModel, - /// The note cursor - pub point: MidiPointModel, - /// The highlight color palette - pub color: ItemTheme, - /// Width of the keyboard - pub keys_width: u16, -} - -has!(Measure:|self:PianoHorizontal|self.size); - -impl PianoHorizontal { - pub fn new (clip: Option<&Arc>>) -> Self { - let size = Measure::new(); - let mut range = MidiRangeModel::from((12, true)); - range.time_axis = size.x.clone(); - range.note_axis = size.y.clone(); - let piano = Self { - keys_width: 5, - size, - range, - buffer: RwLock::new(Default::default()).into(), - point: MidiPointModel::default(), - clip: clip.cloned(), - color: clip.as_ref().map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]), - }; - piano.redraw(); - piano - } -} - -pub(crate) fn note_y_iter (note_lo: usize, note_hi: usize, y0: u16) - -> impl Iterator -{ - (note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n)) -} - -tui_layout!(|self: PianoHorizontal, to|self.content().layout(to)); -tui_draw!(|self: PianoHorizontal, to|self.content().draw(to)); -content!(TuiOut:|self: PianoHorizontal| Bsp::s( - Bsp::e( - Fixed::x(5, format!("{}x{}", self.size.w(), self.size.h())), - self.timeline() - ), - Bsp::e( - self.keys(), - self.size.of(Bsp::b( - Fill::xy(self.notes()), - Fill::xy(self.cursor()), - )) - ), -)); - -impl PianoHorizontal { - /// Draw the piano roll background. - /// - /// This mode uses full blocks on note on and half blocks on legato: โ–ˆโ–„ โ–ˆโ–„ โ–ˆโ–„ - fn draw_bg ( - buf: &mut BigBuffer, - clip: &MidiClip, - zoom: usize, - note_len: usize, - note_point: usize, - time_point: usize, - ) { - for (y, note) in (0..=127).rev().enumerate() { - for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) { - let cell = buf.get_mut(x, y).unwrap(); - if note == (127-note_point) || time == time_point { - cell.set_bg(Rgb(0,0,0)); - } else { - cell.set_bg(clip.color.darkest.rgb); - } - if time % 384 == 0 { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('โ”‚'); - } else if time % 96 == 0 { - cell.set_fg(clip.color.dark.rgb); - cell.set_char('โ•Ž'); - } else if time % note_len == 0 { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('โ”Š'); - } else if (127 - note) % 12 == 0 { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('='); - } else if (127 - note) % 6 == 0 { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('โ€”'); - } else { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('ยท'); - } - } - } - } - /// Draw the piano roll foreground. - /// - /// This mode uses full blocks on note on and half blocks on legato: โ–ˆโ–„ โ–ˆโ–„ โ–ˆโ–„ - fn draw_fg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize) { - let style = Style::default().fg(clip.color.base.rgb);//.bg(Rgb(0, 0, 0)); - let mut notes_on = [false;128]; - for (x, time_start) in (0..clip.length).step_by(zoom).enumerate() { - for (_y, note) in (0..=127).rev().enumerate() { - if let Some(cell) = buf.get_mut(x, note) { - if notes_on[note] { - cell.set_char('โ–‚'); - cell.set_style(style); - } - } - } - let time_end = time_start + zoom; - for time in time_start..time_end.min(clip.length) { - for event in clip.notes[time].iter() { - match event { - MidiMessage::NoteOn { key, .. } => { - let note = key.as_int() as usize; - if let Some(cell) = buf.get_mut(x, note) { - cell.set_char('โ–ˆ'); - cell.set_style(style); - } - notes_on[note] = true - }, - MidiMessage::NoteOff { key, .. } => { - notes_on[key.as_int() as usize] = false - }, - _ => {} - } - } - } - - } - } - fn notes (&self) -> impl Draw { - let time_start = self.get_time_start(); - let note_lo = self.get_note_lo(); - let note_hi = self.get_note_hi(); - let buffer = self.buffer.clone(); - Thunk::new(move|to: &mut TuiOut|{ - let source = buffer.read().unwrap(); - let [x0, y0, w, _h] = to.area().xywh(); - //if h as usize != note_axis { - //panic!("area height mismatch: {h} <> {note_axis}"); - //} - for (area_x, screen_x) in (x0..x0+w).enumerate() { - for (area_y, screen_y, _note) in note_y_iter(note_lo, note_hi, y0) { - let source_x = time_start + area_x; - let source_y = note_hi - area_y; - // TODO: enable loop rollover: - //let source_x = (time_start + area_x) % source.width.max(1); - //let source_y = (note_hi - area_y) % source.height.max(1); - let is_in_x = source_x < source.width; - let is_in_y = source_y < source.height; - if is_in_x && is_in_y { - if let Some(source_cell) = source.get(source_x, source_y) { - if let Some(cell) = to.buffer.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) { - *cell = source_cell.clone(); - } - } - } - } - } - }) - } - fn cursor (&self) -> impl Draw + Layout { - let note_hi = self.get_note_hi(); - let note_lo = self.get_note_lo(); - let note_pos = self.get_note_pos(); - let note_len = self.get_note_len(); - let time_pos = self.get_time_pos(); - let time_start = self.get_time_start(); - let time_zoom = self.get_time_zoom(); - let style = Some(Style::default().fg(self.color.lightest.rgb)); - Thunk::new(move|to: &mut TuiOut|{ - let [x0, y0, w, _] = to.area().xywh(); - for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { - if note == note_pos { - for x in 0..w { - let screen_x = x0 + x; - let time_1 = time_start + x as usize * time_zoom; - let time_2 = time_1 + time_zoom; - if time_1 <= time_pos && time_pos < time_2 { - to.blit(&"โ–ˆ", screen_x, screen_y, style); - let tail = note_len as u16 / time_zoom as u16; - for x_tail in (screen_x + 1)..(screen_x + tail) { - to.blit(&"โ–‚", x_tail, screen_y, style); - } - break - } - } - break - } - } - }) - } - fn keys (&self) -> impl Draw + Layout { - let state = self; - let color = state.color; - let note_lo = state.get_note_lo(); - let note_hi = state.get_note_hi(); - let note_pos = state.get_note_pos(); - let key_style = Some(Style::default().fg(Rgb(192, 192, 192)).bg(Rgb(0, 0, 0))); - let off_style = Some(Style::default().fg(Tui::g(255))); - let on_style = Some(Style::default().fg(Rgb(255,0,0)).bg(color.base.rgb).bold()); - Fill::y(Fixed::x(self.keys_width, Thunk::new(move|to: &mut TuiOut|{ - let [x, y0, _w, _h] = to.area().xywh(); - for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { - to.blit(&to_key(note), x, screen_y, key_style); - if note > 127 { - continue - } - if note == note_pos { - to.blit(&format!("{:<5}", Note::pitch_to_name(note)), x, screen_y, on_style) - } else { - to.blit(&Note::pitch_to_name(note), x, screen_y, off_style) - }; - } - }))) - } - fn timeline (&self) -> impl Draw + Layout + '_ { - Fill::x(Fixed::y(1, Thunk::new(move|to: &mut TuiOut|{ - let [x, y, w, _h] = to.area(); - let style = Some(Style::default().dim()); - let length = self.clip.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); - for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) { - let t = area_x as usize * self.time_zoom().get(); - if t < length { - to.blit(&"|", screen_x, y, style); - } - } - }))) - } -} - -impl TimeRange for PianoHorizontal { - fn time_len (&self) -> &AtomicUsize { self.range.time_len() } - fn time_zoom (&self) -> &AtomicUsize { self.range.time_zoom() } - fn time_lock (&self) -> &AtomicBool { self.range.time_lock() } - fn time_start (&self) -> &AtomicUsize { self.range.time_start() } - fn time_axis (&self) -> &AtomicUsize { self.range.time_axis() } -} - -impl NoteRange for PianoHorizontal { - fn note_lo (&self) -> &AtomicUsize { self.range.note_lo() } - fn note_axis (&self) -> &AtomicUsize { self.range.note_axis() } -} - -impl NotePoint for PianoHorizontal { - fn note_len (&self) -> &AtomicUsize { self.point.note_len() } - fn note_pos (&self) -> &AtomicUsize { self.point.note_pos() } -} - -impl TimePoint for PianoHorizontal { - fn time_pos (&self) -> &AtomicUsize { self.point.time_pos() } -} - -impl MidiViewer for PianoHorizontal { - fn clip (&self) -> &Option>> { - &self.clip - } - fn clip_mut (&mut self) -> &mut Option>> { - &mut self.clip - } - /// Determine the required space to render the clip. - fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { - (clip.length / self.range.time_zoom().get(), 128) - } - fn redraw (&self) { - *self.buffer.write().unwrap() = if let Some(clip) = self.clip.as_ref() { - let clip = clip.read().unwrap(); - let buf_size = self.buffer_size(&clip); - let mut buffer = BigBuffer::from(buf_size); - let time_zoom = self.get_time_zoom(); - self.time_len().set(clip.length); - PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom, - self.get_note_len(), self.get_note_pos(), self.get_time_pos()); - PianoHorizontal::draw_fg(&mut buffer, &clip, time_zoom); - buffer - } else { - Default::default() - } - } - fn set_clip (&mut self, clip: Option<&Arc>>) { - *self.clip_mut() = clip.cloned(); - self.color = clip.map(|p|p.read().unwrap().color) - .unwrap_or(ItemTheme::G[64]); - self.redraw(); - } -} - -impl std::fmt::Debug for PianoHorizontal { - fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - let buffer = self.buffer.read().unwrap(); - f.debug_struct("PianoHorizontal") - .field("time_zoom", &self.range.time_zoom) - .field("buffer", &format!("{}x{}", buffer.width, buffer.height)) - .finish() - } -} - // Update sequencer playhead indicator - //self.now().set(0.); - //if let Some((ref started_at, Some(ref playing))) = self.sequencer.play_clip { - //let clip = clip.read().unwrap(); - //if *playing.read().unwrap() == *clip { - //let pulse = self.current().pulse.get(); - //let start = started_at.pulse.get(); - //let now = (pulse - start) % clip.length as f64; - //self.now().set(now); - //} - //} - -fn to_key (note: usize) -> &'static str { - match note % 12 { - 11 | 9 | 7 | 5 | 4 | 2 | 0 => "โ–ˆโ–ˆโ–ˆโ–ˆโ–Œ", - 10 | 8 | 6 | 3 | 1 => " ", - _ => unreachable!(), - } -} diff --git a/tek/device/editor/editor_view_v.rs b/tek/device/editor/editor_view_v.rs deleted file mode 100644 index 2913f04a..00000000 --- a/tek/device/editor/editor_view_v.rs +++ /dev/null @@ -1,39 +0,0 @@ -//! TODO - -use crate::*; - -pub struct OctaveVertical { - on: [bool; 12], - colors: [Color; 3] -} - -impl Default for OctaveVertical { - fn default () -> Self { - Self { - on: [false; 12], - colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)] - } - } -} - -impl OctaveVertical { - fn color (&self, pitch: usize) -> Color { - let pitch = pitch % 12; - self.colors[if self.on[pitch] { 2 } else { - match pitch { 0 | 2 | 4 | 5 | 6 | 8 | 10 => 0, _ => 1 } - }] - } -} - -impl Content for OctaveVertical { - fn content (&self) -> impl Draw + Layout + '_ { - row!( - Tui::fg_bg(self.color(0), self.color(1), "โ–™"), - Tui::fg_bg(self.color(2), self.color(3), "โ–™"), - Tui::fg_bg(self.color(4), self.color(5), "โ–Œ"), - Tui::fg_bg(self.color(6), self.color(7), "โ–Ÿ"), - Tui::fg_bg(self.color(8), self.color(9), "โ–Ÿ"), - Tui::fg_bg(self.color(10), self.color(11), "โ–Ÿ"), - ) - } -} diff --git a/tek/device/lib.rs b/tek/device/lib.rs deleted file mode 100644 index 56df9abf..00000000 --- a/tek/device/lib.rs +++ /dev/null @@ -1,72 +0,0 @@ -#![feature(let_chains)] -#![feature(trait_alias)] -#![feature(if_let_guard)] -#![feature(impl_trait_in_assoc_type)] - -pub(crate) use std::cmp::Ord; -pub(crate) use std::fmt::{Debug, Formatter}; -pub(crate) use std::thread::JoinHandle; -pub(crate) use std::sync::{Arc, RwLock}; -pub(crate) use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}; -pub(crate) use std::fs::File; -pub(crate) use std::path::PathBuf; -pub(crate) use std::error::Error; -pub(crate) use std::ffi::OsString; - -pub(crate) use ::tengri::{from, has, maybe_has, Usually, Perhaps, Has, MaybeHas}; -pub(crate) use ::tengri::{dsl::*, input::*, output::{*, Layout}, tui::{*, ratatui::prelude::*}}; -pub(crate) use ::tek_engine::*; -pub(crate) use ::tek_engine::midi::{u7, LiveEvent, MidiMessage}; -pub(crate) use ::tek_engine::jack::{Control, ProcessScope, RawMidi}; -pub(crate) use ratatui::{prelude::Rect, widgets::{Widget, canvas::{Canvas, Line}}}; -pub(crate) use Color::*; - -mod device; pub use self::device::*; - -/// Define a type alias for iterators of sized items (columns). -macro_rules! def_sizes_iter { - ($Type:ident => $($Item:ty),+) => { - pub trait $Type<'a> = - Iterator + Send + Sync + 'a; - } -} - -#[cfg(feature = "arranger")] mod arranger; #[cfg(feature = "arranger")] pub use self::arranger::*; -#[cfg(feature = "browse")] mod browse; #[cfg(feature = "browse")] pub use self::browse::*; -#[cfg(feature = "clap")] mod clap; #[cfg(feature = "clap")] pub use self::clap::*; -#[cfg(feature = "clock")] mod clock; #[cfg(feature = "clock")] pub use self::clock::*; -#[cfg(feature = "editor")] mod editor; #[cfg(feature = "editor")] pub use self::editor::*; -#[cfg(feature = "lv2")] mod lv2; #[cfg(feature = "lv2")] pub use self::lv2::*; -#[cfg(feature = "meter")] mod meter; #[cfg(feature = "meter")] pub use self::meter::*; -#[cfg(feature = "mixer")] mod mixer; #[cfg(feature = "mixer")] pub use self::mixer::*; -#[cfg(feature = "pool")] mod pool; #[cfg(feature = "pool")] pub use self::pool::*; -#[cfg(feature = "port")] mod port; #[cfg(feature = "port")] pub use self::port::*; -#[cfg(feature = "sampler")] mod sampler; #[cfg(feature = "sampler")] pub use self::sampler::*; -#[cfg(feature = "sequencer")] mod sequencer; #[cfg(feature = "sequencer")] pub use self::sequencer::*; -#[cfg(feature = "sf2")] mod sf2; #[cfg(feature = "sf2")] pub use self::sf2::*; -#[cfg(feature = "vst2")] mod vst2; #[cfg(feature = "vst2")] pub use self::vst2::*; -#[cfg(feature = "vst3")] mod vst3; #[cfg(feature = "vst3")] pub use self::vst3::*; - -pub fn swap_value ( - target: &mut T, value: &T, returned: impl Fn(T)->U -) -> Perhaps { - if *target == *value { - Ok(None) - } else { - let mut value = value.clone(); - std::mem::swap(target, &mut value); - Ok(Some(returned(value))) - } -} - -pub fn toggle_bool ( - target: &mut bool, value: &Option, returned: impl Fn(Option)->U -) -> Perhaps { - let mut value = value.unwrap_or(!*target); - if value == *target { - Ok(None) - } else { - std::mem::swap(target, &mut value); - Ok(Some(returned(Some(value)))) - } -} diff --git a/tek/device/lv2.rs b/tek/device/lv2.rs deleted file mode 100644 index 4610b30b..00000000 --- a/tek/device/lv2.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod lv2_model; pub use self::lv2_model::*; -mod lv2_audio; //pub use self::lv2_audio::*; -mod lv2_gui; pub use self::lv2_gui::*; -mod lv2_tui; //pub use self::lv2_tui::*; -//pub(self) use std::thread::JoinHandle; diff --git a/tek/device/lv2/lv2_audio.rs b/tek/device/lv2/lv2_audio.rs deleted file mode 100644 index 9f33d9e6..00000000 --- a/tek/device/lv2/lv2_audio.rs +++ /dev/null @@ -1,50 +0,0 @@ -use crate::*; -use super::*; - -audio!(|self: Lv2, _client, scope|{ - let Self { - midi_ins, - midi_outs, - audio_ins, - audio_outs, - lv2_features, - lv2_instance, - lv2_input_buffer, - .. - } = self; - let urid = lv2_features.midi_urid(); - lv2_input_buffer.clear(); - for port in midi_ins.iter() { - let mut atom = ::livi::event::LV2AtomSequence::new( - &lv2_features, - scope.n_frames() as usize - ); - for event in port.iter(scope) { - match event.bytes.len() { - 3 => atom.push_midi_event::<3>( - event.time as i64, - urid, - &event.bytes[0..3] - ).unwrap(), - _ => {} - } - } - lv2_input_buffer.push(atom); - } - let mut outputs = vec![]; - for _ in midi_outs.iter() { - outputs.push(::livi::event::LV2AtomSequence::new( - lv2_features, - scope.n_frames() as usize - )); - } - let ports = ::livi::EmptyPortConnections::new() - .with_atom_sequence_inputs(lv2_input_buffer.iter()) - .with_atom_sequence_outputs(outputs.iter_mut()) - .with_audio_inputs(audio_ins.iter().map(|o|o.as_slice(scope))) - .with_audio_outputs(audio_outs.iter_mut().map(|o|o.as_mut_slice(scope))); - unsafe { - lv2_instance.run(scope.n_frames() as usize, ports).unwrap() - }; - Control::Continue -}); diff --git a/tek/device/lv2/lv2_gui.rs b/tek/device/lv2/lv2_gui.rs deleted file mode 100644 index a296eee5..00000000 --- a/tek/device/lv2/lv2_gui.rs +++ /dev/null @@ -1,58 +0,0 @@ -use crate::*; -use std::thread::{spawn, JoinHandle}; -use ::winit::{ - application::ApplicationHandler, - event::WindowEvent, - event_loop::{ActiveEventLoop, ControlFlow, EventLoop}, - window::{Window, WindowId}, - platform::x11::EventLoopBuilderExtX11 -}; - -//pub struct LV2PluginUI { - //write: (), - //controller: (), - //widget: (), - //features: (), - //transfer: (), -//} - -pub fn run_lv2_ui (mut ui: LV2PluginUI) -> Usually> { - Ok(spawn(move||{ - let event_loop = EventLoop::builder().with_x11().with_any_thread(true).build().unwrap(); - event_loop.set_control_flow(ControlFlow::Wait); - event_loop.run_app(&mut ui).unwrap() - })) -} - -/// A LV2 plugin's X11 UI. -pub struct LV2PluginUI { - pub window: Option -} - -impl LV2PluginUI { - pub fn new () -> Usually { - Ok(Self { window: None }) - } -} - -impl ApplicationHandler for LV2PluginUI { - fn resumed (&mut self, event_loop: &ActiveEventLoop) { - self.window = Some(event_loop.create_window(Window::default_attributes()).unwrap()); - } - fn window_event (&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) { - match event { - WindowEvent::CloseRequested => { - self.window.as_ref().unwrap().set_visible(false); - event_loop.exit(); - }, - WindowEvent::RedrawRequested => { - self.window.as_ref().unwrap().request_redraw(); - } - _ => (), - } - } -} - -fn lv2_ui_instantiate (kind: &str) { - //let host = Suil -} diff --git a/tek/device/lv2/lv2_model.rs b/tek/device/lv2/lv2_model.rs deleted file mode 100644 index bbbe0663..00000000 --- a/tek/device/lv2/lv2_model.rs +++ /dev/null @@ -1,85 +0,0 @@ -use crate::*; - -/// A LV2 plugin. -#[derive(Debug)] -pub struct Lv2 { - /// JACK client handle (needs to not be dropped for standalone mode to work). - pub jack: Jack<'static>, - pub name: Arc, - pub path: Option>, - pub selected: usize, - pub mapping: bool, - pub midi_ins: Vec>, - pub midi_outs: Vec>, - pub audio_ins: Vec>, - pub audio_outs: Vec>, - - pub lv2_world: livi::World, - pub lv2_instance: livi::Instance, - pub lv2_plugin: livi::Plugin, - pub lv2_features: Arc, - pub lv2_port_list: Vec, - pub lv2_input_buffer: Vec, - pub lv2_ui_thread: Option>, -} - -impl Lv2 { - - pub fn new ( - jack: &Jack<'static>, - name: &str, - uri: &str, - ) -> Usually { - let lv2_world = livi::World::with_load_bundle(&uri); - let lv2_features = lv2_world.build_features(livi::FeaturesBuilder { - min_block_length: 1, - max_block_length: 65536, - }); - let lv2_plugin = lv2_world.iter_plugins().nth(0) - .unwrap_or_else(||panic!("plugin not found: {uri}")); - Ok(Self { - jack: jack.clone(), - name: name.into(), - path: Some(String::from(uri).into()), - selected: 0, - mapping: false, - midi_ins: vec![], - midi_outs: vec![], - audio_ins: vec![], - audio_outs: vec![], - lv2_instance: unsafe { - lv2_plugin - .instantiate(lv2_features.clone(), 48000.0) - .expect(&format!("instantiate failed: {uri}")) - }, - lv2_port_list: lv2_plugin.ports().collect::>(), - lv2_input_buffer: Vec::with_capacity(Self::INPUT_BUFFER), - lv2_ui_thread: None, - lv2_world, - lv2_features, - lv2_plugin, - }) - } - - const INPUT_BUFFER: usize = 1024; - -} - - - //fn jack_from_lv2 (name: &str, plugin: &::livi::Plugin) -> Usually { - //let counts = plugin.port_counts(); - //let mut jack = Jack::new(name)?; - //for i in 0..counts.atom_sequence_inputs { - //jack = jack.midi_in(&format!("midi-in-{i}")) - //} - //for i in 0..counts.atom_sequence_outputs { - //jack = jack.midi_out(&format!("midi-out-{i}")); - //} - //for i in 0..counts.audio_inputs { - //jack = jack.audio_in(&format!("audio-in-{i}")); - //} - //for i in 0..counts.audio_outputs { - //jack = jack.audio_out(&format!("audio-out-{i}")); - //} - //Ok(jack) - //} diff --git a/tek/device/lv2/lv2_tui.rs b/tek/device/lv2/lv2_tui.rs deleted file mode 100644 index 8bfad8a2..00000000 --- a/tek/device/lv2/lv2_tui.rs +++ /dev/null @@ -1,124 +0,0 @@ -use crate::*; -use super::*; - -impl Draw for Lv2 { - fn draw (&self, to: &mut TuiOut) { - let area = to.area(); - let [x, y, _, height] = area; - let mut width = 20u16; - let start = self.selected.saturating_sub((height as usize / 2).saturating_sub(1)); - let end = start + height as usize - 2; - //draw_box(buf, Rect { x, y, width, height }); - for i in start..end { - if let Some(port) = self.lv2_port_list.get(i) { - let value = if let Some(value) = self.lv2_instance.control_input(port.index) { - value - } else { - port.default_value - }; - //let label = &format!("Cยทยท Mยทยท {:25} = {value:.03}", port.name); - let label = &format!("{:25} = {value:.03}", port.name); - width = width.max(label.len() as u16 + 4); - let style = if i == self.selected { - Some(Style::default().green()) - } else { - None - } ; - to.blit(&label, x + 2, y + 1 + i as u16 - start as u16, style); - } else { - break - } - } - draw_header(self, to, x, y, width); - } -} - -fn draw_header (state: &Lv2, to: &mut TuiOut, x: u16, y: u16, w: u16) { - let style = Style::default().gray(); - let label1 = format!(" {}", state.name); - to.blit(&label1, x + 1, y, Some(style.white().bold())); - if let Some(ref path) = state.path { - let label2 = format!("{}โ€ฆ", &path[..((w as usize - 10).min(path.len()))]); - to.blit(&label2, x + 2 + label1.len() as u16, y, Some(style.not_dim())); - } - //Ok(Rect { x, y, width: w, height: 1 }) -} - -//handle!(TuiIn: |self:Plugin, from|{ - //match from.event() { - //kpat!(KeyCode::Up) => { - //self.selected = self.selected.saturating_sub(1); - //Ok(Some(true)) - //}, - //kpat!(KeyCode::Down) => { - //self.selected = (self.selected + 1).min(match &self.plugin { - //Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1, - //_ => unimplemented!() - //}); - //Ok(Some(true)) - //}, - //kpat!(KeyCode::PageUp) => { - //self.selected = self.selected.saturating_sub(8); - //Ok(Some(true)) - //}, - //kpat!(KeyCode::PageDown) => { - //self.selected = (self.selected + 10).min(match &self.plugin { - //Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1, - //_ => unimplemented!() - //}); - //Ok(Some(true)) - //}, - //kpat!(KeyCode::Char(',')) => { - //match self.plugin.as_mut() { - //Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => { - //let index = port_list[self.selected].index; - //if let Some(value) = instance.control_input(index) { - //instance.set_control_input(index, value - 0.01); - //} - //}, - //_ => {} - //} - //Ok(Some(true)) - //}, - //kpat!(KeyCode::Char('.')) => { - //match self.plugin.as_mut() { - //Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => { - //let index = port_list[self.selected].index; - //if let Some(value) = instance.control_input(index) { - //instance.set_control_input(index, value + 0.01); - //} - //}, - //_ => {} - //} - //Ok(Some(true)) - //}, - //kpat!(KeyCode::Char('g')) => { - //match self.plugin { - ////Some(PluginKind::LV2(ref mut plugin)) => { - ////plugin.ui_thread = Some(run_lv2_ui(LV2PluginUI::new()?)?); - ////}, - //Some(_) => unreachable!(), - //None => {} - //} - //Ok(Some(true)) - //}, - //_ => Ok(None) - //} -//}); - -//from_atom!("plugin/lv2" => |jack: &Jack, args| -> Plugin { - //let mut name = String::new(); - //let mut path = String::new(); - //atom!(atom in args { - //Atom::Map(map) => { - //if let Some(Atom::Str(n)) = map.get(&Atom::Key(":name")) { - //name = String::from(*n); - //} - //if let Some(Atom::Str(p)) = map.get(&Atom::Key(":path")) { - //path = String::from(*p); - //} - //}, - //_ => panic!("unexpected in lv2 '{name}'"), - //}); - //Plugin::new_lv2(jack, &name, &path) -//}); diff --git a/tek/device/pool.rs b/tek/device/pool.rs deleted file mode 100644 index 90e7e490..00000000 --- a/tek/device/pool.rs +++ /dev/null @@ -1,213 +0,0 @@ -use crate::*; -mod pool_api; pub use self::pool_api::*; -mod pool_view; pub use self::pool_view::*; - -#[derive(Debug)] -pub struct Pool { - pub visible: bool, - /// Collection of clips - pub clips: Arc>>>>, - /// Selected clip - pub clip: AtomicUsize, - /// Mode switch - pub mode: Option, - /// Embedded file browse - pub browse: Option, -} - -//take!(BrowseCommand |state: Pool, iter|Ok(state.browse.as_ref() - //.map(|p|Take::take(p, iter)) - //.transpose()? - //.flatten())); - -impl Default for Pool { - fn default () -> Self { - //use PoolMode::*; - Self { - visible: true, - clips: Arc::from(RwLock::from(vec![])), - clip: 0.into(), - mode: None, - browse: None, - } - } -} - -impl Pool { - pub fn clip_index (&self) -> usize { - self.clip.load(Relaxed) - } - pub fn set_clip_index (&self, value: usize) { - self.clip.store(value, Relaxed); - } - pub fn mode (&self) -> &Option { - &self.mode - } - pub fn mode_mut (&mut self) -> &mut Option { - &mut self.mode - } - pub fn begin_clip_length (&mut self) { - let length = self.clips()[self.clip_index()].read().unwrap().length; - *self.mode_mut() = Some(PoolMode::Length( - self.clip_index(), - length, - ClipLengthFocus::Bar - )); - } - pub fn begin_clip_rename (&mut self) { - let name = self.clips()[self.clip_index()].read().unwrap().name.clone(); - *self.mode_mut() = Some(PoolMode::Rename( - self.clip_index(), - name - )); - } - pub fn begin_import (&mut self) -> Usually<()> { - *self.mode_mut() = Some(PoolMode::Import( - self.clip_index(), - Browse::new(None)? - )); - Ok(()) - } - pub fn begin_export (&mut self) -> Usually<()> { - *self.mode_mut() = Some(PoolMode::Export( - self.clip_index(), - Browse::new(None)? - )); - Ok(()) - } - pub fn new_clip (&self) -> MidiClip { - MidiClip::new("Clip", true, 4 * PPQ, None, Some(ItemTheme::random())) - } - pub fn cloned_clip (&self) -> MidiClip { - let index = self.clip_index(); - let mut clip = self.clips()[index].read().unwrap().duplicate(); - clip.color = ItemTheme::random_near(clip.color, 0.25); - clip - } - pub fn add_new_clip (&self) -> (usize, Arc>) { - let clip = Arc::new(RwLock::new(self.new_clip())); - let index = { - let mut clips = self.clips.write().unwrap(); - clips.push(clip.clone()); - clips.len().saturating_sub(1) - }; - self.clip.store(index, Relaxed); - (index, clip) - } - pub fn delete_clip (&mut self, clip: &MidiClip) -> bool { - let index = self.clips.read().unwrap().iter().position(|x|*x.read().unwrap()==*clip); - if let Some(index) = index { - self.clips.write().unwrap().remove(index); - return true - } - false - } -} - -/// Modes for clip pool -#[derive(Debug, Clone)] -pub enum PoolMode { - /// Renaming a pattern - Rename(usize, Arc), - /// Editing the length of a pattern - Length(usize, usize, ClipLengthFocus), - /// Load clip from disk - Import(usize, Browse), - /// Save clip to disk - Export(usize, Browse), -} - -/// Focused field of `ClipLength` -#[derive(Copy, Clone, Debug)] -pub enum ClipLengthFocus { - /// Editing the number of bars - Bar, - /// Editing the number of beats - Beat, - /// Editing the number of ticks - Tick, -} - -impl ClipLengthFocus { - pub fn next (&mut self) { - use ClipLengthFocus::*; - *self = match self { Bar => Beat, Beat => Tick, Tick => Bar, } - } - pub fn prev (&mut self) { - use ClipLengthFocus::*; - *self = match self { Bar => Tick, Beat => Bar, Tick => Beat, } - } -} - -/// Displays and edits clip length. -#[derive(Clone)] -pub struct ClipLength { - /// Pulses per beat (quaver) - ppq: usize, - /// Beats per bar - bpb: usize, - /// Length of clip in pulses - pulses: usize, - /// Selected subdivision - pub focus: Option, -} - -impl ClipLength { - pub fn _new (pulses: usize, focus: Option) -> Self { - Self { ppq: PPQ, bpb: 4, pulses, focus } - } - pub fn bars (&self) -> usize { - self.pulses / (self.bpb * self.ppq) - } - pub fn beats (&self) -> usize { - (self.pulses % (self.bpb * self.ppq)) / self.ppq - } - pub fn ticks (&self) -> usize { - self.pulses % self.ppq - } - pub fn bars_string (&self) -> Arc { - format!("{}", self.bars()).into() - } - pub fn beats_string (&self) -> Arc { - format!("{}", self.beats()).into() - } - pub fn ticks_string (&self) -> Arc { - format!("{:>02}", self.ticks()).into() - } -} - -pub type ClipPool = Vec>>; - -pub trait HasClips { - fn clips <'a> (&'a self) -> std::sync::RwLockReadGuard<'a, ClipPool>; - fn clips_mut <'a> (&'a self) -> std::sync::RwLockWriteGuard<'a, ClipPool>; - fn add_clip (&self) -> (usize, Arc>) { - let clip = Arc::new(RwLock::new(MidiClip::new("Clip", true, 384, None, None))); - self.clips_mut().push(clip.clone()); - (self.clips().len() - 1, clip) - } -} - -#[macro_export] macro_rules! has_clips { - (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? { - fn clips <'a> (&'a $self) -> std::sync::RwLockReadGuard<'a, ClipPool> { - $cb.read().unwrap() - } - fn clips_mut <'a> (&'a $self) -> std::sync::RwLockWriteGuard<'a, ClipPool> { - $cb.write().unwrap() - } - } - } -} - -has_clips!(|self: Pool|self.clips); - -has_clip!(|self: Pool|self.clips().get(self.clip_index()).map(|c|c.clone())); - -from!(|clip:&Arc>|Pool = { - let model = Self::default(); - model.clips.write().unwrap().push(clip.clone()); - model.clip.store(1, Relaxed); - model -}); diff --git a/tek/device/pool/pool_api.rs b/tek/device/pool/pool_api.rs deleted file mode 100644 index 001b90d9..00000000 --- a/tek/device/pool/pool_api.rs +++ /dev/null @@ -1,207 +0,0 @@ -use crate::*; - -impl Pool { - fn _todo_usize_ (&self) -> usize { todo!() } - fn _todo_bool_ (&self) -> bool { todo!() } - fn _todo_clip_ (&self) -> MidiClip { todo!() } - fn _todo_path_ (&self) -> PathBuf { todo!() } - fn _todo_color_ (&self) -> ItemColor { todo!() } - fn _todo_str_ (&self) -> Arc { todo!() } - fn clip_new (&self) -> MidiClip { self.new_clip() } - fn clip_cloned (&self) -> MidiClip { self.cloned_clip() } - fn clip_index_current (&self) -> usize { 0 } - fn clip_index_after (&self) -> usize { 0 } - fn clip_index_previous (&self) -> usize { 0 } - fn clip_index_next (&self) -> usize { 0 } - fn color_random (&self) -> ItemColor { ItemColor::random() } -} - -def_command!(PoolCommand: |pool: Pool| { - // Toggle visibility of pool - Show { visible: bool } => { - pool.visible = *visible; Ok(Some(Self::Show { visible: !visible })) }, - // Select a clip from the clip pool - Select { index: usize } => { - pool.set_clip_index(*index); Ok(None) }, - // Update the contents of the clip pool - Clip { command: PoolClipCommand } => - Ok(command.execute(pool)?.map(|command|Self::Clip{command})), - // Rename a clip - Rename { command: RenameCommand } => - Ok(command.delegate(pool, |command|Self::Rename{command})?), - // Change the length of a clip - Length { command: CropCommand } => - Ok(command.delegate(pool, |command|Self::Length{command})?), - // Import from file - Import { command: BrowseCommand } => { - Ok(if let Some(browse) = pool.browse.as_mut() { - command.delegate(browse, |command|Self::Import{command})? - } else { - None }) }, - // Export to file - Export { command: BrowseCommand } => { - Ok(if let Some(browse) = pool.browse.as_mut() { - command.delegate(browse, |command|Self::Export{command})? - } else { - None }) }, -}); - -def_command!(PoolClipCommand: |pool: Pool| { - - Delete { index: usize } => { - let index = *index; - let clip = pool.clips_mut().remove(index).read().unwrap().clone(); - Ok(Some(Self::Add { index, clip })) }, - - Swap { index: usize, other: usize } => { - let index = *index; - let other = *other; - pool.clips_mut().swap(index, other); - Ok(Some(Self::Swap { index, other })) }, - - Export { index: usize, path: PathBuf } => { - todo!("export clip to midi file"); }, - - Add { index: usize, clip: MidiClip } => { - let index = *index; - let mut index = index; - let clip = Arc::new(RwLock::new(clip.clone())); - let mut clips = pool.clips_mut(); - if index >= clips.len() { - index = clips.len(); - clips.push(clip) - } else { - clips.insert(index, clip); - } - Ok(Some(Self::Delete { index })) }, - - Import { index: usize, path: PathBuf } => { - let index = *index; - let bytes = std::fs::read(&path)?; - let smf = Smf::parse(bytes.as_slice())?; - let mut t = 0u32; - let mut events = vec![]; - for track in smf.tracks.iter() { - for event in track.iter() { - t += event.delta.as_int(); - if let TrackEventKind::Midi { channel, message } = event.kind { - events.push((t, channel.as_int(), message)); - } - } - } - let mut clip = MidiClip::new("imported", true, t as usize + 1, None, None); - for event in events.iter() { - clip.notes[event.0 as usize].push(event.2); - } - Ok(Self::Add { index, clip }.execute(pool)?) }, - - SetName { index: usize, name: Arc } => { - let index = *index; - let clip = &mut pool.clips_mut()[index]; - let old_name = clip.read().unwrap().name.clone(); - clip.write().unwrap().name = name.clone(); - Ok(Some(Self::SetName { index, name: old_name })) }, - - SetLength { index: usize, length: usize } => { - let index = *index; - let clip = &mut pool.clips_mut()[index]; - let old_len = clip.read().unwrap().length; - clip.write().unwrap().length = *length; - Ok(Some(Self::SetLength { index, length: old_len })) }, - - SetColor { index: usize, color: ItemColor } => { - let index = *index; - let mut color = ItemTheme::from(*color); - std::mem::swap(&mut color, &mut pool.clips()[index].write().unwrap().color); - Ok(Some(Self::SetColor { index, color: color.base })) }, - -}); - -def_command!(RenameCommand: |pool: Pool| { - Begin => unreachable!(), - - Cancel => { - if let Some(PoolMode::Rename(clip, ref mut old_name)) = pool.mode_mut().clone() { - pool.clips()[clip].write().unwrap().name = old_name.clone().into(); - } - Ok(None) }, - - Confirm => { - if let Some(PoolMode::Rename(_clip, ref mut old_name)) = pool.mode_mut().clone() { - let old_name = old_name.clone(); - *pool.mode_mut() = None; - return Ok(Some(Self::Set { value: old_name })) - } - Ok(None) }, - - Set { value: Arc } => { - if let Some(PoolMode::Rename(clip, ref mut _old_name)) = pool.mode_mut().clone() { - pool.clips()[clip].write().unwrap().name = value.clone(); - } - Ok(None) }, -}); - -def_command!(CropCommand: |pool: Pool| { - Begin => unreachable!(), - - Cancel => { - if let Some(PoolMode::Length(..)) = pool.mode_mut().clone() { - *pool.mode_mut() = None; - } - Ok(None) }, - - Set { length: usize } => { - if let Some(PoolMode::Length(clip, ref mut length, ref mut focus)) - = pool.mode_mut().clone() - { - let old_length; - { - let clip = pool.clips()[clip].clone();//.write().unwrap(); - old_length = Some(clip.read().unwrap().length); - clip.write().unwrap().length = *length; - } - *pool.mode_mut() = None; - return Ok(old_length.map(|length|Self::Set { length })) - } - Ok(None) }, - - Next => { - if let Some(PoolMode::Length(clip, ref mut length, ref mut focus)) - = pool.mode_mut().clone() - { - focus.next() - } - Ok(None) }, - - Prev => { - if let Some(PoolMode::Length(clip, ref mut length, ref mut focus)) - = pool.mode_mut().clone() - { - focus.prev() - } - Ok(None) }, - - Inc => { - if let Some(PoolMode::Length(clip, ref mut length, ref mut focus)) - = pool.mode_mut().clone() - { - match focus { - ClipLengthFocus::Bar => { *length += 4 * PPQ }, - ClipLengthFocus::Beat => { *length += PPQ }, - ClipLengthFocus::Tick => { *length += 1 }, - } - } - Ok(None) }, - - Dec => { - if let Some(PoolMode::Length(clip, ref mut length, ref mut focus)) - = pool.mode_mut().clone() - { - match focus { - ClipLengthFocus::Bar => { *length = length.saturating_sub(4 * PPQ) }, - ClipLengthFocus::Beat => { *length = length.saturating_sub(PPQ) }, - ClipLengthFocus::Tick => { *length = length.saturating_sub(1) }, - } - } - Ok(None) }, -}); diff --git a/tek/device/pool/pool_view.rs b/tek/device/pool/pool_view.rs deleted file mode 100644 index 481a7dd9..00000000 --- a/tek/device/pool/pool_view.rs +++ /dev/null @@ -1,42 +0,0 @@ -use crate::*; - -pub struct PoolView<'a>(pub &'a Pool); - -content!(TuiOut: |self: PoolView<'a>| { - let Self(pool) = self; - //let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||Tui::g(32).into()); - //let on_bg = |x|x;//Bsp::b(Repeat(" "), Tui::bg(color.darkest.rgb, x)); - //let border = |x|x;//Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)).enclose(x); - //let height = pool.clips.read().unwrap().len() as u16; - Fixed::x(20, Fill::y(Align::n(Map::new( - ||pool.clips().clone().into_iter(), - move|clip: Arc>, i: usize|{ - let item_height = 1; - let item_offset = i as u16 * item_height; - let selected = i == pool.clip_index(); - let MidiClip { ref name, color, length, .. } = *clip.read().unwrap(); - let bg = if selected { color.light.rgb } else { color.base.rgb }; - let fg = color.lightest.rgb; - let name = if false { format!(" {i:>3}") } else { format!(" {i:>3} {name}") }; - let length = if false { String::default() } else { format!("{length} ") }; - Fixed::y(1, map_south(item_offset, item_height, Tui::bg(bg, lay!( - Fill::x(Align::w(Tui::fg(fg, Tui::bold(selected, name)))), - Fill::x(Align::e(Tui::fg(fg, Tui::bold(selected, length)))), - Fill::x(Align::w(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "โ–ถ"))))), - Fill::x(Align::e(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "โ—€"))))), - )))) - })))) -}); - -content!(TuiOut: |self: ClipLength| { - use ClipLengthFocus::*; - let bars = ||self.bars_string(); - let beats = ||self.beats_string(); - let ticks = ||self.ticks_string(); - match self.focus { - None => row!(" ", bars(), ".", beats(), ".", ticks()), - Some(Bar) => row!("[", bars(), "]", beats(), ".", ticks()), - Some(Beat) => row!(" ", bars(), "[", beats(), "]", ticks()), - Some(Tick) => row!(" ", bars(), ".", beats(), "[", ticks()), - } -}); diff --git a/tek/device/port.rs b/tek/device/port.rs deleted file mode 100644 index d25ca0c0..00000000 --- a/tek/device/port.rs +++ /dev/null @@ -1,89 +0,0 @@ -use crate::*; - -mod port_api; pub use self::port_api::*; -mod port_connect; pub use self::port_connect::*; -mod port_audio_out; //pub use self::port_audio_out::*; -mod port_audio_in; //pub use self::port_audio_in::*; -mod port_midi_out; pub use self::port_midi_out::*; -mod port_midi_in; pub use self::port_midi_in::*; -pub(crate) use ConnectName::*; -pub(crate) use ConnectScope::*; -pub(crate) use ConnectStatus::*; - -#[derive(Debug)] pub struct AudioInput { - /// Handle to JACK client, for receiving reconnect events. - jack: Jack<'static>, - /// Port name - name: Arc, - /// Port handle. - port: Port, - /// List of ports to connect to. - pub connections: Vec, -} - -#[derive(Debug)] pub struct AudioOutput { - /// Handle to JACK client, for receiving reconnect events. - jack: Jack<'static>, - /// Port name - name: Arc, - /// Port handle. - port: Port, - /// List of ports to connect to. - pub connections: Vec, -} - -#[derive(Debug)] pub struct MidiInput { - /// Handle to JACK client, for receiving reconnect events. - jack: Jack<'static>, - /// Port name - name: Arc, - /// Port handle. - port: Port, - /// List of currently held notes. - held: Arc>, - /// List of ports to connect to. - pub connections: Vec, -} - -#[derive(Debug)] pub struct MidiOutput { - /// Handle to JACK client, for receiving reconnect events. - jack: Jack<'static>, - /// Port name - name: Arc, - /// Port handle. - port: Port, - /// List of ports to connect to. - pub connections: Vec, - /// List of currently held notes. - held: Arc>, - /// Buffer - note_buffer: Vec, - /// Buffer - output_buffer: Vec>>, -} - -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/tek/device/port/port_api.rs b/tek/device/port/port_api.rs deleted file mode 100644 index 7742d743..00000000 --- a/tek/device/port/port_api.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::*; -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!(), -}); diff --git a/tek/device/port/port_audio_in.rs b/tek/device/port/port_audio_in.rs deleted file mode 100644 index 1c36d2fd..00000000 --- a/tek/device/port/port_audio_in.rs +++ /dev/null @@ -1,35 +0,0 @@ -use crate::*; - -impl HasJack<'static> for AudioInput { fn jack (&self) -> &Jack<'static> { &self.jack } } - -impl JackPort for AudioInput { - type Port = AudioIn; - type Pair = AudioOut; - fn port_name (&self) -> &Arc { - &self.name - } - fn port (&self) -> &Port { - &self.port - } - fn port_mut (&mut self) -> &mut Port { - &mut self.port - } - fn into_port (self) -> Port { - self.port - } - fn connections (&self) -> &[Connect] { - self.connections.as_slice() - } - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized - { - let port = Self { - port: Self::register(jack, name)?, - jack: jack.clone(), - name: name.as_ref().into(), - connections: connect.to_vec() - }; - port.connect_to_matching()?; - Ok(port) - } -} diff --git a/tek/device/port/port_audio_out.rs b/tek/device/port/port_audio_out.rs deleted file mode 100644 index 451eb3c8..00000000 --- a/tek/device/port/port_audio_out.rs +++ /dev/null @@ -1,35 +0,0 @@ -use crate::*; - -impl HasJack<'static> for AudioOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } - -impl JackPort for AudioOutput { - type Port = AudioOut; - type Pair = AudioIn; - fn port_name (&self) -> &Arc { - &self.name - } - fn port (&self) -> &Port { - &self.port - } - fn port_mut (&mut self) -> &mut Port { - &mut self.port - } - fn into_port (self) -> Port { - self.port - } - fn connections (&self) -> &[Connect] { - self.connections.as_slice() - } - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized - { - let port = Self { - port: Self::register(jack, name)?, - jack: jack.clone(), - name: name.as_ref().into(), - connections: connect.to_vec() - }; - port.connect_to_matching()?; - Ok(port) - } -} diff --git a/tek/device/port/port_connect.rs b/tek/device/port/port_connect.rs deleted file mode 100644 index d8bb6ad7..00000000 --- a/tek/device/port/port_connect.rs +++ /dev/null @@ -1,183 +0,0 @@ -use crate::*; - -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() { - //panic!("{connect:?}"); - let status = match &connect.name { - Exact(name) => self.connect_exact(name), - RegExp(re) => self.connect_regexp(re, connect.scope), - }?; - *connect.status.write().unwrap() = status; - } - 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: ConnectScope - ) -> 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 == 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 - })) - } -} - -#[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, -} - -#[derive(Clone, Debug)] pub struct Connect { - pub name: ConnectName, - pub scope: ConnectScope, - 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 = Exact(name.as_ref().into()); - Self { name, scope: One, status: Arc::new(RwLock::new(vec![])), info } - } - pub fn regexp (name: impl AsRef) -> Self { - let info = format!("~:{}", name.as_ref()).into(); - let name = RegExp(name.as_ref().into()); - Self { name, scope: 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 = RegExp(name.as_ref().into()); - Self { name, scope: All, status: Arc::new(RwLock::new(vec![])), info } - } - pub fn info (&self) -> Arc { - let status = { - let status = self.status.read().unwrap(); - let mut ok = 0; - for (_, _, state) in status.iter() { - if *state == Connected { - ok += 1 - } - } - format!("{ok}/{}", status.len()) - }; - let scope = match self.scope { - One => " ", All => "*", - }; - let name = match &self.name { - Exact(name) => format!("= {name}"), RegExp(name) => format!("~ {name}"), - }; - format!(" ({}) {} {}", status, scope, name).into() - } -} - diff --git a/tek/device/port/port_midi_in.rs b/tek/device/port/port_midi_in.rs deleted file mode 100644 index a66ba049..00000000 --- a/tek/device/port/port_midi_in.rs +++ /dev/null @@ -1,92 +0,0 @@ -use crate::*; - -impl HasJack<'static> for MidiInput { fn jack (&self) -> &Jack<'static> { &self.jack } } - -impl JackPort for MidiInput { - type Port = MidiIn; - type Pair = MidiOut; - fn port_name (&self) -> &Arc { - &self.name - } - fn port (&self) -> &Port { - &self.port - } - fn port_mut (&mut self) -> &mut Port { - &mut self.port - } - fn into_port (self) -> Port { - self.port - } - fn connections (&self) -> &[Connect] { - self.connections.as_slice() - } - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized - { - let port = Self { - port: Self::register(jack, name)?, - jack: jack.clone(), - name: name.as_ref().into(), - connections: connect.to_vec(), - held: Arc::new(RwLock::new([false;128])) - }; - port.connect_to_matching()?; - Ok(port) - } -} - -impl MidiInput { - pub fn parsed <'a> (&'a self, scope: &'a ProcessScope) -> impl Iterator, &'a [u8])> { - parse_midi_input(self.port().iter(scope)) - } -} - -impl>> HasMidiIns for T { - fn midi_ins (&self) -> &Vec { - self.get() - } - fn midi_ins_mut (&mut self) -> &mut Vec { - self.get_mut() - } -} - -/// 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 - }) - } -} - -pub type CollectedMidiInput<'a> = Vec, MidiError>)>>; - -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(()) - } -} - -/// May create new MIDI input ports. -pub trait AddMidiIn { - fn midi_in_add (&mut self) -> Usually<()>; -} diff --git a/tek/device/port/port_midi_out.rs b/tek/device/port/port_midi_out.rs deleted file mode 100644 index 62a70c41..00000000 --- a/tek/device/port/port_midi_out.rs +++ /dev/null @@ -1,130 +0,0 @@ -use crate::*; - -impl HasJack<'static> for MidiOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } - -impl JackPort for MidiOutput { - type Port = MidiOut; - type Pair = MidiIn; - fn port_name (&self) -> &Arc { - &self.name - } - fn port (&self) -> &Port { - &self.port - } - fn port_mut (&mut self) -> &mut Port { - &mut self.port - } - fn into_port (self) -> Port { - self.port - } - fn connections (&self) -> &[Connect] { - self.connections.as_slice() - } - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized - { - let port = Self::register(jack, name)?; - let jack = jack.clone(); - let name = name.as_ref().into(); - let connections = connect.to_vec(); - let port = Self { - jack, - port, - name, - connections, - held: Arc::new([false;128].into()), - note_buffer: vec![0;8], - output_buffer: vec![vec![];65536], - }; - port.connect_to_matching()?; - Ok(port) - } -} - -impl MidiOutput { - /// Clear the section of the output buffer that we will be using, - /// emitting "all notes off" at start of buffer if requested. - pub fn buffer_clear (&mut self, scope: &ProcessScope, reset: bool) { - let n_frames = (scope.n_frames() as usize).min(self.output_buffer.len()); - for frame in &mut self.output_buffer[0..n_frames] { - frame.clear(); - } - if reset { - all_notes_off(&mut self.output_buffer); - } - } - /// Write a note to the output buffer - pub fn buffer_write <'a> ( - &'a mut self, - sample: usize, - event: LiveEvent, - ) { - self.note_buffer.fill(0); - event.write(&mut self.note_buffer).expect("failed to serialize MIDI event"); - self.output_buffer[sample].push(self.note_buffer.clone()); - // Update the list of currently held notes. - if let LiveEvent::Midi { ref message, .. } = event { - update_keys(&mut*self.held.write().unwrap(), message); - } - } - /// Write a chunk of MIDI data from the output buffer to the output port. - pub fn buffer_emit (&mut self, scope: &ProcessScope) { - let samples = scope.n_frames() as usize; - let mut writer = self.port.writer(scope); - for (time, events) in self.output_buffer.iter().enumerate().take(samples) { - for bytes in events.iter() { - writer.write(&RawMidi { time: time as u32, bytes }).unwrap_or_else(|_|{ - panic!("Failed to write MIDI data: {bytes:?}"); - }); - } - } - } -} - - -impl>> HasMidiOuts for T { - fn midi_outs (&self) -> &Vec { - self.get() - } - fn midi_outs_mut (&mut self) -> &mut Vec { - self.get_mut() - } -} - - -/// 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) - } - } -} - -/// 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 output ports. -pub trait AddMidiOut { - fn midi_out_add (&mut self) -> Usually<()>; -} diff --git a/tek/device/sampler.rs b/tek/device/sampler.rs deleted file mode 100644 index be89a841..00000000 --- a/tek/device/sampler.rs +++ /dev/null @@ -1,200 +0,0 @@ -use crate::*; - -pub(crate) use symphonia::{ - core::{ - formats::Packet, - codecs::{Decoder, CODEC_TYPE_NULL}, - //errors::Error as SymphoniaError, - io::MediaSourceStream, - probe::Hint, - audio::SampleBuffer, - }, - default::get_codecs, -}; - -mod sampler_api; pub use self::sampler_api::*; -mod sampler_audio; -mod sampler_browse; pub use self::sampler_browse::*; -mod sampler_midi; pub use self::sampler_midi::*; - -mod sampler_data; -mod sampler_view; - -#[cfg(test)] #[test] fn test_sampler () { - // TODO! - let sample = Sample::new("test", 0, 0, vec![]); -} - -/// The sampler device plays sounds in response to MIDI notes. -#[derive(Debug)] -pub struct Sampler { - /// Name of sampler. - pub name: Arc, - /// Device color. - pub color: ItemTheme, - /// Audio input ports. Samples get recorded here. - pub audio_ins: Vec, - /// Audio input meters. - pub input_meters: Vec, - /// Sample currently being recorded. - pub recording: Option<(usize, Option>>)>, - /// Recording buffer. - pub buffer: Vec>, - /// Samples mapped to MIDI notes. - pub mapped: [Option>>;128], - /// Samples that are not mapped to MIDI notes. - pub unmapped: Vec>>, - /// Sample currently being edited. - pub editing: Option>>, - /// MIDI input port. Triggers sample playback. - pub midi_in: MidiInput, - /// Collection of currently playing instances of samples. - pub voices: Arc>>, - /// Audio output ports. Voices get played here. - pub audio_outs: Vec, - /// Audio output meters. - pub output_meters: Vec, - /// How to mix the voices. - pub mixing_mode: MixingMode, - /// How to meter the inputs and outputs. - pub metering_mode: MeteringMode, - /// Fixed gain applied to all output. - pub output_gain: f32, - /// Currently active modal, if any. - pub mode: Option, - /// Size of rendered sampler. - pub size: Measure, - /// Lowest note displayed. - pub note_lo: AtomicUsize, - /// Currently selected note. - pub note_pt: AtomicUsize, - /// Selected note as row/col. - pub cursor: (AtomicUsize, AtomicUsize), -} - -impl Sampler { - pub fn new ( - jack: &Jack<'static>, - name: impl AsRef, - midi_from: &[Connect], - audio_from: &[&[Connect];2], - audio_to: &[&[Connect];2], - ) -> Usually { - let name = name.as_ref(); - Ok(Self { - name: name.into(), - midi_in: MidiInput::new(jack, &format!("M/{name}"), midi_from)?, - audio_ins: vec![ - AudioInput::new(jack, &format!("L/{name}"), audio_from[0])?, - AudioInput::new(jack, &format!("R/{name}"), audio_from[1])?, - ], - audio_outs: vec![ - AudioOutput::new(jack, &format!("{name}/L"), audio_to[0])?, - AudioOutput::new(jack, &format!("{name}/R"), audio_to[1])?, - ], - input_meters: vec![0.0;2], - output_meters: vec![0.0;2], - mapped: [const { None };128], - unmapped: vec![], - voices: Arc::new(RwLock::new(vec![])), - buffer: vec![vec![0.0;16384];2], - output_gain: 1., - recording: None, - mode: None, - editing: None, - size: Default::default(), - note_lo: 0.into(), - note_pt: 0.into(), - cursor: (0.into(), 0.into()), - color: Default::default(), - mixing_mode: Default::default(), - metering_mode: Default::default(), - }) - } - /// Value of cursor - pub fn cursor (&self) -> (usize, usize) { - (self.cursor.0.load(Relaxed), self.cursor.1.load(Relaxed)) - } -} - -impl NoteRange for Sampler { - fn note_lo (&self) -> &AtomicUsize { - &self.note_lo - } - fn note_axis (&self) -> &AtomicUsize { - &self.size.y - } -} - -impl NotePoint for Sampler { - fn note_len (&self) -> &AtomicUsize { - unreachable!(); - } - fn get_note_len (&self) -> usize { - 0 - } - fn set_note_len (&self, x: usize) -> usize { - 0 /*TODO?*/ - } - fn note_pos (&self) -> &AtomicUsize { - &self.note_pt - } - fn get_note_pos (&self) -> usize { - self.note_pt.load(Relaxed) - } - fn set_note_pos (&self, x: usize) -> usize { - let old = self.note_pt.swap(x, Relaxed); - self.cursor.0.store(x % 8, Relaxed); - self.cursor.1.store(x / 8, Relaxed); - old - } -} - -/// A sound sample. -#[derive(Default, Debug)] -pub struct Sample { - pub name: Arc, - pub start: usize, - pub end: usize, - pub channels: Vec>, - pub rate: Option, - pub gain: f32, - pub color: ItemTheme, -} - -impl Sample { - pub fn new (name: impl AsRef, start: usize, end: usize, channels: Vec>) -> Self { - Self { - name: name.as_ref().into(), - start, - end, - channels, - rate: None, - gain: 1.0, - color: ItemTheme::random(), - } - } - pub fn play (sample: &Arc>, after: usize, velocity: &u7) -> Voice { - Voice { - sample: sample.clone(), - after, - position: sample.read().unwrap().start, - velocity: velocity.as_int() as f32 / 127.0, - } - } -} - -/// A currently playing instance of a sample. -#[derive(Default, Debug, Clone)] -pub struct Voice { - pub sample: Arc>, - pub after: usize, - pub position: usize, - pub velocity: f32, -} - -#[derive(Debug)] -pub enum SamplerMode { - // Load sample from path - Import(usize, Browse), -} diff --git a/tek/device/sampler/sampler_api.rs b/tek/device/sampler/sampler_api.rs deleted file mode 100644 index b7308d36..00000000 --- a/tek/device/sampler/sampler_api.rs +++ /dev/null @@ -1,185 +0,0 @@ -use crate::*; - -def_command!(SamplerCommand: |sampler: Sampler| { - RecordToggle { slot: usize } => { - let slot = *slot; - let recording = sampler.recording.as_ref().map(|x|x.0); - let _ = Self::RecordFinish.execute(sampler)?; - // autoslice: continue recording at next slot - if recording != Some(slot) { - Self::RecordBegin { slot }.execute(sampler) - } else { - Ok(None) - } - }, - RecordBegin { slot: usize } => { - let slot = *slot; - sampler.recording = Some(( - slot, - Some(Arc::new(RwLock::new(Sample::new( - "Sample", 0, 0, vec![vec![];sampler.audio_ins.len()] - )))) - )); - Ok(None) - }, - RecordFinish => { - let _prev_sample = sampler.recording.as_mut().map(|(index, sample)|{ - std::mem::swap(sample, &mut sampler.mapped[*index]); - sample - }); // TODO: undo - Ok(None) - }, - RecordCancel => { - sampler.recording = None; - Ok(None) - }, - PlaySample { slot: usize } => { - let slot = *slot; - if let Some(ref sample) = sampler.mapped[slot] { - sampler.voices.write().unwrap().push(Sample::play(sample, 0, &u7::from(128))); - } - Ok(None) - }, - StopSample { slot: usize } => { - let slot = *slot; - todo!(); - Ok(None) - }, -}); - -def_command!(FileBrowserCommand: |sampler: Sampler|{ - //("begin" [] Some(Self::Begin)) - //("cancel" [] Some(Self::Cancel)) - //("confirm" [] Some(Self::Confirm)) - //("select" [i: usize] Some(Self::Select(i.expect("no index")))) - //("chdir" [p: PathBuf] Some(Self::Chdir(p.expect("no path")))) - //("filter" [f: Arc] Some(Self::Filter(f.expect("no filter"))))) -}); - -impl Sampler { - fn sample_selected (&self) -> usize { - (self.get_note_pos() as u8).into() - } - fn sample_selected_pitch (&self) -> u7 { - (self.get_note_pos() as u8).into() - } - //fn file_browser_filter (&self) -> Arc { - //todo!() - //} - //fn file_browser_path (&self) -> PathBuf { - //todo!(); - //} - ///// Immutable reference to sample at cursor. - //fn sample_selected (&self) -> Option>> { - //for (i, sample) in self.mapped.iter().enumerate() { - //if i == self.cursor().0 { - //return sample.as_ref() - //} - //} - //for (i, sample) in self.unmapped.iter().enumerate() { - //if i + self.mapped.len() == self.cursor().0 { - //return Some(sample) - //} - //} - //None - //} - //fn sample_gain (&self) -> f32 { - //todo!() - //} - //fn sample_above () -> usize { - //self.note_pos().min(119) + 8 - //} - //fn sample_below () -> usize { - //self.note_pos().max(8) - 8 - //} - //fn sample_to_left () -> usize { - //self.note_pos().min(126) + 1 - //} - //fn sample_to_right () -> usize { - //self.note_pos().max(1) - 1 - //} - //fn selected_pitch () -> u7 { - //(self.note_pos() as u8).into() // TODO - //} -} - - //select (&self, state: &mut Sampler, i: usize) -> Option { - //Self::Select(state.set_note_pos(i)) - //} - ///// Assign sample to slot - //set (&self, slot: u7, sample: Option>>) -> Option { - //let i = slot.as_int() as usize; - //let old = self.mapped[i].clone(); - //self.mapped[i] = sample; - //Some(Self::Set(old)) - //} - //set_start (&self, state: &mut Sampler, slot: u7, frame: usize) -> Option { - //todo!() - //} - //set_gain (&self, state: &mut Sampler, slot: u7, g: f32) -> Option { - //todo!() - //} - //note_on (&self, state: &mut Sampler, slot: u7, v: u7) -> Option { - //todo!() - //} - //note_off (&self, state: &mut Sampler, slot: u7) -> Option { - //todo!() - //} - //set_sample (&self, state: &mut Sampler, slot: u7, s: Option>>) -> Option { - //Some(Self::SetSample(p, state.set_sample(p, s))) - //} - //import (&self, state: &mut Sampler, c: FileBrowserCommand) -> Option { - //match c { - //FileBrowserCommand::Begin => { - ////let voices = &state.state.voices; - ////let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); - //state.mode = Some(SamplerMode::Import(0, FileBrowser::new(None)?)); - //None - //}, - //_ => { - //println!("\n\rtodo: import: filebrowser: {c:?}"); - //None - //} - //} - //} - ////(Select [i: usize] Some(Self::Select(state.set_note_pos(i)))) - ////(RecordBegin [p: u7] cmd!(state.begin_recording(p.as_int() as usize))) - ////(RecordCancel [] cmd!(state.cancel_recording())) - ////(RecordFinish [] cmd!(state.finish_recording())) - ////(SetStart [p: u7, frame: usize] cmd_todo!("\n\rtodo: {self:?}")) - ////(SetGain [p: u7, gain: f32] cmd_todo!("\n\rtodo: {self:?}")) - ////(NoteOn [p: u7, velocity: u7] cmd_todo!("\n\rtodo: {self:?}")) - ////(NoteOff [p: u7] cmd_todo!("\n\rtodo: {self:?}")) - ////(SetSample [p: u7, s: Option>>] Some(Self::SetSample(p, state.set_sample(p, s)))) - ////(Import [c: FileBrowserCommand] match c { - ////FileBrowserCommand::Begin => { - //////let voices = &state.state.voices; - //////let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); - ////state.mode = Some(SamplerMode::Import(0, FileBrowser::new(None)?)); - ////None - ////}, - ////_ => { - ////println!("\n\rtodo: import: filebrowser: {c:?}"); - ////None - ////} - ////}))); - ////("import" [,..a] - ////FileBrowserCommand::try_from_expr(state, a).map(Self::Import)) - ////("select" [i: usize] - ////Some(Self::Select(i.expect("no index")))) - ////("record/begin" [i: u7] - ////Some(Self::RecordBegin(i.expect("no index")))) - ////("record/cancel" [] - ////Some(Self::RecordCancel)) - ////("record/finish" [] - ////Some(Self::RecordFinish)) - ////("set/sample" [i: u7, s: Option>>] - ////Some(Self::SetSample(i.expect("no index"), s.expect("no sampler")))) - ////("set/start" [i: u7, s: usize] - ////Some(Self::SetStart(i.expect("no index"), s.expect("no start")))) - ////("set/gain" [i: u7, g: f32] - ////Some(Self::SetGain(i.expect("no index"), g.expect("no gain")))) - ////("note/on" [p: u7, v: u7] - ////Some(Self::NoteOn(p.expect("no slot"), v.expect("no velocity")))) - ////("note/off" [p: u7] - ////Some(Self::NoteOff(p.expect("no slot")))))); diff --git a/tek/device/sampler/sampler_audio.rs b/tek/device/sampler/sampler_audio.rs deleted file mode 100644 index a3083a4e..00000000 --- a/tek/device/sampler/sampler_audio.rs +++ /dev/null @@ -1,127 +0,0 @@ -use crate::*; - -audio!(|self: Sampler, _client, scope|{ - self.process_midi_in(scope); - self.process_audio_out(scope); - self.process_audio_in(scope); - Control::Continue -}); - -impl Sampler { - - pub fn process_audio_in (&mut self, scope: &ProcessScope) { - self.reset_input_meters(); - if self.recording.is_some() { - self.record_into(scope); - } else { - self.update_input_meters(scope); - } - } - - /// Make sure that input meter count corresponds to input channel count - fn reset_input_meters (&mut self) { - let channels = self.audio_ins.len(); - if self.input_meters.len() != channels { - self.input_meters = vec![f32::MIN;channels]; - } - } - - /// Record from inputs to sample - fn record_into (&mut self, scope: &ProcessScope) { - if let Some(ref sample) = self.recording.as_ref().expect("no recording sample").1 { - let mut sample = sample.write().unwrap(); - if sample.channels.len() != self.audio_ins.len() { - panic!("channel count mismatch"); - } - let samples_with_meters = self.audio_ins.iter() - .zip(self.input_meters.iter_mut()) - .zip(sample.channels.iter_mut()); - let mut length = 0; - for ((input, meter), channel) in samples_with_meters { - let slice = input.port().as_slice(scope); - length = length.max(slice.len()); - *meter = to_rms(slice); - channel.extend_from_slice(slice); - } - sample.end += length; - } else { - panic!("tried to record into the void") - } - } - - /// Update input meters - fn update_input_meters (&mut self, scope: &ProcessScope) { - for (input, meter) in self.audio_ins.iter().zip(self.input_meters.iter_mut()) { - let slice = input.port().as_slice(scope); - *meter = to_rms(slice); - } - } - - /// Make sure that output meter count corresponds to input channel count - fn reset_output_meters (&mut self) { - let channels = self.audio_outs.len(); - if self.output_meters.len() != channels { - self.output_meters = vec![f32::MIN;channels]; - } - } - - /// Mix all currently playing samples into the output. - pub fn process_audio_out (&mut self, scope: &ProcessScope) { - self.clear_output_buffer(); - self.populate_output_buffer(scope.n_frames() as usize); - self.write_output_buffer(scope); - } - - /// Zero the output buffer. - fn clear_output_buffer (&mut self) { - for buffer in self.buffer.iter_mut() { - buffer.fill(0.0); - } - } - - /// Write playing voices to output buffer - fn populate_output_buffer (&mut self, frames: usize) { - let Sampler { buffer, voices, output_gain, mixing_mode, .. } = self; - let channel_count = buffer.len(); - match mixing_mode { - MixingMode::Summing => voices.write().unwrap().retain_mut(|voice|{ - mix_summing(buffer.as_mut_slice(), *output_gain, frames, ||voice.next()) - }), - MixingMode::Average => voices.write().unwrap().retain_mut(|voice|{ - mix_average(buffer.as_mut_slice(), *output_gain, frames, ||voice.next()) - }), - } - } - - /// Write output buffer to output ports. - fn write_output_buffer (&mut self, scope: &ProcessScope) { - let Sampler { audio_outs, buffer, .. } = self; - for (i, port) in audio_outs.iter_mut().enumerate() { - let buffer = &buffer[i]; - for (i, value) in port.port_mut().as_mut_slice(scope).iter_mut().enumerate() { - *value = *buffer.get(i).unwrap_or(&0.0); - } - } - } - -} - -impl Iterator for Voice { - type Item = [f32;2]; - fn next (&mut self) -> Option { - if self.after > 0 { - self.after -= 1; - return Some([0.0, 0.0]) - } - let sample = self.sample.read().unwrap(); - if self.position < sample.end { - let position = self.position; - self.position += 1; - return sample.channels[0].get(position).map(|_amplitude|[ - sample.channels[0][position] * self.velocity * sample.gain, - sample.channels[0][position] * self.velocity * sample.gain, - ]) - } - None - } -} diff --git a/tek/device/sampler/sampler_browse.rs b/tek/device/sampler/sampler_browse.rs deleted file mode 100644 index 9d9bf491..00000000 --- a/tek/device/sampler/sampler_browse.rs +++ /dev/null @@ -1,181 +0,0 @@ -use crate::*; - -pub struct AddSampleModal { - exited: bool, - dir: PathBuf, - subdirs: Vec, - files: Vec, - cursor: usize, - offset: usize, - sample: Arc>, - voices: Arc>>, - _search: Option, -} - -impl AddSampleModal { - fn exited (&self) -> bool { - self.exited - } - fn exit (&mut self) { - self.exited = true - } -} - -impl AddSampleModal { - pub fn new ( - sample: &Arc>, - voices: &Arc>> - ) -> Usually { - let dir = std::env::current_dir()?; - let (subdirs, files) = scan(&dir)?; - Ok(Self { - exited: false, - dir, - subdirs, - files, - cursor: 0, - offset: 0, - sample: sample.clone(), - voices: voices.clone(), - _search: None - }) - } - fn rescan (&mut self) -> Usually<()> { - scan(&self.dir).map(|(subdirs, files)|{ - self.subdirs = subdirs; - self.files = files; - }) - } - fn prev (&mut self) { - self.cursor = self.cursor.saturating_sub(1); - } - fn next (&mut self) { - self.cursor = self.cursor + 1; - } - fn try_preview (&mut self) -> Usually<()> { - if let Some(path) = self.cursor_file() { - if let Ok(sample) = Sample::from_file(&path) { - *self.sample.write().unwrap() = sample; - self.voices.write().unwrap().push( - Sample::play(&self.sample, 0, &u7::from(100u8)) - ); - } - //load_sample(&path)?; - //let src = std::fs::File::open(&path)?; - //let mss = MediaSourceStream::new(Box::new(src), Default::default()); - //let mut hint = Hint::new(); - //if let Some(ext) = path.extension() { - //hint.with_extension(&ext.to_string_lossy()); - //} - //let meta_opts: MetadataOptions = Default::default(); - //let fmt_opts: FormatOptions = Default::default(); - //if let Ok(mut probed) = symphonia::default::get_probe() - //.format(&hint, mss, &fmt_opts, &meta_opts) - //{ - //panic!("{:?}", probed.format.metadata()); - //}; - } - Ok(()) - } - fn cursor_dir (&self) -> Option { - if self.cursor < self.subdirs.len() { - Some(self.dir.join(&self.subdirs[self.cursor])) - } else { - None - } - } - fn cursor_file (&self) -> Option { - if self.cursor < self.subdirs.len() { - return None - } - let index = self.cursor.saturating_sub(self.subdirs.len()); - if index < self.files.len() { - Some(self.dir.join(&self.files[index])) - } else { - None - } - } - fn pick (&mut self) -> Usually { - if self.cursor == 0 { - if let Some(parent) = self.dir.parent() { - self.dir = parent.into(); - self.rescan()?; - self.cursor = 0; - return Ok(false) - } - } - if let Some(dir) = self.cursor_dir() { - self.dir = dir; - self.rescan()?; - self.cursor = 0; - return Ok(false) - } - if let Some(path) = self.cursor_file() { - let (end, channels) = read_sample_data(&path.to_string_lossy())?; - let mut sample = self.sample.write().unwrap(); - sample.name = path.file_name().unwrap().to_string_lossy().into(); - sample.end = end; - sample.channels = channels; - return Ok(true) - } - return Ok(false) - } -} - -fn read_sample_data (_: &str) -> Usually<(usize, Vec>)> { - todo!(); -} - -fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { - let (mut subdirs, mut files) = std::fs::read_dir(dir)? - .fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{ - let entry = entry.expect("failed to read drectory entry"); - let meta = entry.metadata().expect("failed to read entry metadata"); - if meta.is_file() { - files.push(entry.file_name()); - } else if meta.is_dir() { - subdirs.push(entry.file_name()); - } - (subdirs, files) - }); - subdirs.sort(); - files.sort(); - Ok((subdirs, files)) -} - -impl Draw for AddSampleModal { - fn draw (&self, _to: &mut TuiOut) { - todo!() - //let area = to.area(); - //to.make_dim(); - //let area = center_box( - //area, - //64.max(area.w().saturating_sub(8)), - //20.max(area.w().saturating_sub(8)), - //); - //to.fill_fg(area, Color::Reset); - //to.fill_bg(area, Nord::bg_lo(true, true)); - //to.fill_char(area, ' '); - //to.blit(&format!("{}", &self.dir.to_string_lossy()), area.x()+2, area.y()+1, Some(Style::default().bold()))?; - //to.blit(&"Select sample:", area.x()+2, area.y()+2, Some(Style::default().bold()))?; - //for (i, (is_dir, name)) in self.subdirs.iter() - //.map(|path|(true, path)) - //.chain(self.files.iter().map(|path|(false, path))) - //.enumerate() - //.skip(self.offset) - //{ - //if i >= area.h() as usize - 4 { - //break - //} - //let t = if is_dir { "๎—พ" } else { "๏’ฅ" }; - //let line = format!("{t} {}", name.to_string_lossy()); - //let line = &line[..line.len().min(area.w() as usize - 4)]; - //to.blit(&line, area.x() + 2, area.y() + 3 + i as u16, Some(if i == self.cursor { - //Style::default().green() - //} else { - //Style::default().white() - //}))?; - //} - //Lozenge(Style::default()).draw(to) - } -} diff --git a/tek/device/sampler/sampler_data.rs b/tek/device/sampler/sampler_data.rs deleted file mode 100644 index 2bca4558..00000000 --- a/tek/device/sampler/sampler_data.rs +++ /dev/null @@ -1,91 +0,0 @@ -use crate::*; - -impl Sample { - - /// Read WAV from file - pub fn read_data (src: &str) -> Usually<(usize, Vec>)> { - let mut channels: Vec> = vec![]; - for channel in wavers::Wav::from_path(src)?.channels() { - channels.push(channel); - } - let mut end = 0; - let mut data: Vec> = vec![]; - for samples in channels.iter() { - let channel = Vec::from(samples.as_ref()); - end = end.max(channel.len()); - data.push(channel); - } - Ok((end, data)) - } - - pub fn from_file (path: &PathBuf) -> Usually { - let name = path.file_name().unwrap().to_string_lossy().into(); - let mut sample = Self { name, ..Default::default() }; - // Use file extension if present - let mut hint = Hint::new(); - if let Some(ext) = path.extension() { - hint.with_extension(&ext.to_string_lossy()); - } - let probed = symphonia::default::get_probe().format( - &hint, - MediaSourceStream::new( - Box::new(File::open(path)?), - Default::default(), - ), - &Default::default(), - &Default::default() - )?; - let mut format = probed.format; - let params = &format.tracks().iter() - .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) - .expect("no tracks found") - .codec_params; - let mut decoder = get_codecs().make(params, &Default::default())?; - loop { - match format.next_packet() { - Ok(packet) => sample.decode_packet(&mut decoder, packet)?, - Err(symphonia::core::errors::Error::IoError(_)) => break decoder.last_decoded(), - Err(err) => return Err(err.into()), - }; - }; - sample.end = sample.channels.iter().fold(0, |l, c|l + c.len()); - Ok(sample) - } - - fn decode_packet ( - &mut self, decoder: &mut Box, packet: Packet - ) -> Usually<()> { - // Decode a packet - let decoded = decoder - .decode(&packet) - .map_err(|e|Box::::from(e))?; - // Determine sample rate - let spec = *decoded.spec(); - if let Some(rate) = self.rate { - if rate != spec.rate as usize { - panic!("sample rate changed"); - } - } else { - self.rate = Some(spec.rate as usize); - } - // Determine channel count - while self.channels.len() < spec.channels.count() { - self.channels.push(vec![]); - } - // Load sample - let mut samples = SampleBuffer::new( - decoded.frames() as u64, - spec - ); - if samples.capacity() > 0 { - samples.copy_interleaved_ref(decoded); - for frame in samples.samples().chunks(spec.channels.count()) { - for (chan, frame) in frame.iter().enumerate() { - self.channels[chan].push(*frame) - } - } - } - Ok(()) - } - -} diff --git a/tek/device/sampler/sampler_midi.rs b/tek/device/sampler/sampler_midi.rs deleted file mode 100644 index f8f3f3d8..00000000 --- a/tek/device/sampler/sampler_midi.rs +++ /dev/null @@ -1,76 +0,0 @@ -use crate::*; - -pub type MidiSample = (Option, Arc>); - -impl Sampler { - - /// Create [Voice]s from [Sample]s in response to MIDI input. - pub fn process_midi_in (&mut self, scope: &ProcessScope) { - let Sampler { midi_in, mapped, voices, .. } = self; - for RawMidi { time, bytes } in midi_in.port().iter(scope) { - if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() { - match message { - MidiMessage::NoteOn { ref key, ref vel } => { - if let Some(ref sample) = mapped[key.as_int() as usize] { - voices.write().unwrap().push(Sample::play(sample, time as usize, vel)); - } - }, - MidiMessage::Controller { controller: _, value: _ } => { - // TODO - } - _ => {} - } - } - } - } - -} - -impl Sample { - pub fn handle_cc (&mut self, controller: u7, value: u7) { - let percentage = value.as_int() as f64 / 127.; - match controller.as_int() { - 20 => { - self.start = (percentage * self.end as f64) as usize; - }, - 21 => { - let length = self.channels[0].len(); - self.end = length.min( - self.start + (percentage * (length as f64 - self.start as f64)) as usize - ); - }, - 22 => { /*attack*/ }, - 23 => { /*decay*/ }, - 24 => { - self.gain = percentage as f32 * 2.0; - }, - 26 => { /* pan */ } - 25 => { /* pitch */ } - _ => {} - } - } -} - -// TODO: -//for port in midi_in.iter() { - //for event in port.iter() { - //match event { - //(time, Ok(LiveEvent::Midi {message, ..})) => match message { - //MidiMessage::NoteOn {ref key, ..} if let Some(editor) = self.editor.as_ref() => { - //editor.set_note_pos(key.as_int() as usize); - //}, - //MidiMessage::Controller {controller, value} if let (Some(editor), Some(sampler)) = ( - //self.editor.as_ref(), - //self.sampler.as_ref(), - //) => { - //// TODO: give sampler its own cursor - //if let Some(sample) = &sampler.mapped[editor.note_pos()] { - //sample.write().unwrap().handle_cc(*controller, *value) - //} - //} - //_ =>{} - //}, - //_ =>{} - //} - //} -//} diff --git a/tek/device/sampler/sampler_view.rs b/tek/device/sampler/sampler_view.rs deleted file mode 100644 index b6331dbd..00000000 --- a/tek/device/sampler/sampler_view.rs +++ /dev/null @@ -1,283 +0,0 @@ -use crate::*; - -impl Sampler { - - pub fn view_grid (&self) -> impl Draw + Layout + use<'_> { - //let cells_x = 8u16; - //let cells_y = 8u16; - //let cell_width = 10u16; - //let cell_height = 2u16; - //let width = cells_x * cell_width; - //let height = cells_y * cell_height; - //let cols = Map::east( - //cell_width, - //move||0..cells_x, - //move|x, _|Map::south( - //cell_height, - //move||0..cells_y, - //move|y, _|self.view_grid_cell("........", x, y, cell_width, cell_height) - //) - //); - //cols - //Thunk::new(|to: &mut TuiOut|{ - //}) - "TODO" - } - - pub fn view_grid_cell <'a> ( - &'a self, name: &'a str, x: u16, y: u16, w: u16, h: u16 - ) -> impl Draw + use<'a> { - let cursor = self.cursor(); - let hi_fg = Color::Rgb(64, 64, 64); - let hi_bg = if y == 0 { Color::Reset } else { Color::Rgb(64, 64, 64) /*prev*/ }; - let tx_fg = if let Some((index, _)) = self.recording - && index % 8 == x as usize - && index / 8 == y as usize - { - Color::Rgb(255, 64, 0) - } else { - Color::Rgb(255, 255, 255) - }; - let tx_bg = if x as usize == cursor.0 && y as usize == cursor.1 { - Color::Rgb(96, 96, 96) - } else { - Color::Rgb(64, 64, 64) - }; - let lo_fg = Color::Rgb(64, 64, 64); - let lo_bg = if y == 7 { Color::Reset } else { tx_bg }; - Fixed::xy(w, h, Bsp::s( - Fixed::y(1, Tui::fg_bg(hi_fg, hi_bg, RepeatH(Phat::<()>::LO))), - Bsp::n( - Fixed::y(1, Tui::fg_bg(lo_fg, lo_bg, RepeatH(Phat::<()>::HI))), - Fill::x(Fixed::y(1, Tui::fg_bg(tx_fg, tx_bg, name))), - ), - )) - } - - const _EMPTY: &[(f64, f64)] = &[(0., 0.), (1., 1.), (2., 2.), (0., 2.), (2., 0.)]; - - pub fn view_list <'a, T: NotePoint + NoteRange> ( - &'a self, compact: bool, editor: &T - ) -> impl Draw + 'a { - let note_lo = editor.get_note_lo(); - let note_pt = editor.get_note_pos(); - let note_hi = editor.get_note_hi(); - Fixed::x(if compact { 4 } else { 12 }, Map::south( - 1, - move||(note_lo..=note_hi).rev(), - move|note, _index| { - //let offset = |a|Push::y(i as u16, Align::n(Fixed::y(1, Fill::x(a)))); - let mut bg = if note == note_pt { Tui::g(64) } else { Color::Reset }; - let mut fg = Tui::g(160); - if let Some(mapped) = &self.mapped[note] { - let sample = mapped.read().unwrap(); - fg = if note == note_pt { - sample.color.lightest.rgb - } else { - Tui::g(224) - }; - bg = if note == note_pt { - sample.color.light.rgb - } else { - sample.color.base.rgb - }; - } - if let Some((index, _)) = self.recording { - if note == index { - bg = if note == note_pt { Color::Rgb(96,24,0) } else { Color::Rgb(64,16,0) }; - fg = Color::Rgb(224,64,32) - } - } - Tui::fg_bg(fg, bg, format!("{note:3} {}", self.view_list_item(note, compact))) - })) - } - - pub fn view_list_item (&self, note: usize, compact: bool) -> String { - if compact { - String::default() - } else { - draw_list_item(&self.mapped[note]) - } - } - - pub fn view_sample (&self, note_pt: usize) -> impl Draw + use<'_> { - Outer(true, Style::default().fg(Tui::g(96))) - .enclose(Fill::xy(draw_viewer(if let Some((_, Some(sample))) = &self.recording { - Some(sample) - } else if let Some(sample) = &self.mapped[note_pt] { - Some(sample) - } else { - None - }))) - } - - pub fn view_sample_info (&self, note_pt: usize) -> impl Draw + use<'_> { - Fill::x(Fixed::y(1, draw_info(if let Some((_, Some(sample))) = &self.recording { - Some(sample) - } else if let Some(sample) = &self.mapped[note_pt] { - Some(sample) - } else { - None - }))) - } - - pub fn view_sample_status (&self, note_pt: usize) -> impl Draw + use<'_> { - Fixed::x(20, draw_info_v(if let Some((_, Some(sample))) = &self.recording { - Some(sample) - } else if let Some(sample) = &self.mapped[note_pt] { - Some(sample) - } else { - None - })) - } - - pub fn view_status (&self, index: usize) -> impl Draw { - draw_status(self.mapped[index].as_ref()) - } - - pub fn view_meters_input (&self) -> impl Draw + use<'_> { - draw_meters(&self.input_meters) - } - - pub fn view_meters_output (&self) -> impl Draw + use<'_> { - draw_meters(&self.output_meters) - } -} - -fn draw_meters (meters: &[f32]) -> impl Draw + use<'_> { - Tui::bg(Black, Fixed::x(2, Map::east(1, ||meters.iter(), |value, _index|{ - Fill::y(RmsMeter(*value)) - }))) -} - -fn draw_list_item (sample: &Option>>) -> String { - if let Some(sample) = sample { - let sample = sample.read().unwrap(); - format!("{:8}", sample.name) - //format!("{:8} {:3} {:6}-{:6}/{:6}", - //sample.name, - //sample.gain, - //sample.start, - //sample.end, - //sample.channels[0].len() - //) - } else { - String::from("........") - } -} - -fn draw_viewer (sample: Option<&Arc>>) -> impl Draw + Layout + use<'_> { - let min_db = -64.0; - Thunk::new(move|to: &mut TuiOut|{ - let [x, y, width, height] = to.area(); - let area = Rect { x, y, width, height }; - if let Some(sample) = &sample { - let sample = sample.read().unwrap(); - let start = sample.start as f64; - let end = sample.end as f64; - let length = end - start; - let step = length / width as f64; - let mut t = start; - let mut lines = vec![]; - while t < end { - let chunk = &sample.channels[0][t as usize..((t + step) as usize).min(sample.end)]; - let total: f32 = chunk.iter().map(|x|x.abs()).sum(); - let count = chunk.len() as f32; - let meter = 10. * (total / count).log10(); - let x = t as f64; - let y = meter as f64; - lines.push(Line::new(x, min_db, x, y, Color::Green)); - t += step / 2.; - } - Canvas::default() - .x_bounds([sample.start as f64, sample.end as f64]) - .y_bounds([min_db, 0.]) - .paint(|ctx| { - for line in lines.iter() { - ctx.draw(line); - } - //FIXME: proportions - //let text = "press record to finish sampling"; - //ctx.print( - //(width - text.len() as u16) as f64 / 2.0, - //height as f64 / 2.0, - //text.red() - //); - }).render(area, &mut to.buffer); - } else { - Canvas::default() - .x_bounds([0.0, width as f64]) - .y_bounds([0.0, height as f64]) - .paint(|_ctx| { - //let text = "press record to begin sampling"; - //ctx.print( - //(width - text.len() as u16) as f64 / 2.0, - //height as f64 / 2.0, - //text.red() - //); - }) - .render(area, &mut to.buffer); - } - }) -} - -fn draw_info (sample: Option<&Arc>>) -> impl Draw + Layout + use<'_> { - When::new(sample.is_some(), Thunk::new(move|to: &mut TuiOut|{ - let sample = sample.unwrap().read().unwrap(); - let theme = sample.color; - to.place(&row!( - FieldH(theme, "Name", format!("{:<10}", sample.name.clone())), - FieldH(theme, "Length", format!("{:<8}", sample.channels[0].len())), - FieldH(theme, "Start", format!("{:<8}", sample.start)), - FieldH(theme, "End", format!("{:<8}", sample.end)), - FieldH(theme, "Trans", "0"), - FieldH(theme, "Gain", format!("{}", sample.gain)), - )) - })) -} - -fn draw_info_v (sample: Option<&Arc>>) -> impl Draw + Layout + use<'_> { - Either::new(sample.is_some(), Thunk::new(move|to: &mut TuiOut|{ - let sample = sample.unwrap().read().unwrap(); - let theme = sample.color; - to.place(&Fixed::x(20, col!( - Fill::x(Align::w(FieldH(theme, "Name ", format!("{:<10}", sample.name.clone())))), - Fill::x(Align::w(FieldH(theme, "Length", format!("{:<8}", sample.channels[0].len())))), - Fill::x(Align::w(FieldH(theme, "Start ", format!("{:<8}", sample.start)))), - Fill::x(Align::w(FieldH(theme, "End ", format!("{:<8}", sample.end)))), - Fill::x(Align::w(FieldH(theme, "Trans ", "0"))), - Fill::x(Align::w(FieldH(theme, "Gain ", format!("{}", sample.gain)))), - ))) - }), Thunk::new(|to: &mut TuiOut|to.place(&Tui::fg(Red, col!( - Tui::bold(true, "ร— No sample."), - "[r] record", - "[Shift-F9] import", - ))))) -} - -fn draw_status (sample: Option<&Arc>>) -> impl Draw + Layout { - Tui::bold(true, Tui::fg(Tui::g(224), sample - .map(|sample|{ - let sample = sample.read().unwrap(); - format!("Sample {}-{}", sample.start, sample.end) - }) - .unwrap_or_else(||"No sample".to_string()))) -} - -fn draw_sample ( - to: &mut TuiOut, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool -) -> Usually { - let style = if focus { Style::default().green() } else { Style::default() }; - if focus { - to.blit(&"๐Ÿฌด", x+1, y, Some(style.bold())); - } - let label1 = format!("{:3} {:12}", - note.map(|n|n.to_string()).unwrap_or(String::default()), - sample.name); - let label2 = format!("{:>6} {:>6} +0.0", - sample.start, - sample.end); - to.blit(&label1, x+2, y, Some(style.bold())); - to.blit(&label2, x+3+label1.len()as u16, y, Some(style)); - Ok(label1.len() + label2.len() + 4) -} diff --git a/tek/device/sequencer.rs b/tek/device/sequencer.rs deleted file mode 100644 index 3b6d9fee..00000000 --- a/tek/device/sequencer.rs +++ /dev/null @@ -1,28 +0,0 @@ -mod seq_audio; pub use self::seq_audio::*; -mod seq_clip; pub use self::seq_clip::*; -mod seq_launch; pub use self::seq_launch::*; -mod seq_model; pub use self::seq_model::*; -mod seq_view; pub use self::seq_view::*; - -#[cfg(test)] #[test] pub fn test_midi_clip () { - use crate::*; - let clip = MidiClip::stop_all(); - println!("{clip:?}"); - - let clip = MidiClip::default(); - let mut clip = MidiClip::new("clip", true, 1, None, None); - clip.set_length(96); - clip.toggle_loop(); - clip.record_event(12, MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }); - assert!(clip.contains_note_on(36.into(), 6, 18)); - assert_eq!(&clip.notes, &clip.duplicate().notes); - - let clip = std::sync::Arc::new(clip); - assert_eq!(clip.clone(), clip); -} - -#[cfg(test)] #[test] fn test_midi_play () { - use crate::*; - let sequencer = Sequencer::default(); - println!("{sequencer:?}"); -} diff --git a/tek/device/sequencer/seq_audio.rs b/tek/device/sequencer/seq_audio.rs deleted file mode 100644 index 40ae5e22..00000000 --- a/tek/device/sequencer/seq_audio.rs +++ /dev/null @@ -1,214 +0,0 @@ -use crate::*; - -/// JACK process callback for a sequencer's clip sequencer/recorder. -impl Audio for Sequencer { - fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { - if self.clock().is_rolling() { - self.process_rolling(scope) - } else { - self.process_stopped(scope) - } - } -} - -impl Sequencer { - fn process_rolling (&mut self, scope: &ProcessScope) -> Control { - self.process_clear(scope, false); - // Write chunk of clip to output, handle switchover - if self.process_playback(scope) { - self.process_switchover(scope); - } - // Monitor input to output - self.process_monitoring(scope); - // Record and/or monitor input - self.process_recording(scope); - // Emit contents of MIDI buffers to JACK MIDI output ports. - self.midi_outs_emit(scope); - Control::Continue - } - fn process_stopped (&mut self, scope: &ProcessScope) -> Control { - if self.monitoring() && self.midi_ins().len() > 0 && self.midi_outs().len() > 0 { - self.process_monitoring(scope) - } - Control::Continue - } - fn process_monitoring (&mut self, scope: &ProcessScope) { - let notes_in = self.notes_in().clone(); // For highlighting keys and note repeat - let monitoring = self.monitoring(); - for input in self.midi_ins.iter() { - for (sample, event, bytes) in input.parsed(scope) { - if let LiveEvent::Midi { message, .. } = event { - if monitoring { - self.midi_buf[sample].push(bytes.to_vec()); - } - // FIXME: don't lock on every event! - update_keys(&mut notes_in.write().unwrap(), &message); - } - } - } - } - /// Clear the section of the output buffer that we will be using, - /// emitting "all notes off" at start of buffer if requested. - fn process_clear (&mut self, scope: &ProcessScope, reset: bool) { - let n_frames = (scope.n_frames() as usize).min(self.midi_buf_mut().len()); - for frame in &mut self.midi_buf_mut()[0..n_frames] { - frame.clear(); - } - if reset { - all_notes_off(self.midi_buf_mut()); - } - for port in self.midi_outs_mut().iter_mut() { - // Clear output buffer(s) - port.buffer_clear(scope, false); - } - } - fn process_recording (&mut self, scope: &ProcessScope) { - if self.monitoring() { - self.monitor(scope); - } - if let Some((started, ref clip)) = self.play_clip.clone() { - self.record_clip(scope, started, clip); - } - if let Some((_start_at, _clip)) = &self.next_clip() { - self.record_next(); - } - } - fn process_playback (&mut self, scope: &ProcessScope) -> bool { - // If a clip is playing, write a chunk of MIDI events from it to the output buffer. - // If no clip is playing, prepare for switchover immediately. - if let Some((started, clip)) = &self.play_clip { - // Length of clip, to repeat or stop on end. - let length = clip.as_ref().map_or(0, |p|p.read().unwrap().length); - // Index of first sample to populate. - let offset = self.clock().get_sample_offset(scope, &started); - // Write MIDI events from clip at sample offsets corresponding to pulses. - for (sample, pulse) in self.clock().get_pulses(scope, offset) { - // If a next clip is enqueued, and we're past the end of the current one, - // break the loop here (FIXME count pulse correctly) - let past_end = if clip.is_some() { pulse >= length } else { true }; - // Is it time for switchover? - if self.next_clip().is_some() && past_end { - return true - } - // If there's a currently playing clip, output notes from it to buffer: - if let Some(clip) = clip { - // Source clip from which the MIDI events will be taken. - let clip = clip.read().unwrap(); - // Clip with zero length is not processed - if clip.length > 0 { - // Current pulse index in source clip - let pulse = pulse % clip.length; - // Output each MIDI event from clip at appropriate frames of output buffer: - for message in clip.notes[pulse].iter() { - for port in self.midi_outs.iter_mut() { - port.buffer_write(sample, LiveEvent::Midi { - channel: 0.into(), /* TODO */ - message: *message - }); - } - } - } - } - } - false - } else { - true - } - } - /// Handle switchover from current to next playing clip. - fn process_switchover (&mut self, scope: &ProcessScope) { - let midi_buf = self.midi_buf_mut(); - let sample0 = scope.last_frame_time() as usize; - //let samples = scope.n_frames() as usize; - if let Some((start_at, clip)) = &self.next_clip() { - let start = start_at.sample.get() as usize; - let sample = self.clock().started.read().unwrap() - .as_ref().unwrap().sample.get() as usize; - // If it's time to switch to the next clip: - if start <= sample0.saturating_sub(sample) { - // Samples elapsed since clip was supposed to start - let _skipped = sample0 - start; - // Switch over to enqueued clip - let started = Moment::from_sample(self.clock().timebase(), start as f64); - // Launch enqueued clip - *self.play_clip_mut() = Some((started, clip.clone())); - // Unset enqueuement (TODO: where to implement looping?) - *self.next_clip_mut() = None; - // Fill in remaining ticks of chunk from next clip. - self.process_playback(scope); - } - } - } -} - -pub trait HasMidiBuffers { - fn note_buf_mut (&mut self) -> &mut Vec; - fn midi_buf_mut (&mut self) -> &mut Vec>>; -} - -impl HasMidiBuffers for Sequencer { - fn note_buf_mut (&mut self) -> &mut Vec { - &mut self.note_buf - } - fn midi_buf_mut (&mut self) -> &mut Vec>> { - &mut self.midi_buf - } -} - -pub trait MidiMonitor: HasMidiIns + HasMidiBuffers { - fn notes_in (&self) -> &Arc>; - fn monitoring (&self) -> bool; - fn monitoring_mut (&mut self) -> &mut bool; - fn toggle_monitor (&mut self) { - *self.monitoring_mut() = !self.monitoring(); - } - fn monitor (&mut self, scope: &ProcessScope) { - } -} - -pub trait MidiRecord: MidiMonitor + HasClock + HasPlayClip { - fn recording (&self) -> bool; - fn recording_mut (&mut self) -> &mut bool; - fn toggle_record (&mut self) { - *self.recording_mut() = !self.recording(); - } - - fn overdub (&self) -> bool; - fn overdub_mut (&mut self) -> &mut bool; - fn toggle_overdub (&mut self) { - *self.overdub_mut() = !self.overdub(); - } - - fn record_clip ( - &mut self, - scope: &ProcessScope, - started: Moment, - clip: &Option>>, - ) { - if let Some(clip) = clip { - let sample0 = scope.last_frame_time() as usize; - let start = started.sample.get() as usize; - let _recording = self.recording(); - let timebase = self.clock().timebase().clone(); - let quant = self.clock().quant.get(); - let mut clip = clip.write().unwrap(); - let length = clip.length; - for input in self.midi_ins_mut().iter() { - for (sample, event, _bytes) in parse_midi_input(input.port().iter(scope)) { - if let LiveEvent::Midi { message, .. } = event { - clip.record_event({ - let sample = (sample0 + sample - start) as f64; - let pulse = timebase.samples_to_pulse(sample); - let quantized = (pulse / quant).round() * quant; - quantized as usize % length - }, message); - } - } - } - } - } - - fn record_next (&mut self) { - // TODO switch to next clip and record into it - } -} diff --git a/tek/device/sequencer/seq_clip.rs b/tek/device/sequencer/seq_clip.rs deleted file mode 100644 index 83762fe1..00000000 --- a/tek/device/sequencer/seq_clip.rs +++ /dev/null @@ -1,114 +0,0 @@ -//! MIDI clip data. -use crate::*; - -pub trait HasMidiClip { - fn clip (&self) -> Option>>; -} - -#[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 } - } - } -} - -/// A MIDI sequence. -#[derive(Debug, Clone, Default)] -pub struct MidiClip { - pub uuid: uuid::Uuid, - /// Name of clip - pub name: Arc, - /// Temporal resolution in pulses per quarter note - pub ppq: usize, - /// Length of clip in pulses - pub length: usize, - /// Notes in clip - pub notes: MidiData, - /// Whether to loop the clip or play it once - pub looped: bool, - /// Start of loop - pub loop_start: usize, - /// Length of loop - pub loop_length: usize, - /// All notes are displayed with minimum length - pub percussive: bool, - /// Identifying color of clip - pub color: ItemTheme, -} - -/// MIDI message structural -pub type MidiData = Vec>; - -impl MidiClip { - pub fn new ( - name: impl AsRef, - looped: bool, - length: usize, - notes: Option, - color: Option, - ) -> Self { - Self { - uuid: uuid::Uuid::new_v4(), - name: name.as_ref().into(), - ppq: PPQ, - length, - notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]), - looped, - loop_start: 0, - loop_length: length, - percussive: true, - color: color.unwrap_or_else(ItemTheme::random) - } - } - pub fn count_midi_messages (&self) -> usize { - let mut count = 0; - for tick in self.notes.iter() { - count += tick.len(); - } - count - } - pub fn set_length (&mut self, length: usize) { - self.length = length; - self.notes = vec![Vec::with_capacity(16);length]; - } - pub fn duplicate (&self) -> Self { - let mut clone = self.clone(); - clone.uuid = uuid::Uuid::new_v4(); - clone - } - pub fn toggle_loop (&mut self) { self.looped = !self.looped; } - pub fn record_event (&mut self, pulse: usize, message: MidiMessage) { - if pulse >= self.length { panic!("extend clip first") } - self.notes[pulse].push(message); - } - /// Check if a range `start..end` contains MIDI Note On `k` - pub fn contains_note_on (&self, k: u7, start: usize, end: usize) -> bool { - for events in self.notes[start.max(0)..end.min(self.notes.len())].iter() { - for event in events.iter() { - if let MidiMessage::NoteOn {key,..} = event { if *key == k { return true } } - } - } - false - } - pub fn stop_all () -> Self { - Self::new( - "Stop", - false, - 1, - Some(vec![vec![MidiMessage::Controller { - controller: 123.into(), - value: 0.into() - }]]), - Some(ItemColor::from_rgb(Color::Rgb(32, 32, 32)).into()) - ) - } -} - -impl PartialEq for MidiClip { - fn eq (&self, other: &Self) -> bool { - self.uuid == other.uuid - } -} - -impl Eq for MidiClip {} diff --git a/tek/device/sequencer/seq_launch.rs b/tek/device/sequencer/seq_launch.rs deleted file mode 100644 index fb8a0524..00000000 --- a/tek/device/sequencer/seq_launch.rs +++ /dev/null @@ -1,89 +0,0 @@ -use crate::*; - -pub trait HasPlayClip: HasClock { - - fn reset (&self) -> bool; - - fn reset_mut (&mut self) -> &mut bool; - - fn play_clip (&self) -> &Option<(Moment, Option>>)>; - - fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)>; - - fn next_clip (&self) -> &Option<(Moment, Option>>)>; - - fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)>; - - fn pulses_since_start (&self) -> Option { - if let Some((started, Some(_))) = self.play_clip().as_ref() { - let elapsed = self.clock().playhead.pulse.get() - started.pulse.get(); - return Some(elapsed) - } - None - } - - fn pulses_since_start_looped (&self) -> Option<(f64, f64)> { - if let Some((started, Some(clip))) = self.play_clip().as_ref() { - let elapsed = self.clock().playhead.pulse.get() - started.pulse.get(); - let length = clip.read().unwrap().length.max(1); // prevent div0 on empty clip - let times = (elapsed as usize / length) as f64; - let elapsed = (elapsed as usize % length) as f64; - return Some((times, elapsed)) - } - None - } - - fn enqueue_next (&mut self, clip: Option<&Arc>>) { - *self.next_clip_mut() = Some((self.clock().next_launch_instant(), clip.cloned())); - *self.reset_mut() = true; - } - - fn play_status (&self) -> impl Draw { - let (name, color): (Arc, ItemTheme) = if let Some((_, Some(clip))) = self.play_clip() { - let MidiClip { ref name, color, .. } = *clip.read().unwrap(); - (name.clone(), color) - } else { - ("".into(), Tui::g(64).into()) - }; - let time: String = self.pulses_since_start_looped() - .map(|(times, time)|format!("{:>3}x {:>}", times+1.0, self.clock().timebase.format_beats_1(time))) - .unwrap_or_else(||String::from(" ")).into(); - FieldV(color, "Now:", format!("{} {}", time, name)) - } - - fn next_status (&self) -> impl Draw { - let mut time: Arc = String::from("--.-.--").into(); - let mut name: Arc = String::from("").into(); - let mut color = ItemTheme::G[64]; - let clock = self.clock(); - if let Some((t, Some(clip))) = self.next_clip() { - let clip = clip.read().unwrap(); - name = clip.name.clone(); - color = clip.color.clone(); - time = { - let target = t.pulse.get(); - let current = clock.playhead.pulse.get(); - if target > current { - let remaining = target - current; - format!("-{:>}", clock.timebase.format_beats_1(remaining)) - } else { - String::new() - } - }.into() - } else if let Some((t, Some(clip))) = self.play_clip() { - let clip = clip.read().unwrap(); - if clip.looped { - name = clip.name.clone(); - color = clip.color.clone(); - let target = t.pulse.get() + clip.length as f64; - let current = clock.playhead.pulse.get(); - if target > current { - time = format!("-{:>}", clock.timebase.format_beats_0(target - current)).into() - } - } else { - name = "Stop".to_string().into(); - } - }; - FieldV(color, "Next:", format!("{} {}", time, name)) - } -} diff --git a/tek/device/sequencer/seq_model.rs b/tek/device/sequencer/seq_model.rs deleted file mode 100644 index 8b92885b..00000000 --- a/tek/device/sequencer/seq_model.rs +++ /dev/null @@ -1,154 +0,0 @@ -//! MIDI sequencer -use crate::*; - -impl> HasSequencer for T { - fn sequencer (&self) -> &Sequencer { - self.get() - } - fn sequencer_mut (&mut self) -> &mut Sequencer { - self.get_mut() - } -} - -pub trait HasSequencer { - fn sequencer (&self) -> &Sequencer; - fn sequencer_mut (&mut self) -> &mut Sequencer; -} - -/// Contains state for playing a clip -pub struct Sequencer { - /// State of clock and playhead - pub clock: Clock, - /// Start time and clip being played - pub play_clip: Option<(Moment, Option>>)>, - /// Start time and next clip - pub next_clip: Option<(Moment, Option>>)>, - /// Play input through output. - pub monitoring: bool, - /// Write input to sequence. - pub recording: bool, - /// Overdub input to sequence. - pub overdub: bool, - /// Send all notes off - pub reset: bool, // TODO?: after Some(nframes) - /// Record from MIDI ports to current sequence. - pub midi_ins: Vec, - /// Play from current sequence to MIDI ports - pub midi_outs: Vec, - /// Notes currently held at input - pub notes_in: Arc>, - /// Notes currently held at output - pub notes_out: Arc>, - /// MIDI output buffer - pub note_buf: Vec, - /// MIDI output buffer - pub midi_buf: Vec>>, -} - -impl Default for Sequencer { - fn default () -> Self { - Self { - play_clip: None, - next_clip: None, - recording: false, - monitoring: true, - overdub: false, - - notes_in: RwLock::new([false;128]).into(), - notes_out: RwLock::new([false;128]).into(), - note_buf: vec![0;8], - midi_buf: vec![], - reset: true, - - midi_ins: vec![], - midi_outs: vec![], - clock: Clock::default(), - } - } -} - -impl Sequencer { - pub fn new ( - name: impl AsRef, - jack: &Jack<'static>, - clock: Option<&Clock>, - clip: Option<&Arc>>, - midi_from: &[Connect], - midi_to: &[Connect], - ) -> Usually { - let _name = name.as_ref(); - let clock = clock.cloned().unwrap_or_default(); - Ok(Self { - midi_ins: vec![MidiInput::new(jack, &format!("M/{}", name.as_ref()), midi_from)?,], - midi_outs: vec![MidiOutput::new(jack, &format!("{}/M", name.as_ref()), midi_to)?, ], - play_clip: clip.map(|clip|(Moment::zero(&clock.timebase), Some(clip.clone()))), - clock, - reset: true, - notes_in: RwLock::new([false;128]).into(), - notes_out: RwLock::new([false;128]).into(), - ..Default::default() - }) - } -} - -impl std::fmt::Debug for Sequencer { - fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("Sequencer") - .field("clock", &self.clock) - .field("play_clip", &self.play_clip) - .field("next_clip", &self.next_clip) - .finish() - } -} - -has!(Clock: |self:Sequencer|self.clock); -has!(Vec: |self:Sequencer|self.midi_ins); -has!(Vec: |self:Sequencer|self.midi_outs); - -impl MidiMonitor for Sequencer { - fn notes_in (&self) -> &Arc> { - &self.notes_in - } - fn monitoring (&self) -> bool { - self.monitoring - } - fn monitoring_mut (&mut self) -> &mut bool { - &mut self.monitoring - } -} - -impl MidiRecord for Sequencer { - fn recording (&self) -> bool { - self.recording - } - fn recording_mut (&mut self) -> &mut bool { - &mut self.recording - } - fn overdub (&self) -> bool { - self.overdub - } - fn overdub_mut (&mut self) -> &mut bool { - &mut self.overdub - } -} - -impl HasPlayClip for Sequencer { - fn reset (&self) -> bool { - self.reset - } - fn reset_mut (&mut self) -> &mut bool { - &mut self.reset - } - fn play_clip (&self) -> &Option<(Moment, Option>>)> { - &self.play_clip - } - fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)> { - &mut self.play_clip - } - fn next_clip (&self) -> &Option<(Moment, Option>>)> { - &self.next_clip - } - fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)> { - &mut self.next_clip - } -} diff --git a/tek/device/sequencer/seq_view.rs b/tek/device/sequencer/seq_view.rs deleted file mode 100644 index 5635363e..00000000 --- a/tek/device/sequencer/seq_view.rs +++ /dev/null @@ -1,66 +0,0 @@ -use crate::*; - -pub trait MidiViewer: HasSize + MidiRange + MidiPoint + Debug + Send + Sync { - fn buffer_size (&self, clip: &MidiClip) -> (usize, usize); - fn redraw (&self); - fn clip (&self) -> &Option>>; - fn clip_mut (&mut self) -> &mut Option>>; - fn set_clip (&mut self, clip: Option<&Arc>>) { - *self.clip_mut() = clip.cloned(); - self.redraw(); - } - /// Make sure cursor is within note range - fn autoscroll (&self) { - let note_pos = self.get_note_pos().min(127); - let note_lo = self.get_note_lo(); - let note_hi = self.get_note_hi(); - if note_pos < note_lo { - self.note_lo().set(note_pos); - } else if note_pos > note_hi { - self.note_lo().set((note_lo + note_pos).saturating_sub(note_hi)); - } - } - /// Make sure time range is within display - fn autozoom (&self) { - if self.time_lock().get() { - let time_len = self.get_time_len(); - let time_axis = self.get_time_axis(); - let time_zoom = self.get_time_zoom(); - loop { - let time_zoom = self.time_zoom().get(); - let time_area = time_axis * time_zoom; - if time_area > time_len { - let next_time_zoom = NoteDuration::prev(time_zoom); - if next_time_zoom <= 1 { - break - } - let next_time_area = time_axis * next_time_zoom; - if next_time_area >= time_len { - self.time_zoom().set(next_time_zoom); - } else { - break - } - } else if time_area < time_len { - let prev_time_zoom = NoteDuration::next(time_zoom); - if prev_time_zoom > 384 { - break - } - let prev_time_area = time_axis * prev_time_zoom; - if prev_time_area <= time_len { - self.time_zoom().set(prev_time_zoom); - } else { - break - } - } - } - if time_zoom != self.time_zoom().get() { - self.redraw() - } - } - //while time_len.div_ceil(time_zoom) > time_axis { - //println!("\r{time_len} {time_zoom} {time_axis}"); - //time_zoom = Note::next(time_zoom); - //} - //self.time_zoom().set(time_zoom); - } -} diff --git a/tek/tek.rs b/tek/tek.rs deleted file mode 100644 index 8f7a31c1..00000000 --- a/tek/tek.rs +++ /dev/null @@ -1,1063 +0,0 @@ -#![feature( - adt_const_params, - associated_type_defaults, - closure_lifetime_binder, - if_let_guard, - impl_trait_in_assoc_type, - trait_alias, - type_alias_impl_trait, - type_changing_struct_update, -)] - -#![allow( - clippy::unit_arg -)] - -mod deps; pub use self::deps::*; -mod config; pub use self::config::*; -mod device; pub use self::device::*; -mod engine; pub use self::engine::*; - -#[cfg(test)] mod test; - -/// Total state -#[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: Measure, - /// 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, -} - -audio!( - |self: App, client, scope|{ - 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 - }; - |self, event|{ - 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:?}"); } - } - } -); - -tui_draw!(|self: App, to|to.place(&self.content())); -content!(TuiOut: |self: App|Thunk::new(|to: &mut TuiOut|{ - for (index, dsl) in self.mode.view.iter().enumerate() { - self.view(to, dsl).unwrap_or_else(|e|panic!("render #{index} failed ({e}): {dsl}")); - } -})); -impl View for App { - fn view_expr <'a> (&'a self, to: &mut TuiOut, expr: &'a impl DslExpr) -> Usually<()> { - if evaluate_output_expression(self, to, expr)? - || evaluate_output_expression_tui(self, to, expr)? { - Ok(()) - } else { - Err(format!("App::view_expr: unexpected: {expr:?}").into()) - } - } - fn view_word <'a> (&'a self, to: &mut TuiOut, dsl: &'a impl DslExpr) -> Usually<()> { - let mut frags = dsl.src()?.unwrap().split("/"); - match frags.next() { - Some(":logo") => to.place(&view_logo()), - Some(":status") => to.place(&Fixed::y(1, "TODO: Status Bar")), - Some(":meters") => match frags.next() { - Some("input") => to.place(&Tui::bg(Rgb(30, 30, 30), Fill::y(Align::s("Input Meters")))), - Some("output") => to.place(&Tui::bg(Rgb(30, 30, 30), Fill::y(Align::s("Output Meters")))), - _ => panic!() - }, - Some(":tracks") => match frags.next() { - None => to.place(&"TODO tracks"), - Some("names") => to.place(&self.project.view_track_names(self.color.clone())),//Tui::bg(Rgb(40, 40, 40), Fill::x(Align::w("Track Names")))), - Some("inputs") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::x(Align::w("Track Inputs")))), - Some("devices") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::x(Align::w("Track Devices")))), - Some("outputs") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::x(Align::w("Track Outputs")))), - _ => panic!() - }, - Some(":scenes") => match frags.next() { - None => to.place(&"TODO scenes"), - Some(":scenes/names") => to.place(&"TODO Scene Names"), - _ => panic!() - }, - Some(":editor") => to.place(&"TODO Editor"), - Some(":dialog") => match frags.next() { - Some("menu") => to.place(&if let Dialog::Menu(selected, items) = &self.dialog { - let items = items.clone(); - let selected = selected; - Some(Fill::xy(Thunk::new(move|to: &mut TuiOut|{ - for (index, MenuItem(item, _)) in items.0.iter().enumerate() { - to.place(&Push::y((2 * index) as u16, - Tui::fg_bg( - if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) }, - if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) }, - Fixed::y(2, Align::n(Fill::x(item))) - ))); - } - }))) - } else { - None - }), - _ => unimplemented!("App::view_word: {dsl:?} ({frags:?})"), - }, - Some(":templates") => to.place(&{ - let modes = self.config.modes.clone(); - let height = (modes.read().unwrap().len() * 2) as u16; - Fixed::y(height, Min::x(30, Thunk::new(move |to: &mut TuiOut|{ - for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() { - let bg = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) }; - let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or(""); - let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or(""); - let fg1 = Rgb(224, 192, 128); - let fg2 = Rgb(224, 128, 32); - let field_name = Fill::x(Align::w(Tui::fg(fg1, name))); - let field_id = Fill::x(Align::e(Tui::fg(fg2, id))); - let field_info = Fill::x(Align::w(info)); - to.place(&Push::y((2 * index) as u16, - Fixed::y(2, Fill::x(Tui::bg(bg, Bsp::s( - Bsp::a(field_name, field_id), field_info)))))); - } - }))) - }), - Some(":sessions") => to.place(&Fixed::y(6, Min::x(30, Thunk::new(|to: &mut TuiOut|{ - let fg = Rgb(224, 192, 128); - for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { - let bg = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; - to.place(&Push::y((2 * index) as u16, - &Fixed::y(2, Fill::x(Tui::bg(bg, Align::w(Tui::fg(fg, name))))))); - } - })))), - Some(":browse/title") => to.place(&Fill::x(Align::w(FieldV(Default::default(), - match self.dialog.browser_target().unwrap() { - BrowseTarget::SaveProject => "Save project:", - BrowseTarget::LoadProject => "Load project:", - BrowseTarget::ImportSample(_) => "Import sample:", - BrowseTarget::ExportSample(_) => "Export sample:", - BrowseTarget::ImportClip(_) => "Import clip:", - BrowseTarget::ExportClip(_) => "Export clip:", - }, Shrink::x(3, Fixed::y(1, Tui::fg(Tui::g(96), RepeatH("๐Ÿญป")))))))), - Some(":device") => { - let selected = self.dialog.device_kind().unwrap(); - to.place(&Bsp::s(Tui::bold(true, "Add device"), Map::south(1, - move||device_kinds().iter(), - move|_label: &&'static str, i|{ - let bg = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }; - let lb = if i == selected { "[ " } else { " " }; - let rb = if i == selected { " ]" } else { " " }; - Fill::x(Tui::bg(bg, Bsp::e(lb, Bsp::w(rb, "FIXME device name")))) }))) - }, - Some(":debug") => to.place(&Fixed::y(1, format!("[{:?}]", to.area()))), - Some(_) => { - let views = self.config.views.read().unwrap(); - if let Some(dsl) = views.get(dsl.src()?.unwrap()) { - let dsl = dsl.clone(); - std::mem::drop(views); - self.view(to, &dsl)? - } else { - unimplemented!("{dsl:?}"); - } - }, - _ => unreachable!() - } - Ok(()) - } -} -fn view_logo () -> impl Draw + Layout { - Fixed::xy(32, 7, Tui::bold(true, Tui::fg(Rgb(240,200,180), col!{ - Fixed::y(1, ""), - Fixed::y(1, ""), - Fixed::y(1, "~~ โ•“โ”€โ•ฅโ”€โ•– โ•“โ”€โ”€โ•– โ•ฅ โ•– ~~~~~~~~~~~~"), - Fixed::y(1, Bsp::e("~~~~ โ•‘ ~ โ•Ÿโ”€โ•Œ ~โ•Ÿโ”€< ~~ ", Bsp::e(Tui::fg(Rgb(230,100,40), "v0.3.0"), " ~~"))), - Fixed::y(1, "~~~~ โ•จ ~ โ•™โ”€โ”€โ•œ โ•จ โ•œ ~~~~~~~~~~~~"), - }))) -} - -// Allow source to be read as Literal string -dsl_ns!(App: Arc { - literal = |dsl|Ok(dsl.src()?.map(|x|x.into())); -}); - -// Provide boolean values. -dsl_ns!(App: bool { - // TODO literal = ... - word = |app| { - ":mode/editor" => app.project.editor.is_some(), - ":focused/dialog" => !matches!(app.dialog, Dialog::None), - ":focused/message" => matches!(app.dialog, Dialog::Message(..)), - ":focused/add_device" => matches!(app.dialog, Dialog::Device(..)), - ":focused/browser" => app.dialog.browser().is_some(), - ":focused/pool/import" => matches!(app.pool.mode, Some(PoolMode::Import(..))), - ":focused/pool/export" => matches!(app.pool.mode, Some(PoolMode::Export(..))), - ":focused/pool/rename" => matches!(app.pool.mode, Some(PoolMode::Rename(..))), - ":focused/pool/length" => matches!(app.pool.mode, Some(PoolMode::Length(..))), - ":focused/clip" => !app.editor_focused() && matches!(app.selection(), - Selection::TrackClip{..}), - ":focused/track" => !app.editor_focused() && matches!(app.selection(), - Selection::Track(..)), - ":focused/scene" => !app.editor_focused() && matches!(app.selection(), - Selection::Scene(..)), - ":focused/mix" => !app.editor_focused() && matches!(app.selection(), - Selection::Mix), - }; -}); - -// TODO: provide colors here -dsl_ns!(App: ItemTheme {}); - -dsl_ns!(App: Dialog { - word = |app| { - ":dialog/none" => Dialog::None, - ":dialog/options" => Dialog::Options, - ":dialog/device" => Dialog::Device(0), - ":dialog/device/prev" => Dialog::Device(0), - ":dialog/device/next" => Dialog::Device(0), - ":dialog/help" => Dialog::Help(0), - ":dialog/save" => Dialog::Browse(BrowseTarget::SaveProject, - Browse::new(None).unwrap().into()), - ":dialog/load" => Dialog::Browse(BrowseTarget::LoadProject, - Browse::new(None).unwrap().into()), - ":dialog/import/clip" => Dialog::Browse(BrowseTarget::ImportClip(Default::default()), - Browse::new(None).unwrap().into()), - ":dialog/export/clip" => Dialog::Browse(BrowseTarget::ExportClip(Default::default()), - Browse::new(None).unwrap().into()), - ":dialog/import/sample" => Dialog::Browse(BrowseTarget::ImportSample(Default::default()), - Browse::new(None).unwrap().into()), - ":dialog/export/sample" => Dialog::Browse(BrowseTarget::ExportSample(Default::default()), - Browse::new(None).unwrap().into()), - }; -}); - -dsl_ns!(App: Selection { - word = |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(), - }; -}); - -dsl_ns!(App: Color { - word = |app| { - ":color/bg" => Color::Rgb(28, 32, 36), - }; - expr = |app| { - "g" (n: u8) => Color::Rgb(n, n, n), - "rgb" (r: u8, g: u8, b: u8) => Color::Rgb(r, g, b), - }; -}); - -dsl_ns!(App: Option { - word = |app| { - ":editor/pitch" => Some( - (app.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into() - ) - }; -}); - -dsl_ns!(App: Option { - word = |app| { - ":selected/scene" => app.selection().scene(), - ":selected/track" => app.selection().track(), - }; -}); - -dsl_ns!(App: Option>> { - word = |app| { - ":selected/clip" => if let Selection::TrackClip { track, scene } = app.selection() { - app.scenes()[*scene].clips[*track].clone() - } else { - None - } - }; -}); - -dsl_ns!(App: u8 { - literal = |dsl|Ok(if let Some(src) = dsl.src()? { - Some(to_number(src)? as u8) - } else { - None - }); -}); - -dsl_ns!(App: u16 { - literal = |dsl|Ok(if let Some(src) = dsl.src()? { - Some(to_number(src)? as u16) - } else { - None - }); - word = |app| { - ":w/sidebar" => app.project.w_sidebar(app.editor().is_some()), - ":h/sample-detail" => 6.max(app.height() as u16 * 3 / 9), - }; -}); - -dsl_ns!(App: usize { - literal = |dsl|Ok(if let Some(src) = dsl.src()? { - Some(to_number(src)? as usize) - } else { - None - }); - word = |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), - }; -}); - -dsl_ns!(App: isize { - literal = |dsl|Ok(if let Some(src) = dsl.src()? { - Some(to_number(src)? as isize) - } else { - None - }); -}); - -has!(Jack<'static>: |self: App|self.jack); -has!(Pool: |self: App|self.pool); -has!(Dialog: |self: App|self.dialog); -has!(Clock: |self: App|self.project.clock); -has!(Option: |self: App|self.project.editor); -has!(Selection: |self: App|self.project.selection); -has!(Vec: |self: App|self.project.midi_ins); -has!(Vec: |self: App|self.project.midi_outs); -has!(Vec: |self: App|self.project.scenes); -has!(Vec: |self: App|self.project.tracks); -has!(Measure: |self: App|self.size); -has_clips!( |self: App|self.pool.clips); -maybe_has!(Track: |self: App| { MaybeHas::::get(&self.project) }; - { MaybeHas::::get_mut(&mut self.project) }); -maybe_has!(Scene: |self: App| { MaybeHas::::get(&self.project) }; - { MaybeHas::::get_mut(&mut self.project) }); - -impl HasClipsSize for App { - fn clips_size (&self) -> &Measure { &self.project.inner_size } -} -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 HasJack<'static> for App { - fn jack (&self) -> &Jack<'static> { &self.jack } -} -impl ScenesView for App { - fn w_side (&self) -> u16 { 20 } - fn w_mid (&self) -> u16 { (self.width() as u16).saturating_sub(self.w_side()) } - fn h_scenes (&self) -> u16 { (self.height() as u16).saturating_sub(20) } -} - - -impl App { - - pub fn editor_focused (&self) -> bool { - false - } - - pub fn toggle_dialog (&mut self, mut dialog: Dialog) -> Dialog { - std::mem::swap(&mut self.dialog, &mut dialog); - dialog - } - - pub fn toggle_editor (&mut self, value: Option) { - //FIXME: self.editing.store(value.unwrap_or_else(||!self.is_editing()), Relaxed); - let value = value.unwrap_or_else(||!self.editor().is_some()); - if value { - // Create new clip in pool when entering empty cell - if let Selection::TrackClip { track, scene } = *self.selection() - && let Some(scene) = self.project.scenes.get_mut(scene) - && let Some(slot) = scene.clips.get_mut(track) - && slot.is_none() - && let Some(track) = self.project.tracks.get_mut(track) - { - let (index, mut clip) = self.pool.add_new_clip(); - // autocolor: new clip colors from scene and track color - let color = track.color.base.mix(scene.color.base, 0.5); - clip.write().unwrap().color = ItemColor::random_near(color, 0.2).into(); - if let Some(editor) = &mut self.project.editor { - editor.set_clip(Some(&clip)); - } - *slot = Some(clip.clone()); - //Some(clip) - } else { - //None - } - } else if let Selection::TrackClip { track, scene } = *self.selection() - && let Some(scene) = self.project.scenes.get_mut(scene) - && let Some(slot) = scene.clips.get_mut(track) - && let Some(clip) = slot.as_mut() - { - // Remove clip from arrangement when exiting empty clip editor - let mut swapped = None; - if clip.read().unwrap().count_midi_messages() == 0 { - std::mem::swap(&mut swapped, slot); - } - if let Some(clip) = swapped { - self.pool.delete_clip(&clip.read().unwrap()); - } - } - } - - pub fn browser (&self) -> Option<&Browse> { - if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None } - } - - pub fn device_pick (&mut self, index: usize) { - self.dialog = Dialog::Device(index); - } - - pub fn add_device (&mut self, index: usize) -> Usually<()> { - match index { - 0 => { - let name = self.jack.with_client(|c|c.name().to_string()); - let midi = self.project.track().expect("no active track").sequencer.midi_outs[0].port_name(); - let track = self.track().expect("no active track"); - let port = format!("{}/Sampler", &track.name); - let connect = Connect::exact(format!("{name}:{midi}")); - let sampler = if let Ok(sampler) = Sampler::new( - &self.jack, &port, &[connect], &[&[], &[]], &[&[], &[]] - ) { - self.dialog = Dialog::None; - Device::Sampler(sampler) - } else { - self.dialog = Dialog::Message("Failed to add device.".into()); - return Err("failed to add device".into()) - }; - let track = self.track_mut().expect("no active track"); - track.devices.push(sampler); - Ok(()) - }, - 1 => { - todo!(); - Ok(()) - }, - _ => unreachable!(), - } - } - - pub fn update_clock (&self) { - ViewCache::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80) - } -} - -/// Various possible dialog modes. -#[derive(Debug, Clone, Default, PartialEq)] -pub enum Dialog { - #[default] None, - Help(usize), - Menu(usize, MenuItems), - Device(usize), - Message(Arc), - Browse(BrowseTarget, Arc), - Options, -} - -#[derive(Debug, Clone, Default, PartialEq)] -pub struct MenuItems(pub Arc<[MenuItem]>); - -impl AsRef> for MenuItems { - fn as_ref (&self) -> &Arc<[MenuItem]> { - &self.0 - } -} - -#[derive(Clone)] -pub struct MenuItem( - /// Label - pub Arc, - /// Callback - pub ArcUsually<()> + Send + Sync>> -); - -impl Default for MenuItem { - fn default () -> Self { Self("".into(), Arc::new(Box::new(|_|Ok(())))) } -} - -impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) }); - -impl PartialEq for MenuItem { - fn eq (&self, other: &Self) -> bool { - self.0 == other.0 - } -} - -impl Dialog { - pub fn welcome () -> Self { - Self::Menu(1, MenuItems([ - MenuItem("Resume session".into(), Arc::new(Box::new(|_|Ok(())))), - MenuItem("Create new session".into(), Arc::new(Box::new(|app|Ok({ - app.dialog = Dialog::None; - app.mode = app.config.modes.clone().read().unwrap().get(":arranger").cloned().unwrap(); - })))), - MenuItem("Load old session".into(), Arc::new(Box::new(|_|Ok(())))), - ].into())) - } - pub fn menu_next (&self) -> Self { - match self { - Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.0.len()), items.clone()), - _ => Self::None - } - } - pub fn menu_prev (&self) -> Self { - match self { - Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.0.len()), items.clone()), - _ => Self::None - } - } - pub fn menu_selected (&self) -> Option { - if let Self::Menu(selected, _) = self { Some(*selected) } else { None } - } - pub fn device_kind (&self) -> Option { - if let Self::Device(index) = self { Some(*index) } else { None } - } - pub fn device_kind_next (&self) -> Option { - self.device_kind().map(|index|(index + 1) % device_kinds().len()) - } - pub fn device_kind_prev (&self) -> Option { - self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1))) - } - pub fn message (&self) -> Option<&str> { - todo!() - } - pub fn browser (&self) -> Option<&Arc> { - todo!() - } - pub fn browser_target (&self) -> Option<&BrowseTarget> { - todo!() - } -} - -handle!(TuiIn:|self: App, input|{ - let mut commands = vec![]; - for id in self.mode.keys.iter() { - if let Some(event_map) = self.config.binds.clone().read().unwrap().get(id.as_ref()) { - if let Some(bindings) = event_map.query(input.event()) { - for binding in bindings { - for command in binding.commands.iter() { - if let Some(command) = self.from(command)? as Option { - commands.push(command) - } - } - } - } - } - } - for command in commands.into_iter() { - let result = command.execute(self); - match result { - Ok(undo) => { - self.history.push((command, undo)); - }, - Err(e) => { - self.history.push((command, None)); - return Err(e) - } - } - } - Ok(None) -}); - -#[derive(Debug, Copy, Clone)] -pub enum Axis { X, Y, Z, I } - -impl<'a> DslNs<'a, AppCommand> for App {} -impl<'a> DslNsExprs<'a, AppCommand> for App {} -impl<'a> DslNsWords<'a, AppCommand> for App { - dsl_words!('a |app| -> AppCommand { - "x/inc" => AppCommand::Inc { axis: Axis::X }, - "x/dec" => AppCommand::Dec { axis: Axis::X }, - "y/inc" => AppCommand::Inc { axis: Axis::Y }, - "y/dec" => AppCommand::Dec { axis: Axis::Y }, - "confirm" => AppCommand::Confirm, - "cancel" => AppCommand::Cancel, - }); -} - -impl Default for AppCommand { fn default () -> Self { Self::Nop } } - -def_command!(AppCommand: |app: App| { - Nop => Ok(None), - Confirm => Ok(match &app.dialog { - Dialog::Menu(index, items) => { - let callback = items.0[*index].1.clone(); - callback(app)?; - None - }, - _ => todo!(), - }), - Cancel => todo!(), // TODO delegate: - Inc { axis: Axis } => Ok(match (&app.dialog, axis) { - (Dialog::None, _) => todo!(), - (Dialog::Menu(_, _), Axis::Y) => AppCommand::SetDialog { dialog: app.dialog.menu_next() } - .execute(app)?, - _ => todo!() - }), - Dec { axis: Axis } => Ok(match (&app.dialog, axis) { - (Dialog::None, _) => None, - (Dialog::Menu(_, _), Axis::Y) => AppCommand::SetDialog { dialog: app.dialog.menu_prev() } - .execute(app)?, - _ => todo!() - }), - SetDialog { dialog: Dialog } => { - swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog }) - }, -}); - -pub fn wrap_inc (index: usize, count: usize) -> usize { - if count > 0 { (index + 1) % count } else { 0 } -} - -pub fn wrap_dec (index: usize, count: usize) -> usize { - if count > 0 { index.overflowing_sub(1).0.min(count.saturating_sub(1)) } else { 0 } -} - -impl Dialog { -} - - //AppCommand => { - //("x/inc" / - //("stop-all") => todo!(),//app.project.stop_all(), - //("enqueue", clip: Option>>) => todo!(), - //("history", delta: isize) => todo!(), - //("zoom", zoom: usize) => todo!(), - //("select", selection: Selection) => todo!(), - //("dialog" / command: DialogCommand) => todo!(), - //("project" / command: ArrangementCommand) => todo!(), - //("clock" / command: ClockCommand) => todo!(), - //("sampler" / command: SamplerCommand) => todo!(), - //("pool" / command: PoolCommand) => todo!(), - //("edit" / editor: MidiEditCommand) => todo!(), - //}; - - //DialogCommand; - - //ArrangementCommand; - - //ClockCommand; - - //SamplerCommand; - - //PoolCommand; - - //MidiEditCommand; - - -//take!(DialogCommand |state: App, iter|Take::take(&state.dialog, iter)); -//#[derive(Clone, Debug)] -//pub enum DialogCommand { - //Open { dialog: Dialog }, - //Close -//} - -//impl Command> for DialogCommand { - //fn execute (self, state: &mut Option) -> Perhaps { - //match self { - //Self::Open { dialog } => { - //*state = Some(dialog); - //}, - //Self::Close => { - //*state = None; - //} - //}; - //Ok(None) - //} -//} - -//dsl!(DialogCommand: |self: Dialog, iter|todo!()); -//Dsl::take(&mut self.dialog, iter)); - -//#[tengri_proc::command(Option)]//Nope. -//impl DialogCommand { - //fn open (dialog: &mut Option, new: Dialog) -> Perhaps { - //*dialog = Some(new); - //Ok(None) - //} - //fn close (dialog: &mut Option) -> Perhaps { - //*dialog = None; - //Ok(None) - //} -//} -// -//dsl_bind!(AppCommand: App { - //enqueue = |app, clip: Option>>| { todo!() }; - //history = |app, delta: isize| { todo!() }; - //zoom = |app, zoom: usize| { todo!() }; - //stop_all = |app| { app.tracks_stop_all(); Ok(None) }; - ////dialog = |app, command: DialogCommand| - ////Ok(command.delegate(&mut app.dialog, |c|Self::Dialog{command: c})?); - //project = |app, command: ArrangementCommand| - //Ok(command.delegate(&mut app.project, |c|Self::Project{command: c})?); - //clock = |app, command: ClockCommand| - //Ok(command.execute(app.clock_mut())?.map(|c|Self::Clock{command: c})); - //sampler = |app, command: SamplerCommand| - //Ok(app.project.sampler_mut().map(|s|command.delegate(s, |command|Self::Sampler{command})) - //.transpose()?.flatten()); - //pool = |app, command: PoolCommand| { - //let undo = command.clone().delegate(&mut app.pool, |command|AppCommand::Pool{command})?; - //// update linked editor after pool action - //match command { - //// autoselect: automatically load selected clip in editor - //PoolCommand::Select { .. } | - //// autocolor: update color in all places simultaneously - //PoolCommand::Clip { command: PoolClipCommand::SetColor { .. } } => { - //let clip = app.pool.clip().clone(); - //app.editor_mut().map(|editor|editor.set_clip(clip.as_ref())) - //}, - //_ => None - //}; - //Ok(undo) - //}; - //select = |app, selection: Selection| { - //*app.project.selection_mut() = selection; - ////todo! - ////if let Some(ref mut editor) = app.editor_mut() { - ////editor.set_clip(match selection { - ////Selection::TrackClip { track, scene } if let Some(Some(Some(clip))) = app - ////.project - ////.scenes.get(scene) - ////.map(|s|s.clips.get(track)) - ////=> - ////Some(clip), - ////_ => - ////None - ////}); - ////} - //Ok(None) - ////("select" [t: usize, s: usize] Some(match (t.expect("no track"), s.expect("no scene")) { - ////(0, 0) => Self::Select(Selection::Mix), - ////(t, 0) => Self::Select(Selection::Track(t)), - ////(0, s) => Self::Select(Selection::Scene(s)), - ////(t, s) => Self::Select(Selection::TrackClip { track: t, scene: s }) }))) - //// autoedit: load focused clip in editor. - //}; - ////fn color (app: &mut App, theme: ItemTheme) -> Perhaps { - ////Ok(app.set_color(Some(theme)).map(|theme|Self::Color{theme})) - ////} - ////fn launch (app: &mut App) -> Perhaps { - ////app.project.launch(); - ////Ok(None) - ////} - //toggle_editor = |app, value: bool|{ app.toggle_editor(Some(value)); Ok(None) }; - //editor = |app, command: MidiEditCommand| Ok(if let Some(editor) = app.editor_mut() { - //let undo = command.clone().delegate(editor, |command|AppCommand::Editor{command})?; - //// update linked sampler after editor action - //app.project.sampler_mut().map(|sampler|match command { - //// autoselect: automatically select sample in sampler - //MidiEditCommand::SetNotePos { pos } => { sampler.set_note_pos(pos); }, - //_ => {} - //}); - //undo - //} else { - //None - //}); -//}); -//take!(ClockCommand |state: App, iter|Take::take(state.clock(), iter)); -//take!(MidiEditCommand |state: App, iter|Ok(state.editor().map(|x|Take::take(x, iter)).transpose()?.flatten())); -//take!(PoolCommand |state: App, iter|Take::take(&state.pool, iter)); -//take!(SamplerCommand |state: App, iter|Ok(state.project.sampler().map(|x|Take::take(x, iter)).transpose()?.flatten())); -//take!(ArrangementCommand |state: App, iter|Take::take(&state.project, iter)); - -//pub fn view_nil (_: &App) -> TuiCb { - //|to|to.place(&Fill::xy("ยท")) -//} - - //Bsp::s("", - //Map::south(1, - //move||app.config.binds.layers.iter() - //.filter_map(|a|(a.0)(app).then_some(a.1)) - //.flat_map(|a|a) - //.filter_map(|x|if let Value::Exp(_, iter)=x.value{ Some(iter) } else { None }) - //.skip(offset) - //.take(20), - //|mut b,i|Fixed::x(60, Align::w(Bsp::e("(", Bsp::e( - //b.next().map(|t|Fixed::x(16, Align::w(Tui::fg(Rgb(64,224,0), format!("{}", t.value))))), - //Bsp::e(" ", Align::w(format!("{}", b.0.0.trim()))))))))))), - - //Dialog::Browse(BrowseTarget::Load, browser) => { - //"bobcat".boxed() - ////Bsp::s( - ////Fill::x(Align::w(Margin::xy(1, 1, Bsp::e( - ////Tui::bold(true, " Load project: "), - ////Shrink::x(3, Fixed::y(1, RepeatH("๐Ÿญป"))))))), - ////Outer(true, Style::default().fg(Tui::g(96))) - ////.enclose(Fill::xy(browser))) - //}, - //Dialog::Browse(BrowseTarget::Export, browser) => { - //"bobcat".boxed() - ////Bsp::s( - ////Fill::x(Align::w(Margin::xy(1, 1, Bsp::e( - ////Tui::bold(true, " Export: "), - ////Shrink::x(3, Fixed::y(1, RepeatH("๐Ÿญป"))))))), - ////Outer(true, Style::default().fg(Tui::g(96))) - ////.enclose(Fill::xy(browser))) - //}, - //Dialog::Browse(BrowseTarget::Import, browser) => { - //"bobcat".boxed() - ////Bsp::s( - ////Fill::x(Align::w(Margin::xy(1, 1, Bsp::e( - ////Tui::bold(true, " Import: "), - ////Shrink::x(3, Fixed::y(1, RepeatH("๐Ÿญป"))))))), - ////Outer(true, Style::default().fg(Tui::g(96))) - ////.enclose(Fill::xy(browser))) - //}, -// - //pub fn view_history (&self) -> impl Content { - //Fixed::y(1, Fill::x(Align::w(FieldH(self.color, - //format!("History ({})", self.history.len()), - //self.history.last().map(|last|Fill::x(Align::w(format!("{:?}", last.0)))))))) - //} - //pub fn view_status_h2 (&self) -> impl Content { - //self.update_clock(); - //let theme = self.color; - //let clock = self.clock(); - //let playing = clock.is_rolling(); - //let cache = clock.view_cache.clone(); - ////let selection = self.selection().describe(self.tracks(), self.scenes()); - //let hist_len = self.history.len(); - //let hist_last = self.history.last(); - //Fixed::y(2, Stack::east(move|add: &mut dyn FnMut(&dyn Draw)|{ - //add(&Fixed::x(5, Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, - //Either::new(false, // TODO - //Thunk::new(move||Fixed::x(9, Either::new(playing, - //Tui::fg(Rgb(0, 255, 0), " PLAYING "), - //Tui::fg(Rgb(255, 128, 0), " STOPPED "))) - //), - //Thunk::new(move||Fixed::x(5, Either::new(playing, - //Tui::fg(Rgb(0, 255, 0), Bsp::s(" ๐Ÿญ๐Ÿญ‘๐Ÿฌฝ ", " ๐Ÿญž๐Ÿญœ๐Ÿญ˜ ",)), - //Tui::fg(Rgb(255, 128, 0), Bsp::s(" โ–—โ–„โ–– ", " โ–โ–€โ–˜ ",)))) - //) - //) - //))); - //add(&" "); - //{ - //let cache = cache.read().unwrap(); - //add(&Fixed::x(15, Align::w(Bsp::s( - //FieldH(theme, "Beat", cache.beat.view.clone()), - //FieldH(theme, "Time", cache.time.view.clone()), - //)))); - //add(&Fixed::x(13, Align::w(Bsp::s( - //Fill::x(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))), - //Fill::x(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))), - //)))); - //add(&Fixed::x(12, Align::w(Bsp::s( - //Fill::x(Align::w(FieldH(theme, "Buf", cache.buf.view.clone()))), - //Fill::x(Align::w(FieldH(theme, "Lat", cache.lat.view.clone()))), - //)))); - ////add(&Bsp::s( - //////Fill::x(Align::w(FieldH(theme, "Selected", Align::w(selection)))), - ////Fill::x(Align::w(FieldH(theme, format!("History ({})", hist_len), - ////hist_last.map(|last|Fill::x(Align::w(format!("{:?}", last.0))))))), - ////"" - ////)); - //////if let Some(last) = self.history.last() { - //////add(&FieldV(theme, format!("History ({})", self.history.len()), - //////Fill::x(Align::w(format!("{:?}", last.0))))); - //////} - //} - //})) - //} - //pub fn view_status_v (&self) -> impl Content + use<'_> { - //self.update_clock(); - //let cache = self.project.clock.view_cache.read().unwrap(); - //let theme = self.color; - //let playing = self.clock().is_rolling(); - //Tui::bg(theme.darker.rgb, Fixed::xy(20, 5, Outer(true, Style::default().fg(Tui::g(96))).enclose( - //col!( - //Fill::x(Align::w(Bsp::e( - //Align::w(Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, - //Either::new(false, // TODO - //Thunk::new(move||Fixed::x(9, Either::new(playing, - //Tui::fg(Rgb(0, 255, 0), " PLAYING "), - //Tui::fg(Rgb(255, 128, 0), " STOPPED "))) - //), - //Thunk::new(move||Fixed::x(5, Either::new(playing, - //Tui::fg(Rgb(0, 255, 0), Bsp::s(" ๐Ÿญ๐Ÿญ‘๐Ÿฌฝ ", " ๐Ÿญž๐Ÿญœ๐Ÿญ˜ ",)), - //Tui::fg(Rgb(255, 128, 0), Bsp::s(" โ–—โ–„โ–– ", " โ–โ–€โ–˜ ",)))) - //) - //) - //)), - //Bsp::s( - //FieldH(theme, "Beat", cache.beat.view.clone()), - //FieldH(theme, "Time", cache.time.view.clone()), - //), - //))), - //Fill::x(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))), - //Fill::x(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))), - //Fill::x(Align::w(FieldH(theme, "Buf", Bsp::e(cache.buf.view.clone(), Bsp::e(" = ", cache.lat.view.clone()))))), - //)))) - //} - //pub fn view_status (&self) -> impl Content + use<'_> { - //self.update_clock(); - //let cache = self.project.clock.view_cache.read().unwrap(); - //view_status(Some(self.project.selection.describe(self.tracks(), self.scenes())), - //cache.sr.view.clone(), cache.buf.view.clone(), cache.lat.view.clone()) - //} - //pub fn view_transport (&self) -> impl Content + use<'_> { - //self.update_clock(); - //let cache = self.project.clock.view_cache.read().unwrap(); - //view_transport(self.project.clock.is_rolling(), - //cache.bpm.view.clone(), cache.beat.view.clone(), cache.time.view.clone()) - //} - //pub fn view_editor (&self) -> impl Content + use<'_> { - //let bg = self.editor() - //.and_then(|editor|editor.clip().clone()) - //.map(|clip|clip.read().unwrap().color.darker) - //.unwrap_or(self.color.darker); - //Fill::xy(Tui::bg(bg.rgb, self.editor())) - //} - //pub fn view_editor_status (&self) -> impl Content + use<'_> { - //self.editor().map(|e|Fixed::x(20, Outer(true, Style::default().fg(Tui::g(96))).enclose( - //Fill::y(Align::n(Bsp::s(e.clip_status(), e.edit_status())))))) - //} - //pub fn view_midi_ins_status (&self) -> impl Content + use<'_> { - //self.project.view_midi_ins_status(self.color) - //} - //pub fn view_midi_outs_status (&self) -> impl Content + use<'_> { - //self.project.view_midi_outs_status(self.color) - //} - //pub fn view_audio_ins_status (&self) -> impl Content + use<'_> { - //self.project.view_audio_ins_status(self.color) - //} - //pub fn view_audio_outs_status (&self) -> impl Content + use<'_> { - //self.project.view_audio_outs_status(self.color) - //} - //pub fn view_scenes (&self) -> impl Content + use<'_> { - //Bsp::e( - //Fixed::x(20, Align::nw(self.project.view_scenes_names())), - //self.project.view_scenes_clips(), - //) - //} - //pub fn view_scenes_names (&self) -> impl Content + use<'_> { - //self.project.view_scenes_names() - //} - //pub fn view_scenes_clips (&self) -> impl Content + use<'_> { - //self.project.view_scenes_clips() - //} - //pub fn view_tracks_inputs <'a> (&'a self) -> impl Content + use<'a> { - //Fixed::y(1 + self.project.midi_ins.len() as u16, - //self.project.view_inputs(self.color)) - //} - //pub fn view_tracks_outputs <'a> (&'a self) -> impl Content + use<'a> { - //self.project.view_outputs(self.color) - //} - //pub fn view_tracks_devices <'a> (&'a self) -> impl Content + use<'a> { - //Fixed::y(4, self.project.view_track_devices(self.color)) - //} - //pub fn view_tracks_names <'a> (&'a self) -> impl Content + use<'a> { - //Fixed::y(2, self.project.view_track_names(self.color)) - //} - //pub fn view_pool (&self) -> impl Content + use<'_> { - //Fixed::x(20, Bsp::s( - //Fill::x(Align::w(FieldH(self.color, "Clip pool:", ""))), - //Fill::y(Align::n(Tui::bg(Rgb(0, 0, 0), Outer(true, Style::default().fg(Tui::g(96))) - //.enclose(PoolView(&self.pool))))))) - //} - //pub fn view_samples_keys (&self) -> impl Content + use<'_> { - //self.project.sampler().map(|s|s.view_list(true, self.editor().unwrap())) - //} - //pub fn view_samples_grid (&self) -> impl Content + use<'_> { - //self.project.sampler().map(|s|s.view_grid()) - //} - //pub fn view_sample_viewer (&self) -> impl Content + use<'_> { - //self.project.sampler().map(|s|s.view_sample(self.editor().unwrap().get_note_pos())) - //} - //pub fn view_sample_info (&self) -> impl Content + use<'_> { - //self.project.sampler().map(|s|s.view_sample_info(self.editor().unwrap().get_note_pos())) - //} - //pub fn view_sample_status (&self) -> impl Content + use<'_> { - //self.project.sampler().map(|s|Outer(true, Style::default().fg(Tui::g(96))).enclose( - //Fill::y(Align::n(s.view_sample_status(self.editor().unwrap().get_note_pos()))))) - //} - ////let options = ||["Projects", "Settings", "Help", "Quit"].iter(); - ////let option = |a,i|Tui::fg(Rgb(255,255,255), format!("{}", a)); - ////Bsp::s(Tui::bold(true, "tek!"), Bsp::s("", Map::south(1, options, option))) - -//#[dizzle::ns] -//#[dizzle::ns::include(TuiOut)] -//trait AppApi { - //#[dizzle::ns::word("sessions")] - //fn view_sessions (&self) -> Box> { - //Min::x(30, Fixed::y(6, Stack::south( - //move|add: &mut dyn FnMut(&dyn Render)|{ - //let fg = Rgb(224, 192, 128); - //for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { - //let bg = if index == 0 { Rgb(48,64,32) } else { Rgb(16, 32, 24) }; - //add(&Fixed::y(2, Fill::x(Tui::bg(bg, Align::w(Tui::fg(fg, name)))))); - //} - //} - //))).into() - //} - //#[dizzle::ns::expr("bold")] - //fn view_bold (&self, value: bool, x: Box>) -> Box> { - //Box::new(Tui::bold(value, x)) - //} -//} - -/////////////////////////////////////////////////////////////////////////////////////////////////// -//has_editor!(|self: App|{ - //editor = self.editor; - //editor_w = { - //let size = self.size.w(); - //let editor = self.editor.as_ref().expect("missing editor"); - //let time_len = editor.time_len().get(); - //let time_zoom = editor.time_zoom().get().max(1); - //(5 + (time_len / time_zoom)).min(size.saturating_sub(20)).max(16) - //}; - //editor_h = 15; - //is_editing = self.editor.is_some(); -//}); diff --git a/tek/tek_bind.rs b/tek/tek_bind.rs deleted file mode 100644 index c7b7e813..00000000 --- a/tek/tek_bind.rs +++ /dev/null @@ -1 +0,0 @@ -use crate::*;