diff --git a/Cargo.lock b/Cargo.lock index 911a4403..3f2d7093 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -723,6 +723,7 @@ dependencies = [ "dizzle_proc", "itertools 0.14.0", "konst", + "peg", "thiserror 2.0.19", ] @@ -875,6 +876,15 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "futures" version = "0.3.33" @@ -1283,6 +1293,26 @@ dependencies = [ "rustversion", ] +[[package]] +name = "inotify" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "153be1941a183ec9ccd095ddbe17a8b8d435ef6c76e9e02451b933c3999af2c8" +dependencies = [ + "bitflags 2.13.1", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d" +dependencies = [ + "libc", +] + [[package]] name = "instability" version = "0.3.12" @@ -1464,6 +1494,26 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00af7901ba50898c9e545c24d5c580c96a982298134e8037d8978b6594782c07" +[[package]] +name = "kqueue" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "273c0752728918e0ac4976f2b275b6fefb9ecd400585dec929419f3844cd87b5" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" +dependencies = [ + "bitflags 2.13.1", + "libc", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1673,6 +1723,45 @@ dependencies = [ "jni-sys 0.3.1", ] +[[package]] +name = "notify" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +dependencies = [ + "bitflags 2.13.1", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + +[[package]] +name = "notify-debouncer-mini" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17849edfaabd9a5fef1c606d99cfc615a8e99f7ac4366406d86c7942a3184cf2" +dependencies = [ + "log", + "notify", + "notify-types", + "tempfile", +] + +[[package]] +name = "notify-types" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" +dependencies = [ + "bitflags 2.13.1", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2051,6 +2140,33 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "peg" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aad070be5b63aa72103f2fcdd70a83adbd5e90112ce5b574171ff1c65501773" +dependencies = [ + "peg-macros", + "peg-runtime", +] + +[[package]] +name = "peg-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd8ef6825cae95355031ae26a99b616a2a21f22ba2de0197c43dfb05acbe7ee" +dependencies = [ + "peg-runtime", + "proc-macro2", + "quote", +] + +[[package]] +name = "peg-runtime" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7011d97b484a5ebdc4b1fdb3b12d5e4bbbea56e9d22b688f2e79e04b65a7d8a6" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -3035,7 +3151,8 @@ dependencies = [ "gtk", "konst", "livi", - "midly", + "notify", + "notify-debouncer-mini", "palette", "proptest", "proptest-derive", @@ -3074,6 +3191,7 @@ dependencies = [ "crossterm 0.29.0", "dizzle", "jack", + "midly", "palette", "quanta", "rand 0.8.7", @@ -3638,7 +3756,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -3647,7 +3765,16 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", ] [[package]] @@ -3665,14 +3792,31 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -3681,48 +3825,96 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winit" version = "0.30.13" diff --git a/Cargo.toml b/Cargo.toml index 8453c345..0e1ba1bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,6 @@ backtrace = { version = "0.3.72" } builder-pattern = { version = "0.4.2" } bumpalo = { version = "3.19.0" } konst = { version = "0.3.16", features = [ "rust_1_83" ] } -midly = { version = "0.5" } palette = { version = "0.7.6", features = [ "random" ] } quanta = { version = "0.12.3" } rand = { version = "0.8.5" } @@ -36,6 +35,8 @@ winit = { optional = true, version = "0.30.4", features = [ "x11" ]} wavers = { optional = true, version = "1.4.3" } uuid = { optional = true, version = "1.10.0", features = [ "v4" ] } gtk = { optional = true, version = "0.18.1" } +notify = "8.2.0" +notify-debouncer-mini = "0.7.0" #once_cell = "1.19.0" #no_deadlocks = "1.3.2" diff --git a/Justfile b/Justfile index 21326baa..33f077c9 100644 --- a/Justfile +++ b/Justfile @@ -1,4 +1,4 @@ -export RUSTFLAGS := "--cfg procmacro2_semver_exempt -Zmacro-backtrace -Clink-arg=-fuse-ld=mold" +#export RUSTFLAGS := "--cfg procmacro2_semver_exempt -Zmacro-backtrace -Clink-arg=-fuse-ld=mold" export RUST_BACKTRACE := "1" default +ARGS="new": diff --git a/src/app.rs b/src/app.rs index 10827db7..a583e394 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,11 +1,25 @@ use crate::*; pub mod audio; #[allow(unused)] pub use self::audio::*; pub mod bind; pub use self::bind::*; -pub mod cli; pub use self::cli::*; pub mod config; pub use self::config::*; pub mod draw; pub use self::draw::*; pub mod size; pub use self::size::*; - +primitive!(u8: try_to_u8); +primitive!(u16: try_to_u16); +primitive!(usize: try_to_usize); +primitive!(isize: try_to_isize); +impl_has!(Clock: |self: App|self.project.clock); +impl_has!(Vec: |self: App|self.project.midi_ins); +impl_has!(Vec: |self: App|self.project.midi_outs); +impl_has!(Dialog: |self: App|self.dialog); +impl_has!(Jack<'static>: |self: App|self.jack); +impl_has!(Pool: |self: App|self.pool); +impl_has!(Selection: |self: App|self.project.selection); +impl_as_ref!(Vec: |self: App|self.project.as_ref()); +impl_as_mut!(Vec: |self: App|self.project.as_mut()); +impl_as_ref_opt!(MidiEditor: |self: App|self.project.as_ref_opt()); +impl_as_mut_opt!(MidiEditor: |self: App|self.project.as_mut_opt()); +impl_has_clips!( |self: App|self.pool.clips); /// Total application state. /// /// ``` @@ -33,16 +47,16 @@ pub mod size; pub use self::size::*; #[namespace(u8)] #[namespace(isize)] #[namespace(ItemTheme)] -#[namespace(Arc get_arc_str)] -#[namespace(u16 get_u16)] -#[namespace(usize get_usize)] -#[namespace(bool get_bool)] -#[namespace(Selection get_selection)] -#[namespace(Color get_color)] -#[namespace(Option get_opt_u7)] -#[namespace(Option get_opt_u16)] -#[namespace(Option get_opt_usize)] -#[namespace(Option>> get_clip)] +#[namespace(Arc App::get_arc_str)] +#[namespace(u16 App::get_u16)] +#[namespace(usize App::get_usize)] +#[namespace(bool App::get_bool)] +#[namespace(Selection App::get_selection)] +#[namespace(Color App::get_color)] +#[namespace(Option App::get_opt_u7)] +#[namespace(Option App::get_opt_u16)] +#[namespace(Option App::get_opt_usize)] +#[namespace(Option>> App::get_clip)] pub struct App { /// Base color. pub color: ItemTheme, @@ -67,128 +81,7 @@ pub struct App { /// Error, if any pub error: Arc>>> } - -fn get_arc_str (_state: &App, src: impl Language) -> Perhaps> { - Ok(src.src()?.map(|x|x.into())) -} - -fn get_u16 (state: &App, src: impl Language) -> Perhaps { - Ok(Some(match src.word()? { - Some(":w/sidebar") => state.project.w_sidebar(state.editor().is_some()), - Some(":h/sample-detail") => 6.max(state.size.h() as u16 * 3 / 9), - _ => return try_to_u16(src) - })) -} - -fn get_usize (state: &App, src: impl Language) -> Perhaps { - Ok(Some(match src.word()? { - Some(":scene-count") => state.scenes().len(), - Some(":track-count") => state.tracks().len(), - Some(":device-kind") => state.dialog.device_kind().unwrap_or(0), - Some(":device-kind/next") => state.dialog.device_kind_next().unwrap_or(0), - Some(":device-kind/prev") => state.dialog.device_kind_prev().unwrap_or(0), - _ => return try_to_usize(src) - })) -} - -fn get_bool (state: &App, src: impl Language) -> Perhaps { - src.word()?.map(|word|Ok(match word { - "Y" => true, - "N" => false, - ":mode/editor" => state.project.editor.is_some(), - ":focused/dialog" => !matches!(state.dialog, Dialog::None), - ":focused/message" => matches!(state.dialog, Dialog::Message(..)), - ":focused/add_device" => matches!(state.dialog, Dialog::Device(..)), - ":focused/browser" => state.dialog.browser().is_some(), - ":focused/pool/import" => matches!(state.pool.mode, Some(PoolMode::Import(..))), - ":focused/pool/export" => matches!(state.pool.mode, Some(PoolMode::Export(..))), - ":focused/pool/rename" => matches!(state.pool.mode, Some(PoolMode::Rename(..))), - ":focused/pool/length" => matches!(state.pool.mode, Some(PoolMode::Length(..))), - ":focused/clip" => !state.editor_focused() && matches!(state.selection(), Selection::TrackClip{..}), - ":focused/track" => !state.editor_focused() && matches!(state.selection(), Selection::Track(..)), - ":focused/scene" => !state.editor_focused() && matches!(state.selection(), Selection::Scene(..)), - ":focused/mix" => !state.editor_focused() && matches!(state.selection(), Selection::Mix), - _ => return Err(format!("not bool: {word}").into()) - })).transpose() -} - -fn get_selection (state: &App, src: impl Language) -> Perhaps { - src.word()?.map(|word|Ok(match word { - ":select/scene" => state.selection().select_scene(state.tracks().len()), - ":select/scene/next" => state.selection().select_scene_next(state.scenes().len()), - ":select/scene/prev" => state.selection().select_scene_prev(), - ":select/track" => state.selection().select_track(state.tracks().len()), - ":select/track/next" => state.selection().select_track_next(state.tracks().len()), - ":select/track/prev" => state.selection().select_track_prev(), - _ => return Err(format!("not selection: {word}").into()) - })).transpose() -} - -fn get_color (_state: &App, src: impl Language) -> Perhaps { - Ok(if let Ok(Some(expr)) = src.expr() { - match expr.head()? { - Some("g") => { - let n = try_to_u8(expr.tail())?.ok_or::>( - format!("not gray: {expr}").into() - )?; - Some(Color::Rgb(n, n, n)) - }, - Some("rgb") => { - let r = try_to_u8(expr.tail().head())?.ok_or::>( - format!("not red: {expr}").into() - )?; - let g = try_to_u8(expr.tail().tail().head())?.ok_or::>( - format!("not green: {expr}").into() - )?; - let b = try_to_u8(expr.tail().tail().tail().head())?.ok_or::>( - format!("not blue: {expr}").into() - )?; - Some(Color::Rgb(r, g, b)) - }, - Some(_) => return Err(format!("not a color expression: {expr}").into()), - None => return Err(format!("not a color expression: {expr}").into()) - } - } else if let Ok(Some(sym)) = src.word() { - match sym { - ":color/bg" => Some(Color::Rgb(28, 32, 36)), - _ => return Err(format!("not a color: {sym}").into()) - } - } else { - return Err(format!("not a color: {:?}", src.src()?).into()) - }) -} - -fn get_opt_u7 (state: &App, src: impl Language) -> Perhaps> { - src.word()?.map(|word|Ok(match word { - ":editor/pitch" => Some(( - state.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8 - ).into()), - _ => return Err(format!("unknown midi note: {word}").into()) - })).transpose() -} - -fn get_opt_u16 (_state: &App, _src: impl Language) -> Perhaps> { - Ok(None) -} - -fn get_opt_usize (state: &App, src: impl Language) -> Perhaps> { - src.word()?.map(|word|Ok(match word { - ":selected/scene" => state.selection().scene(), - ":selected/track" => state.selection().track(), - _ => return Err(format!("unknown opt: {word}").into()) - })).transpose() -} - -fn get_clip (state: &App, src: impl Language) -> Perhaps>>> { - src.word()?.map(|word|Ok(match word { - ":selected/clip" if let Selection::TrackClip { track, scene } = state.selection() => - state.scenes()[*scene].clips[*track].clone(), - _ => return Err(format!("not a clip: {word}").into()) - })).transpose() -} - impl App { - /// Create a new application instance from a backend, project, config, and mode /// /// ``` @@ -212,36 +105,6 @@ impl App { ..Default::default() } } - - pub fn confirm (&mut self) -> Perhaps { - Ok(match &self.dialog { - Dialog::Menu(index, items) => { - let callback = items.0[*index].1.clone(); - callback(self)?; - None - }, - _ => todo!(), - }) - } - - pub fn inc (&mut self, axis: &ControlAxis) -> Perhaps { - Ok(match (&self.dialog, axis) { - (Dialog::None, _) => todo!(), - (Dialog::Menu(_, _), ControlAxis::Y) => - AppCommand::SetDialog { dialog: self.dialog.menu_next() }.act(self)?, - _ => todo!() - }) - } - - pub fn dec (&mut self, axis: &ControlAxis) -> Perhaps { - Ok(match (&self.dialog, axis) { - (Dialog::None, _) => None, - (Dialog::Menu(_, _), ControlAxis::Y) => - AppCommand::SetDialog { dialog: self.dialog.menu_prev() }.act(self)?, - _ => todo!() - }) - } - /// Update memoized render of clock values. /// ``` /// tek::App::default().update_clock(); @@ -360,27 +223,139 @@ impl App { } } } + fn get_arc_str (&self, src: impl Language) -> Perhaps> { + Ok(src.src()?.map(|x|x.into())) + } + fn get_u16 (&self, src: impl Language) -> Perhaps { + Ok(Some(match src.word()? { + Some(":w/sidebar") => self.project.w_sidebar(self.editor().is_some()), + Some(":h/sample-detail") => 6.max(self.size.h() as u16 * 3 / 9), + _ => return try_to_u16(src) + })) + } + fn get_usize (&self, src: impl Language) -> Perhaps { + Ok(Some(match src.word()? { + Some(":scene-count") => self.scenes().len(), + Some(":track-count") => self.tracks().len(), + Some(":device-kind") => self.dialog.device_kind().unwrap_or(0), + Some(":device-kind/next") => self.dialog.device_kind_next().unwrap_or(0), + Some(":device-kind/prev") => self.dialog.device_kind_prev().unwrap_or(0), + _ => return try_to_usize(src) + })) + } + fn get_bool (&self, src: impl Language) -> Perhaps { + src.word()?.map(|word|Ok(match word { + "Y" => true, + "N" => false, + ":mode/editor" => self.project.editor.is_some(), + ":focused/dialog" => !matches!(self.dialog, Dialog::None), + ":focused/message" => matches!(self.dialog, Dialog::Message(..)), + ":focused/add_device" => matches!(self.dialog, Dialog::Device(..)), + ":focused/browser" => self.dialog.browser().is_some(), + ":focused/pool/import" => matches!(self.pool.mode, Some(PoolMode::Import(..))), + ":focused/pool/export" => matches!(self.pool.mode, Some(PoolMode::Export(..))), + ":focused/pool/rename" => matches!(self.pool.mode, Some(PoolMode::Rename(..))), + ":focused/pool/length" => matches!(self.pool.mode, Some(PoolMode::Length(..))), + ":focused/clip" => !self.editor_focused() && matches!(self.selection(), Selection::TrackClip{..}), + ":focused/track" => !self.editor_focused() && matches!(self.selection(), Selection::Track(..)), + ":focused/scene" => !self.editor_focused() && matches!(self.selection(), Selection::Scene(..)), + ":focused/mix" => !self.editor_focused() && matches!(self.selection(), Selection::Mix), + _ => return Err(format!("not bool: {word}").into()) + })).transpose() + } + fn get_selection (&self, src: impl Language) -> Perhaps { + src.word()?.map(|word|Ok(match word { + ":select/scene" => self.selection().select_scene(self.tracks().len()), + ":select/scene/next" => self.selection().select_scene_next(self.scenes().len()), + ":select/scene/prev" => self.selection().select_scene_prev(), + ":select/track" => self.selection().select_track(self.tracks().len()), + ":select/track/next" => self.selection().select_track_next(self.tracks().len()), + ":select/track/prev" => self.selection().select_track_prev(), + _ => return Err(format!("not selection: {word}").into()) + })).transpose() + } + fn get_color (&self, src: impl Language) -> Perhaps { + if let Some(expr) = src.expr()? { + match (expr.head()?, expr.tail()?) { + (Some("g"), Some(tail)) => { + let n = try_to_u8(expr.tail().map_err(Into::into))?.ok_or(LanguageError::Domain("not gray"))?; + Ok(Some(Color::Rgb(n, n, n))) + }, + (Some("rgb"), Some(tail)) => { + let r = try_to_u8(expr.tail().map_err(Into::into))? + .ok_or(LanguageError::Domain("not red"))?; + let g = try_to_u8(expr.tail().tail().head().map_err(Into::into))? + .ok_or(LanguageError::Domain("not green"))?; + let b = try_to_u8(expr.tail().tail().tail().head().map_err(Into::into))? + .ok_or(LanguageError::Domain("not blue"))?; + Ok(Some(Color::Rgb(r, g, b))) + }, + (Some(_), _) => return Err(format!("not a color expression: {expr}").into()), + (None, _) => return Err(format!("not a color expression: {expr}").into()), + } + } else if let Ok(Some(sym)) = src.word() { + Ok(match sym { + ":color/bg" => Some(Color::Rgb(28, 32, 36)), + ":color/fg" => Some(Color::Rgb(98, 92, 96)), + _ => return Err(format!("not a color: {sym}").into()) + }) + } else { + return Err(format!("not a color: {:?}", src.src()?).into()) + } + } + fn get_opt_u7 (&self, src: impl Language) -> Perhaps> { + src.word()?.map(|word|Ok(match word { + ":editor/pitch" => Some(( + self.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8 + ).into()), + _ => return Err(format!("unknown midi note: {word}").into()) + })).transpose() + } + fn get_opt_u16 (&self, _src: impl Language) -> Perhaps> { + Ok(None) + } + fn get_opt_usize (&self, src: impl Language) -> Perhaps> { + src.word()?.map(|word|Ok(match word { + ":selected/scene" => self.selection().scene(), + ":selected/track" => self.selection().track(), + _ => return Err(format!("unknown opt: {word}").into()) + })).transpose() + } + fn get_clip (&self, src: impl Language) -> Perhaps>>> { + src.word()?.map(|word|Ok(match word { + ":selected/clip" if let Selection::TrackClip { track, scene } = self.selection() => + self.scenes()[*scene].clips[*track].clone(), + _ => return Err(format!("not a clip: {word}").into()) + })).transpose() + } + pub fn inc (&mut self, axis: &ControlAxis) -> Perhaps { + Ok(match (&self.dialog, axis) { + (Dialog::None, _) => todo!(), + (Dialog::Menu(_, _), ControlAxis::Y) => + AppCommand::SetDialog { dialog: self.dialog.menu_next() }.act(self)?, + _ => todo!() + }) + } + pub fn dec (&mut self, axis: &ControlAxis) -> Perhaps { + Ok(match (&self.dialog, axis) { + (Dialog::None, _) => None, + (Dialog::Menu(_, _), ControlAxis::Y) => + AppCommand::SetDialog { dialog: self.dialog.menu_prev() }.act(self)?, + _ => todo!() + }) + } + pub fn confirm (&mut self) -> Perhaps { + Ok(match &self.dialog { + Dialog::Menu(index, items) => { + let callback = items.0[*index].1.clone(); + callback(self)?; + None + }, + _ => todo!(), + }) + } } -impl_has!(Clock: |self: App|self.project.clock); -impl_has!(Vec: |self: App|self.project.midi_ins); -impl_has!(Vec: |self: App|self.project.midi_outs); -impl_has!(Dialog: |self: App|self.dialog); -impl_has!(Jack<'static>: |self: App|self.jack); -impl_has!(Pool: |self: App|self.pool); -impl_has!(Selection: |self: App|self.project.selection); -impl_as_ref!(Vec: |self: App|self.project.as_ref()); -impl_as_mut!(Vec: |self: App|self.project.as_mut()); -impl_as_ref_opt!(MidiEditor: |self: App|self.project.as_ref_opt()); -impl_as_mut_opt!(MidiEditor: |self: App|self.project.as_mut_opt()); -impl_has_clips!( |self: App|self.pool.clips); - -primitive!(u8: try_to_u8); -primitive!(u16: try_to_u16); -primitive!(usize: try_to_usize); -primitive!(isize: try_to_isize); - - pub fn swap_value ( target: &mut T, value: &T, returned: impl Fn(T)->U ) -> Perhaps { @@ -421,4 +396,3 @@ pub fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { files.sort(); Ok((subdirs, files)) } - diff --git a/src/app/bind.rs b/src/app/bind.rs index 01628e03..c1a514f8 100644 --- a/src/app/bind.rs +++ b/src/app/bind.rs @@ -1,6 +1,6 @@ use crate::*; -tui_keys!(|self: App, input| { +tui_keys!(self: App, input { let commands = tek_commands_collect(self, input)?; let results = tek_commands_execute(self, commands)?; self.history.extend(results.into_iter()); diff --git a/src/app/cli.rs b/src/app/cli.rs deleted file mode 100644 index 61250d14..00000000 --- a/src/app/cli.rs +++ /dev/null @@ -1,259 +0,0 @@ -use crate::*; - -/// The command-line interface descriptor. -/// -/// ``` -/// let cli: tek::Cli = Default::default(); -/// -/// use clap::CommandFactory; -/// tek::Cli::command().debug_assert(); -/// ``` -#[derive(Parser)] -#[command(name = "tek", version, about = Some(HEADER), long_about = Some(HEADER))] -#[derive(Debug, Default)] pub struct Cli { - /// Pre-defined configuration modes. - /// - /// TODO: Replace these with scripted configurations. - #[command(subcommand)] pub action: Action, -} - -/// Application modes that can be passed to the mommand line interface. -/// -/// ``` -/// let action: tek::Action = Default::default(); -/// ``` -#[derive(Debug, Clone, Subcommand, Default)] pub enum Action { - /// Continue where you left off - #[default] Resume, - /// Run headlessly in current session. - Headless, - /// Show status of current session. - Status, - /// List known sessions. - List, - /// Continue work in a copy of the current session. - Fork, - /// Create a new empty session. - New { - /// Name of JACK client - #[arg(short='n', long)] name: Option, - /// Whether to attempt to become transport master - #[arg(short='Y', long, default_value_t = false)] sync_lead: bool, - /// Whether to sync to external transport master - #[arg(short='y', long, default_value_t = true)] sync_follow: bool, - /// Initial tempo in beats per minute - #[arg(short='b', long, default_value = None)] bpm: Option, - /// Whether to include a transport toolbar (default: true) - #[arg(short='c', long, default_value_t = true)] show_clock: bool, - /// MIDI outs to connect to (multiple instances accepted) - #[arg(short='I', long)] midi_from: Vec, - /// MIDI outs to connect to (multiple instances accepted) - #[arg(short='i', long)] midi_from_re: Vec, - /// MIDI ins to connect to (multiple instances accepted) - #[arg(short='O', long)] midi_to: Vec, - /// MIDI ins to connect to (multiple instances accepted) - #[arg(short='o', long)] midi_to_re: Vec, - /// Audio outs to connect to left input - #[arg(short='l', long)] left_from: Vec, - /// Audio outs to connect to right input - #[arg(short='r', long)] right_from: Vec, - /// Audio ins to connect from left output - #[arg(short='L', long)] left_to: Vec, - /// Audio ins to connect from right output - #[arg(short='R', long)] right_to: Vec, - /// Tracks to creat - #[arg(short='t', long)] tracks: Option, - /// Scenes to create - #[arg(short='s', long)] scenes: Option, - }, - /// Import media as new session. - Import, - /// Show configuration. - Config, - /// Show version. - Version, -} - -/// Command-line configuration. -#[cfg(feature = "cli")] -impl Cli { - pub fn run (&self) -> Usually<()> { - self.action.run(Config::init_new(None)?) - } -} - -#[cfg(feature = "cli")] -impl Action { - fn run (&self, config: Config) -> Usually<()> { - use Action::*; - match self { - Version => tek_show_version(), - Config => tek_print_config(&config), - List => todo!("list sessions"), - Resume => todo!("resume session"), - New { - name, bpm, tracks, scenes, sync_lead, sync_follow, - midi_from: mf, midi_from_re: mfr, midi_to: mt, midi_to_re: mtr, - left_from: lf, right_from: rf, left_to: lt, right_to: rt, .. - } => { - let name = name.as_ref().map_or("tek", |x|x.as_str()); - let jack = Jack::new(&name)?; - let mut proj = tek_project_new( - &jack, Clock::new(&jack, *bpm)?, &lf, <, &rf, &rt, &mf, &mt, &mfr, &mtr - )?; - proj.tracks_add(tracks.unwrap_or(0), None, &[], &[])?; - proj.scenes_add(scenes.unwrap_or(0))?; - //if matches!(self, Action::Status) { - //// Show status and exit - //tek_print_status(&proj); - //return Ok(()) - //} - // Initialize the app state - let app = Arc::new(RwLock::new(App::new(&jack, proj, config, ":menu"))); - //if matches!(self, Action::Headless) { - //// TODO: Headless mode (daemon + client over IPC, then over network...) - //println!("todo headless"); - //return Ok(()) - //} - let (_keyboard, _terminal) = Exit::run(|exited|tui_io( - exited.as_ref(), - &app, - Duration::from_millis(100), - Duration::from_millis(10), - std::io::stdout() - ))?; - // Run the [Tui] and [Jack] threads with the [App] state. - //Tui::new(Box::new(std::io::stdout()))?.run(true, &jack.run(move|jack|{ - //// Between jack init and app's first cycle: - //jack.sync_lead(*sync_lead, |mut state|{ - //let clock = app.clock(); - //clock.playhead.update_from_sample(state.position.frame() as f64); - //state.position.bbt = Some(clock.bbt()); - //state.position - //})?; - //jack.sync_follow(*sync_follow)?; - //// FIXME: They don't work properly. - //Ok(app) - //})?)?; - }, - _ => todo!() - } - Ok(()) - } -} - -pub fn tek_project_new ( - jack: &Jack<'static>, - clock: Clock, - left_from: &[impl AsRef], - left_to: &[impl AsRef], - right_from: &[impl AsRef], - right_to: &[impl AsRef], - midi_from: &[impl AsRef], - midi_to: &[impl AsRef], - midi_from_re: &[impl AsRef], - midi_to_re: &[impl AsRef], -) -> Usually { - // TODO: Collect audio IO: - let empty = &[] as &[&str]; - let left_froms = Connect::collect(left_from, empty, empty); - let left_tos = Connect::collect(left_to, empty, empty); - let right_froms = Connect::collect(right_from, empty, empty); - let right_tos = Connect::collect(right_to, empty, empty); - let _audio_froms = &[left_froms.as_slice(), right_froms.as_slice()]; - let _audio_tos = &[left_tos.as_slice(), right_tos.as_slice()]; - Ok(Arrangement::new( - jack, - None, - clock, - vec![], - vec![], - Connect::collect(&midi_from, &[] as &[&str], &midi_from_re).iter().enumerate() - .map(|(index, connect)|jack.midi_in(&format!("M/{index}"), &[connect.clone()])) - .collect::, _>>()?, - Connect::collect(&midi_to, &[] as &[&str], &midi_to_re).iter().enumerate() - .map(|(index, connect)|jack.midi_out(&format!("{index}/M"), &[connect.clone()])) - .collect::, _>>()? - )) -} - -pub fn tek_show_version () { - println!("todo version"); -} - -pub fn tek_print_config (config: &Config) { - use ::ansi_term::Color::*; - println!("{:?}", config.dirs); - for (k, v) in config.views.read().unwrap().iter() { - println!("{} {} {v}", Green.paint("VIEW"), Green.bold().paint(format!("{k:<16}"))); - } - for (k, v) in config.binds.read().unwrap().iter() { - println!("{} {}", Green.paint("BIND"), Green.bold().paint(format!("{k:<16}"))); - for (k, v) in v.0.iter() { - print!("{} ", &Yellow.paint(match &k.0 { - Event::Key(KeyEvent { modifiers, .. }) => - format!("{:>16}", format!("{modifiers}")), - _ => unimplemented!() - })); - print!("{}", &Yellow.bold().paint(match &k.0 { - Event::Key(KeyEvent { code, .. }) => - format!("{:<10}", format!("{code}")), - _ => unimplemented!() - })); - for v in v.iter() { - print!(" => {:?}", v.commands); - print!(" {}", v.condition.as_ref().map(|x|format!("{x:?}")).unwrap_or_default()); - println!(" {}", v.description.as_ref().map(|x|x.as_ref()).unwrap_or_default()); - //println!(" {:?}", v.source); - } - } - } - config.modes.for_each(|k, v|{ - println!(); - for v in v.name.iter() { print!("{}", Green.bold().paint(format!("{v} "))); } - for v in v.info.iter() { print!("\n{}", Green.paint(format!("{v}"))); } - print!("\n{} {}", Blue.paint("TOOL"), Green.bold().paint(format!("{k:<16}"))); - print!("\n{}", Blue.paint("KEYS")); - for v in v.keys.iter() { print!("{}", Green.paint(format!(" {v}"))); } - println!(); - v.modes.for_each(|k, v|{ - print!("{} {} {:?}", Blue.paint("MODE"), Green.bold().paint(format!("{k:<16}")), v.name); - print!( " INFO={:?}", v.info); - print!( " VIEW={:?}", v.view); - println!(" KEYS={:?}", v.keys); - }); - print!("{}", Blue.paint("VIEW")); - for v in v.view.iter() { print!("{}", Green.paint(format!(" {v}"))); } - println!(); - }); -} - -pub fn tek_print_status (project: &Arrangement) { - println!("Name: {:?}", &project.name); - println!("JACK: {:?}", &project.jack); - println!("Buffer: {:?}", &project.clock.chunk); - println!("Sample rate: {:?}", &project.clock.timebase.sr); - println!("MIDI PPQ: {:?}", &project.clock.timebase.ppq); - println!("Tempo: {:?}", &project.clock.timebase.bpm); - println!("Quantize: {:?}", &project.clock.quant); - println!("Launch: {:?}", &project.clock.sync); - println!("Playhead: {:?}us", &project.clock.playhead.usec); - println!("Playhead: {:?}s", &project.clock.playhead.sample); - println!("Playhead: {:?}p", &project.clock.playhead.pulse); - println!("Started: {:?}", &project.clock.started); - println!("Tracks:"); - for (i, t) in project.tracks.iter().enumerate() { - println!(" Track {i}: {} {} {:?} {:?}", t.name, t.width, - &t.sequencer.play_clip, &t.sequencer.next_clip); - } - println!("Scenes:"); - for (i, t) in project.scenes.iter().enumerate() { - println!(" Scene {i}: {} {:?}", &t.name, &t.clips); - } - println!("MIDI Ins: {:?}", &project.midi_ins); - println!("MIDI Outs: {:?}", &project.midi_outs); - println!("Audio Ins: {:?}", &project.audio_ins); - println!("Audio Outs: {:?}", &project.audio_outs); - // TODO git integration - // TODO dawvert integration -} diff --git a/src/app/config.rs b/src/app/config.rs index fab190fa..ff935638 100644 --- a/src/app/config.rs +++ b/src/app/config.rs @@ -38,8 +38,11 @@ impl Config { const DEFAULTS: &'static str = include_str!("../tek.edn"); pub fn watch (callback: impl FnOnce(Self)->T) -> Usually { - let config = Self::init_new(None)?; - let result = callback(config); + let config = Self::init_new(None)?; + let watcher = notify_debouncer_mini::new_debouncer(Duration::from_millis(500), |res| { + println!("{res:?}"); + })?; + let result = callback(config); Ok(result) } diff --git a/src/app/draw.rs b/src/app/draw.rs index a92af05b..3f439d12 100644 --- a/src/app/draw.rs +++ b/src/app/draw.rs @@ -47,7 +47,7 @@ fn tek_draw_expr (state: &App, to: &mut Tui, lang: &impl Expression) -> Drawn Drawn { let mut frags = dsl.src()?.unwrap().split("/"); match frags.next() { - Some(":logo") => view_logo().draw(to), + //Some(":logo") => view_logo().draw(to), Some(":meters") => draw_meter_section(to, frags), Some(":tracks") => draw_tracks(to, frags, state), Some(":scenes") => draw_scenes(to, frags), @@ -58,6 +58,7 @@ fn tek_draw_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Drawn view_device(state).draw(to), Some(":status") => "TODO: Status Bar".exact_h(1).draw(to), Some(":editor") => "TODO Editor".draw(to), + Some(":transport") => view_transport(true, "", "", "").draw(to), Some(":debug") => format!("[{:?}]", to.area()).exact_h(1).draw(to), Some(_) => { let views = state.config.views.read().unwrap(); @@ -111,7 +112,7 @@ pub fn draw_dialog ( let f = if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) }; let b = if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) }; fg_bg(f, b, item.full_w().align_w().exact_h(2)) - .push_y((2 * index) as u16).draw(to)?; + .push_y((4 * index) as u16).draw(to)?; } Ok(Some(to.area().into())) }).full_wh()) @@ -143,18 +144,6 @@ pub fn draw_templates (to: &mut Tui, frags: std::str::Split<&str>, state: &App) }).min_w(30).exact_h(height).draw(to) } -/// ``` -/// let _ = tek::view_logo(); -/// ``` -pub fn view_logo () -> impl Draw { - fg(Rgb(240, 200, 180), south!{ - "~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~".exact_h(1), - "~~~~ ║ ~ ╟─╌ ~╟─< ~~ v0.3.0 ~~".exact_h(1), - //east("~~~~ ║ ~ ╟─╌ ~╟─< ~~ ", east(fg(Rgb(230,100,40), "v0.3.0"), " ~~")).exact_h(1), - "~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~".exact_h(1), - }.exact_w(32)) -} - /// ``` /// let x = ""; /// let _ = tek::view_transport(true, x.as_ref(), x.as_ref(), x.as_ref()); @@ -163,7 +152,7 @@ pub fn view_logo () -> impl Draw { pub fn view_transport (play: bool, bpm: &str, beat: &str, time: &str) -> impl Draw { let theme = ItemTheme::G[96]; bg(Black, east!(above( - button_play_pause(play, false).align_w().full_wh(), + button_play_pause(play, false).align_w(), east!( field_h(theme, "BPM", bpm), field_h(theme, "Beat", beat), @@ -711,12 +700,12 @@ pub fn per_track_top <'a, T: Draw + 'a, U: TracksSizes<'a>> ( } pub fn field_h ( - theme: ItemTheme, head: impl Draw, body: impl Draw + _theme: ItemTheme, _head: impl Draw, _body: impl Draw ) -> impl Draw { } pub fn field_v ( - theme: ItemTheme, head: impl Draw, body: impl Draw + _theme: ItemTheme, _head: impl Draw, _body: impl Draw ) -> impl Draw { } diff --git a/src/deps.rs b/src/deps.rs index 021f10b4..2f9683a8 100644 --- a/src/deps.rs +++ b/src/deps.rs @@ -1,21 +1,3 @@ -#[cfg(feature = "cli")] -pub(crate) use ::clap::{self, Parser, Subcommand}; - -pub(crate) use ::xdg::BaseDirectories; - -pub(crate) use ::midly::{Smf, TrackEventKind, MidiMessage, Error as MidiError, num::*, live::*}; - -pub(crate) use tengri::{ - *, - lang::*, - crossterm::event::{Event, KeyEvent}, - ratatui::{ - self, - prelude::{Rect, Style, Stylize, Buffer, Color::{self, *}}, - widgets::{Widget, canvas::{Canvas, Line}}, - }, -}; - #[allow(unused)] pub(crate) use ::{ std::{ @@ -31,4 +13,20 @@ pub(crate) use ::{ time::Duration, thread::{spawn, JoinHandle}, }, + xdg::{ + BaseDirectories, + }, + tengri::{ + *, + lang::*, + midly::{Smf, TrackEventKind, MidiMessage, Error as MidiError, num::*, live::*}, + crossterm::event::{Event, KeyEvent}, + ratatui::{ + self, + prelude::{Rect, Style, Stylize, Buffer, Color::{self, *}}, + widgets::{Widget, canvas::{Canvas, Line}}, + }, + }, }; +#[cfg(feature = "cli")] +pub(crate) use ::clap::{self, Parser, Subcommand}; diff --git a/src/device.rs b/src/device.rs index 614a8735..8e842c52 100644 --- a/src/device.rs +++ b/src/device.rs @@ -100,6 +100,7 @@ pub trait HasDevices: AsRef> + AsMut> { } } +pub use ::tengri::sing::*; pub mod arrange; pub use self::arrange::*; pub mod browse; pub use self::browse::*; pub mod clock; pub use self::clock::*; @@ -108,9 +109,36 @@ pub mod editor; pub use self::editor::*; pub mod menu; pub use self::menu::*; pub mod meter; pub use self::meter::*; pub mod mix; pub use self::mix::*; -pub mod port; pub use self::port::*; pub mod sampler; pub use self::sampler::*; pub mod sequence; pub use self::sequence::*; #[cfg(feature = "plugin")] pub mod plugin; #[cfg(feature = "plugin")] pub use self::plugin::*; + +def_command!(AudioInputCommand: |port: AudioInput| { + Close => todo!(), + Connect { audio_out: Arc } => todo!(), +}); + +def_command!(AudioOutputCommand: |port: AudioOutput| { + Close => todo!(), + Connect { audio_in: Arc } => todo!(), +}); + +def_command!(MidiInputCommand: |port: MidiInput| { + Close => todo!(), + Connect { midi_out: Arc } => todo!(), +}); + +def_command!(MidiOutputCommand: |port: MidiOutput| { + Close => todo!(), + Connect { midi_in: Arc } => todo!(), +}); + +pub struct Junction(T); + +impl View for Junction { + fn view (&self) -> impl Draw { + T::KIND + } +} diff --git a/src/device/arrange.rs b/src/device/arrange.rs index e3836693..d54a8b2e 100644 --- a/src/device/arrange.rs +++ b/src/device/arrange.rs @@ -5,7 +5,8 @@ use crate::*; /// ``` /// let arranger = tek::Arrangement::default(); /// ``` -#[derive(Default, Debug)] pub struct Arrangement { +#[derive(Default, Debug)] +pub struct Arrangement { /// Project name. pub name: Arc, /// Base color. @@ -19,6 +20,8 @@ use crate::*; pub size: Sizer, /// Display size of clips area pub size_inner: Sizer, + /// Selected UI element + pub selection: Selection, /// Source of time #[cfg(feature = "clock")] pub clock: Clock, /// Allows one MIDI clip to be edited @@ -31,8 +34,6 @@ use crate::*; #[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 @@ -45,34 +46,58 @@ use crate::*; #[cfg(feature = "scene")] pub scene_scroll: usize, } -impl HasJack<'static> for Arrangement { fn jack (&self) -> &Jack<'static> { &self.jack } } -impl_has!(Jack<'static>: |self: Arrangement| self.jack); -impl_has!(Sizer: |self: Arrangement| self.size); -impl_has!(Vec: |self: Arrangement| self.midi_ins); -impl_has!(Vec: |self: Arrangement| self.midi_outs); -impl_has!(Clock: |self: Arrangement| self.clock); -impl_has!(Selection: |self: Arrangement| self.selection); -impl_as_ref_opt!(MidiEditor: |self: Arrangement| self.editor.as_ref()); -impl_as_mut_opt!(MidiEditor: |self: Arrangement| self.editor.as_mut()); +/// Represents the current user selection in the arranger +#[derive(PartialEq, Clone, Copy, Debug, Default)] +pub enum Selection { + #[default] + /// Nothing is selected + Nothing, + /// The whole mix is selected + Mix, + /// A MIDI input is selected. + Input(usize), + /// A MIDI output is selected. + Output(usize), + /// A scene is selected. + #[cfg(feature = "scene")] Scene(usize), + /// A track is selected. + #[cfg(feature = "track")] Track(usize), + /// A clip (track × scene) is selected. + #[cfg(feature = "track")] TrackClip { track: usize, scene: usize }, + /// A track's MIDI input connection is selected. + #[cfg(feature = "track")] TrackInput { track: usize, port: usize }, + /// A track's MIDI output connection is selected. + #[cfg(feature = "track")] TrackOutput { track: usize, port: usize }, + /// A track device slot is selected. + #[cfg(feature = "track")] TrackDevice { track: usize, device: usize }, +} impl Arrangement { /// Create a new arrangement. pub fn new ( - jack: &Jack<'static>, - name: Option>, - clock: Clock, - tracks: Vec, - scenes: Vec, - midi_ins: Vec, - midi_outs: Vec, + jack: &Jack<'static>, + name: Arc, + clock: Clock, + tracks: impl Iterator, + scenes: impl Iterator, + midi_ins: impl Iterator, + midi_outs: impl Iterator, + audio_ins: impl Iterator, + audio_outs: impl Iterator, ) -> Self { Self { - clock, tracks, scenes, midi_ins, midi_outs, - jack: jack.clone(), - name: name.unwrap_or_default(), - color: ItemTheme::random(), - selection: Selection::TrackClip { track: 0, scene: 0 }, + jack: jack.clone(), + color: ItemTheme::random(), + selection: Selection::TrackClip { track: 0, scene: 0 }, + tracks: tracks.collect(), + scenes: scenes.collect(), + midi_ins: midi_ins.collect(), + midi_outs: midi_outs.collect(), + audio_ins: audio_ins.collect(), + audio_outs: audio_outs.collect(), + clock, + name, ..Default::default() } } @@ -117,7 +142,759 @@ impl Arrangement { } -#[cfg(feature = "clip")] mod clip; #[cfg(feature = "clip")] pub use self::clip::*; -#[cfg(feature = "scene")] mod scene; #[cfg(feature = "scene")] pub use self::scene::*; -#[cfg(feature = "track")] mod track; #[cfg(feature = "track")] pub use self::track::*; -#[cfg(feature = "select")] mod select; #[cfg(feature = "select")] pub use self::select::*; +impl Selection { + pub fn describe ( + &self, + #[cfg(feature = "track")] tracks: &[Track], + #[cfg(feature = "scene")] scenes: &[Scene], + ) -> Arc { + use Selection::*; + format!("{}", match self { + Mix => "Everything".to_string(), + #[cfg(feature = "scene")] Scene(s) => + scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name)).unwrap_or_else(||"S??".into()), + #[cfg(feature = "track")] Track(t) => + tracks.get(*t).map(|track|format!("T{t}: {}", &track.name)).unwrap_or_else(||"T??".into()), + TrackClip { track, scene } => match (tracks.get(*track), scenes.get(*scene)) { + (Some(_), Some(s)) => match s.clip(*track) { + Some(clip) => format!("T{track} S{scene} C{}", &clip.read().unwrap().name), + None => format!("T{track} S{scene}: Empty") + }, + _ => format!("T{track} S{scene}: Empty"), + }, + _ => todo!() + }).into() + } + #[cfg(feature = "scene")] pub fn scene (&self) -> Option { + use Selection::*; + match self { Scene(scene) | TrackClip { scene, .. } => Some(*scene), _ => None } + } + #[cfg(feature = "scene")] pub fn select_scene (&self, scene_count: usize) -> Self { + use Selection::*; + match self { + Mix | Track(_) => Scene(0), + Scene(s) => Scene((s + 1) % scene_count), + TrackClip { scene, .. } => Track(*scene), + _ => todo!(), + } + } + #[cfg(feature = "scene")] pub fn select_scene_next (&self, len: usize) -> Self { + use Selection::*; + match self { + Mix => Scene(0), + Track(t) => TrackClip { track: *t, scene: 0 }, + Scene(s) => if s + 1 < len { Scene(s + 1) } else { Mix }, + TrackClip { track, scene } => if scene + 1 < len { TrackClip { track: *track, scene: scene + 1 } } else { Track(*track) }, + _ => todo!() + } + } + #[cfg(feature = "scene")] pub fn select_scene_prev (&self) -> Self { + use Selection::*; + match self { + Mix | Scene(0) => Mix, + Scene(s) => Scene(s - 1), + Track(t) => Track(*t), + TrackClip { track, scene: 0 } => Track(*track), + TrackClip { track, scene } => TrackClip { track: *track, scene: scene - 1 }, + _ => todo!() + } + } + #[cfg(feature = "track")] pub fn track (&self) -> Option { + use Selection::*; + if let Track(track)|TrackClip{track,..}|TrackInput{track,..}|TrackOutput{track,..}|TrackDevice{track,..} = self { + Some(*track) + } else { + None + } + } + #[cfg(feature = "track")] pub fn select_track (&self, track_count: usize) -> Self { + use Selection::*; + match self { + Mix => Track(0), + Scene(_) => Mix, + Track(t) => Track((t + 1) % track_count), + TrackClip { track, .. } => Track(*track), + _ => todo!(), + } + } + #[cfg(feature = "track")] pub fn select_track_next (&self, len: usize) -> Self { + use Selection::*; + match self { + Mix => Track(0), + Scene(s) => TrackClip { track: 0, scene: *s }, + Track(t) => if t + 1 < len { Track(t + 1) } else { Mix }, + TrackClip {track, scene} => if track + 1 < len { TrackClip { track: track + 1, scene: *scene } } else { Scene(*scene) }, + _ => todo!() + } + } + #[cfg(feature = "track")] pub fn select_track_prev (&self) -> Self { + use Selection::*; + match self { + Mix => Mix, + Scene(s) => Scene(*s), + Track(0) => Mix, + Track(t) => Track(t - 1), + TrackClip { track: 0, scene } => Scene(*scene), + TrackClip { track: t, scene } => TrackClip { track: t - 1, scene: *scene }, + _ => todo!() + } + } +} + +impl +AsMut> HasSelection for T {} + +pub trait HasSelection: AsRef + AsMut { + fn selection (&self) -> &Selection { self.as_ref() } + fn selection_mut (&mut self) -> &mut Selection { self.as_mut() } + /// Get the active track + #[cfg(feature = "track")] + fn selected_track (&self) -> Option<&Track> where Self: HasTracks { + let index = self.selection().track()?; + self.tracks().get(index) + } + /// Get a mutable reference to the active track + #[cfg(feature = "track")] + fn selected_track_mut (&mut self) -> Option<&mut Track> where Self: HasTracks { + let index = self.selection().track()?; + self.tracks_mut().get_mut(index) + } + /// Get the active scene + #[cfg(feature = "scene")] + fn selected_scene (&self) -> Option<&Scene> where Self: HasScenes { + let index = self.selection().scene()?; + self.scenes().get(index) + } + /// Get a mutable reference to the active scene + #[cfg(feature = "scene")] + fn selected_scene_mut (&mut self) -> Option<&mut Scene> where Self: HasScenes { + let index = self.selection().scene()?; + self.scenes_mut().get_mut(index) + } + /// Get the active clip + #[cfg(feature = "clip")] + fn selected_clip (&self) -> Option>> where Self: HasScenes + HasTracks { + self.selected_scene()?.clips.get(self.selection().track()?)?.clone() + } +} + +impl HasJack<'static> for Arrangement { fn jack (&self) -> &Jack<'static> { &self.jack } } +impl_has!(Jack<'static>: |self: Arrangement| self.jack); +impl_has!(Sizer: |self: Arrangement| self.size); +impl_has!(Vec: |self: Arrangement| self.midi_ins); +impl_has!(Vec: |self: Arrangement| self.midi_outs); +impl_has!(Clock: |self: Arrangement| self.clock); +impl_has!(Selection: |self: Arrangement| self.selection); +impl_as_ref_opt!(MidiEditor: |self: Arrangement| self.editor.as_ref()); +impl_as_mut_opt!(MidiEditor: |self: Arrangement| self.editor.as_mut()); + +pub trait HasClipsSize { + fn clips_size (&self) -> &Sizer; +} +pub trait ClipsView: TracksView + ScenesView { + /// Draw clips per scene + fn view_scenes_clips <'a> (&'a self) -> impl Draw + 'a { + view_scenes_clips( + ||self.scenes_with_sizes(), + self.tracks_with_sizes(), + self.selection(), + self.editor(), + self.clips_size(), + self.is_editing(), + ) + } +} + +impl HasClipsSize for App { + fn clips_size (&self) -> &Sizer { &self.project.size_inner } +} + +impl HasClipsSize for Arrangement { + fn clips_size (&self) -> &Sizer { &self.size_inner } +} + +/// TODO: Preserve the generic passthru syntax; +/// remove this macro (only used twice) and potentially the trait. +#[macro_export] macro_rules! impl_has_clips { + (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? { + fn clips <'a> (&'a $self) -> std::sync::RwLockReadGuard<'a, ClipPool> { + $cb.read().unwrap() + } + fn clips_mut <'a> (&'a $self) -> std::sync::RwLockWriteGuard<'a, ClipPool> { + $cb.write().unwrap() + } + } + } +} +#[macro_export] macro_rules! has_clip { + (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? HasMidiClip for $Struct $(<$($L),*$($T),*>)? { + fn clip (&$self) -> Option>> { $cb } + } + } +} +def_command!(ClipCommand: |clip: MidiClip| { + SetColor { color: Option } => { + //(SetColor [t: usize, s: usize, c: ItemTheme] + //clip.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o))))); + //("color" [a: usize, b: usize] Some(Self::SetColor(a.unwrap(), b.unwrap(), ItemTheme::random()))) + todo!() + }, + SetLoop { looping: Option } => { + //(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}")) + //("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap()))) + todo!() + } +}); +impl Arrangement { + /// Put a clip in a slot + pub fn clip_put ( + &mut self, track: usize, scene: usize, clip: Option>> + ) -> Option>> { + let old = self.scenes[scene].clips[track].clone(); + self.scenes[scene].clips[track] = clip; + old + } + + /// Change the color of a clip, returning the previous one + pub fn clip_set_color ( + &self, track: usize, scene: usize, color: ItemTheme + ) -> Option { + self.scenes[scene].clips[track].as_ref().map(|clip|{ + let mut clip = clip.write().unwrap(); + let old = clip.color.clone(); + clip.color = color.clone(); + panic!("{color:?} {old:?}"); + //old + }) + } + + /// Toggle looping for the active clip + pub fn toggle_loop (&mut self) { + if let Some(clip) = self.selected_clip() { + clip.write().unwrap().toggle_loop() + } + } +} + +/// Default scene height. +pub const H_SCENE: usize = 2; + +/// Default editor height. +pub const H_EDITOR: usize = 15; + +/// A scene consists of a set of clips to play together. +/// +/// ``` +/// let scene: tek::Scene = Default::default(); +/// let _ = scene.pulses(); +/// let _ = scene.is_playing(&[]); +/// ``` +#[derive(Debug, Default)] pub struct Scene { + /// Name of scene + pub name: Arc, + /// Identifying color of scene + pub color: ItemTheme, + /// Clips in scene, one per track + pub clips: Vec>>>, +} + +impl Scene { + /// Returns the pulse length of the longest clip in the scene + pub fn pulses (&self) -> usize { + self.clips.iter().fold(0, |a, p|{ + a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0)) + }) + } + /// Returns true if all clips in the scene are + /// currently playing on the given collection of tracks. + pub fn is_playing (&self, tracks: &[Track]) -> bool { + self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate() + .all(|(track_index, clip)|match clip { + Some(c) => tracks + .get(track_index) + .map(|track|{ + if let Some((_, Some(clip))) = track.sequencer().play_clip() { + *clip.read().unwrap() == *c.read().unwrap() + } else { + false + } + }) + .unwrap_or(false), + None => true + }) + } + pub fn clip (&self, index: usize) -> Option<&Arc>> { + match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None } + } + fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } + fn _todo_usize_stub_ (&self) -> usize { todo!() } + fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } + fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } +} + +pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + Send + Sync { + fn h_scenes (&self) -> u16; + fn w_side (&self) -> u16; + fn w_mid (&self) -> u16; + + fn view_scenes_names (&self) -> impl Draw { + view_scenes_names( + self.scenes_with_sizes(), self.selection(), self.editor(), self.is_editing() + ) + } + 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 { + H_SCENE + }; + if y + height <= self.clips_size().h() as usize { + let data = (s, scene, y, y + height); + y += height; + Some(data) + } else { + None + } + }) + } +} + +pub trait HasSceneScroll: HasScenes { + fn scene_scroll (&self) -> usize; +} + +pub trait HasScene: AsRefOpt + AsMutOpt { + fn scene_mut (&mut self) -> Option<&mut Scene> { self.as_mut_opt() } + fn scene (&self) -> Option<&Scene> { self.as_ref_opt() } +} + +pub trait HasScenes: AsRef> + AsMut> { + fn scenes (&self) -> &Vec { self.as_ref() } + fn scenes_mut (&mut self) -> &mut Vec { self.as_mut() } + /// Generate the default name for a new scene + fn scene_default_name (&self) -> Arc { format!("s{:3>}", self.scenes().len() + 1).into() } + fn scene_longest_name (&self) -> usize { self.scenes().iter().map(|s|s.name.len()).fold(0, usize::max) } + /// Add multiple scenes + fn scenes_add (&mut self, n: usize) -> Usually<()> where Self: HasTracks { + let scene_color_1 = ItemColor::random(); + let scene_color_2 = ItemColor::random(); + for i in 0..n { + let _ = self.scene_add(None, Some( + scene_color_1.mix(scene_color_2, i as f32 / n as f32).into() + ))?; + } + Ok(()) + } + /// Add a scene + fn scene_add (&mut self, name: Option<&str>, color: Option) + -> Usually<(usize, &mut Scene)> where Self: HasTracks + { + let scene = Scene { + name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()), + clips: vec![None;self.tracks().len()], + color: color.unwrap_or_else(ItemTheme::random), + }; + self.scenes_mut().push(scene); + let index = self.scenes().len() - 1; + Ok((index, &mut self.scenes_mut()[index])) + } +} + +impl HasSceneScroll for Arrangement { + fn scene_scroll (&self) -> usize { self.scene_scroll } +} + +impl HasSceneScroll for App { + fn scene_scroll (&self) -> usize { self.project.scene_scroll() } +} + +impl ScenesView for Arrangement { + fn h_scenes (&self) -> u16 { (self.size.h() as u16).saturating_sub(20) } + fn w_side (&self) -> u16 { (self.size.w() as u16 * 2 / 10).max(20) } + fn w_mid (&self) -> u16 { (self.size.w() as u16).saturating_sub(2 * self.w_side()).max(40) } +} + +pub type SceneWith<'a, T> = + (usize, &'a Scene, usize, usize, T); + +def_command!(SceneCommand: |scene: Scene| { + SetSize { size: usize } => { todo!() }, + SetZoom { size: usize } => { todo!() }, + SetName { name: Arc } => + swap_value(&mut scene.name, name, |name|Self::SetName{name}), + SetColor { color: ItemTheme } => + swap_value(&mut scene.color, color, |color|Self::SetColor{color}), +}); + +#[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: App| self.project.as_ref_opt()); +#[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: App| self.project.as_mut_opt()); +#[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: Arrangement| self.selected_scene()); +#[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: Arrangement| self.selected_scene_mut()); +impl_has!(Vec: |self: Arrangement| self.scenes); +impl ClipsView for T {} +impl>+AsMut>> HasScenes for T {} +impl+AsMutOpt+Send+Sync> HasScene for T {} + +impl ScenesView for App { + fn w_mid (&self) -> u16 { + (self.size.w() as u16).saturating_sub(self.w_side()) + } + fn w_side (&self) -> u16 { + 20 + } + fn h_scenes (&self) -> u16 { + (self.size.h() as u16).saturating_sub(20) + } +} + +/// A track consists of a sequencer and zero or more devices chained after it. +/// +/// ``` +/// let track: tek::Track = Default::default(); +/// ``` +#[derive(Debug, Default)] pub struct Track { + /// Name of track + pub name: Arc, + /// Identifying color of track + pub color: ItemTheme, + /// Preferred width of track column + pub width: usize, + /// MIDI sequencer state + pub sequencer: Sequencer, + /// Device chain + pub devices: Vec, +} + +impl Track { + /// Create a new track with only the default [Sequencer]. + pub fn new ( + name: &impl AsRef, + color: Option, + jack: &Jack<'static>, + clock: Option<&Clock>, + clip: Option<&Arc>>, + midi_from: &[Connect], + midi_to: &[Connect], + ) -> Usually { + Ok(Self { + name: name.as_ref().into(), + color: color.unwrap_or_default(), + sequencer: Sequencer::new( + format!("{}/sequencer", name.as_ref()), jack, clock, clip, midi_from, midi_to + )?, + ..Default::default() + }) + } + pub fn audio_ins (&self) -> &[AudioInput] { + self.devices.first().map(|x|x.audio_ins()).unwrap_or_default() + } + pub fn audio_outs (&self) -> &[AudioOutput] { + self.devices.last().map(|x|x.audio_outs()).unwrap_or_default() + } + fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } + fn _todo_usize_stub_ (&self) -> usize { todo!() } + fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } + fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } + + pub fn per <'a, T: Draw + 'a, U: TracksSizes<'a>> ( + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a + ) -> impl Draw + 'a { + view_track_per(tracks, callback) + } +} + +#[cfg(feature = "sampler")] +impl Track { + + /// Create a new track connecting the [Sequencer] to a [Sampler]. + pub fn new_with_sampler ( + name: &impl AsRef, + color: Option, + jack: &Jack<'static>, + clock: Option<&Clock>, + clip: Option<&Arc>>, + midi_from: &[Connect], + midi_to: &[Connect], + audio_from: &[&[Connect];2], + audio_to: &[&[Connect];2], + ) -> Usually { + let mut track = Self::new(name, color, jack, clock, clip, midi_from, midi_to)?; + let client_name = jack.with_client(|c|c.name().to_string()); + let port_name = track.sequencer.midi_outs[0].port_name(); + let connect = [Connect::exact(format!("{client_name}:{}", port_name))]; + track.devices.push(Device::Sampler(Sampler::new( + jack, &format!("{}/sampler", name.as_ref()), &connect, audio_from, audio_to + )?)); + Ok(track) + } + + pub fn sampler (&self, mut nth: usize) -> Option<&Sampler> { + for device in self.devices.iter() { + match device { + Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; }, + _ => {} + } + } + None + } + + pub fn sampler_mut (&mut self, mut nth: usize) -> Option<&mut Sampler> { + for device in self.devices.iter_mut() { + match device { + Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; }, + _ => {} + } + } + None + } +} + +impl HasWidth for Track { + const MIN_WIDTH: usize = 9; + fn width_inc (&mut self) { self.width += 1; } + fn width_dec (&mut self) { if self.width > Track::MIN_WIDTH { self.width -= 1; } } +} + +pub trait HasTracks: AsRef> + AsMut> { + fn tracks (&self) -> &Vec { self.as_ref() } + fn tracks_mut (&mut self) -> &mut Vec { self.as_mut() } + /// Run audio callbacks for every track and every device + fn process_tracks (&mut self, client: &Client, scope: &ProcessScope) -> Control { + for track in self.tracks_mut().iter_mut() { + if Control::Quit == Audio::process(&mut track.sequencer, client, scope) { + return Control::Quit + } + for device in track.devices.iter_mut() { + if Control::Quit == DeviceAudio(device).process(client, scope) { + return Control::Quit + } + } + } + Control::Continue + } + fn track_longest_name (&self) -> usize { + self.tracks().iter().map(|s|s.name.len()).fold(0, usize::max) + } + /// Stop all playing clips + fn tracks_stop_all (&mut self) { + for track in self.tracks_mut().iter_mut() { + track.sequencer.enqueue_next(None); + } + } + /// Stop all playing clips + fn tracks_launch (&mut self, clips: Option>>>>) { + if let Some(clips) = clips { + for (clip, track) in clips.iter().zip(self.tracks_mut()) { + track.sequencer.enqueue_next(clip.as_ref()); + } + } else { + for track in self.tracks_mut().iter_mut() { + track.sequencer.enqueue_next(None); + } + } + } + /// Spacing between tracks. + const TRACK_SPACING: usize = 0; +} + +pub trait HasTrack: AsRefOpt + AsMutOpt { + fn track (&self) -> Option<&Track> { self.as_ref_opt() } + fn track_mut (&mut self) -> Option<&mut Track> { self.as_mut_opt() } + + #[cfg(feature = "port")] + fn view_midi_ins_status <'a> (&'a self, theme: ItemTheme) -> impl Draw + 'a { + view_midi_ins_status(theme, self.track()) + } + + #[cfg(feature = "port")] + fn view_midi_outs_status (&self, theme: ItemTheme) -> impl Draw + '_ { + view_midi_outs_status(theme, self.track()) + } + + #[cfg(feature = "port")] + fn view_audio_ins_status (&self, theme: ItemTheme) -> impl Draw { + view_audio_ins_status(theme, self.track()) + } + + #[cfg(feature = "port")] + fn view_audio_outs_status (&self, theme: ItemTheme) -> impl Draw { + view_audio_outs_status(theme, self.track()) + } +} + +pub trait HasTrackScroll: HasTracks { + fn track_scroll (&self) -> usize; +} + +pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll { + /// Draw name of each track + fn view_track_names (&self, theme: ItemTheme) -> impl Draw { + view_track_names( + theme, self.tracks_with_sizes(), self.tracks().len(), self.scenes().len(), self.selection() + ) + } + /// Draw outputs per track + fn view_track_outputs <'a> (&'a self, theme: ItemTheme, _h: u16) -> impl Draw { + view_track_outputs( + theme, self.tracks_with_sizes(), self.midi_outs().iter() + ) + } + /// Draw inputs per track + fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Draw { + let mut h = 0u16; + for track in self.tracks().iter() { + h = h.max(track.sequencer.midi_ins.len() as u16); + } + view_track_inputs(theme, self.tracks_with_sizes(), h) + } + /// Iterate over tracks with their corresponding sizes. + fn tracks_with_sizes (&self) -> impl TracksSizes<'_> { + let _editor_width = self.editor().map(|e|e.size.w()); + let _active_track = self.selection().track(); + let mut x = 0; + let w = self.clips_size().w() as usize; + self.tracks().iter().enumerate().map_while(move |(index, track)|{ + let width = track.width.max(8); + if x + width < w { + let data = (index, track, x, x + width); + x += width + Self::TRACK_SPACING; + Some(data) + } else { + None + } + }) + } + +} + +impl HasTrackScroll for Arrangement { + fn track_scroll (&self) -> usize { self.track_scroll } +} + +def_command!(TrackCommand: |track: Track| { + Stop => { track.sequencer.enqueue_next(None); Ok(None) }, + 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 }), + 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 }), +}); + +impl>+AsMut>> HasTracks for T {} +impl+AsMutOpt+Send+Sync> HasTrack for T {} +impl TracksView for T {} +impl_has!(Vec: |self: Arrangement| self.tracks); +impl_as_ref_opt!(Track: |self: Arrangement| self.selected_track()); +impl_as_mut_opt!(Track: |self: Arrangement| self.selected_track_mut()); +impl_as_ref!(Vec: |self: App| self.project.as_ref()); +impl_as_mut!(Vec: |self: App| self.project.as_mut()); +#[cfg(feature = "select")] impl_as_ref_opt!(Track: |self: App| self.project.as_ref_opt()); +#[cfg(feature = "select")] impl_as_mut_opt!(Track: |self: App| self.project.as_mut_opt()); + +impl Arrangement { + + pub fn view_inputs (&self, _theme: ItemTheme) -> impl Draw + '_ { + view_inputs( + self.tracks_with_sizes(), self.midi_ins().as_slice() + ) + } + + pub fn view_outputs (&self, theme: ItemTheme) -> impl Draw { + view_outputs( + theme, self.tracks_with_sizes(), self.midi_outs(), self.outputs_height() + ) + } + + pub fn view_track_devices (&self, theme: ItemTheme) -> impl Draw { + view_track_devices( + theme, self.tracks_with_sizes(), self.track(), self.devices_height() + ) + } + + fn devices_height (&self) -> u16 { + let mut h = 2; + for track in self.tracks().iter() { + h = h.max(track.devices.len() * 2); + } + h as u16 + } + + fn outputs_height (&self) -> u16 { + let mut h = 1; + for output in self.midi_outs().iter() { + h += 1 + output.connections.len(); + } + h as u16 + } + + /// 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])) + } +} + +impl HasTrackScroll for App { + fn track_scroll (&self) -> usize { + self.project.track_scroll() + } +} + +pub(crate) fn track_width (_index: usize, track: &Track) -> u16 { + track.width as u16 +} diff --git a/src/device/arrange/clip.rs b/src/device/arrange/clip.rs deleted file mode 100644 index e572a9da..00000000 --- a/src/device/arrange/clip.rs +++ /dev/null @@ -1,95 +0,0 @@ -use crate::*; - -/// TODO: Preserve the generic passthru syntax; -/// remove this macro (only used twice) and potentially the trait. -#[macro_export] macro_rules! impl_has_clips { - (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? { - fn clips <'a> (&'a $self) -> std::sync::RwLockReadGuard<'a, ClipPool> { - $cb.read().unwrap() - } - fn clips_mut <'a> (&'a $self) -> std::sync::RwLockWriteGuard<'a, ClipPool> { - $cb.write().unwrap() - } - } - } -} - -#[macro_export] macro_rules! has_clip { - (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? HasMidiClip for $Struct $(<$($L),*$($T),*>)? { - fn clip (&$self) -> Option>> { $cb } - } - } -} - -pub trait ClipsView: TracksView + ScenesView { - /// Draw clips per scene - fn view_scenes_clips <'a> (&'a self) -> impl Draw + 'a { - view_scenes_clips( - ||self.scenes_with_sizes(), - self.tracks_with_sizes(), - self.selection(), - self.editor(), - self.clips_size(), - self.is_editing(), - ) - } -} - -def_command!(ClipCommand: |clip: MidiClip| { - SetColor { color: Option } => { - //(SetColor [t: usize, s: usize, c: ItemTheme] - //clip.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o))))); - //("color" [a: usize, b: usize] Some(Self::SetColor(a.unwrap(), b.unwrap(), ItemTheme::random()))) - todo!() - }, - SetLoop { looping: Option } => { - //(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}")) - //("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap()))) - todo!() - } -}); - -impl Arrangement { - /// Put a clip in a slot - pub fn clip_put ( - &mut self, track: usize, scene: usize, clip: Option>> - ) -> Option>> { - let old = self.scenes[scene].clips[track].clone(); - self.scenes[scene].clips[track] = clip; - old - } - - /// Change the color of a clip, returning the previous one - pub fn clip_set_color ( - &self, track: usize, scene: usize, color: ItemTheme - ) -> Option { - self.scenes[scene].clips[track].as_ref().map(|clip|{ - let mut clip = clip.write().unwrap(); - let old = clip.color.clone(); - clip.color = color.clone(); - panic!("{color:?} {old:?}"); - //old - }) - } - - /// Toggle looping for the active clip - pub fn toggle_loop (&mut self) { - if let Some(clip) = self.selected_clip() { - clip.write().unwrap().toggle_loop() - } - } -} - -pub trait HasClipsSize { - fn clips_size (&self) -> &Sizer; -} - -impl HasClipsSize for App { - fn clips_size (&self) -> &Sizer { &self.project.size_inner } -} - -impl HasClipsSize for Arrangement { - fn clips_size (&self) -> &Sizer { &self.size_inner } -} diff --git a/src/device/arrange/scene.rs b/src/device/arrange/scene.rs deleted file mode 100644 index d7c57966..00000000 --- a/src/device/arrange/scene.rs +++ /dev/null @@ -1,174 +0,0 @@ -use crate::*; - -/// Default scene height. -pub const H_SCENE: usize = 2; - -/// Default editor height. -pub const H_EDITOR: usize = 15; - -/// A scene consists of a set of clips to play together. -/// -/// ``` -/// let scene: tek::Scene = Default::default(); -/// let _ = scene.pulses(); -/// let _ = scene.is_playing(&[]); -/// ``` -#[derive(Debug, Default)] pub struct Scene { - /// Name of scene - pub name: Arc, - /// Identifying color of scene - pub color: ItemTheme, - /// Clips in scene, one per track - pub clips: Vec>>>, -} - -impl Scene { - /// Returns the pulse length of the longest clip in the scene - pub fn pulses (&self) -> usize { - self.clips.iter().fold(0, |a, p|{ - a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0)) - }) - } - /// Returns true if all clips in the scene are - /// currently playing on the given collection of tracks. - pub fn is_playing (&self, tracks: &[Track]) -> bool { - self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate() - .all(|(track_index, clip)|match clip { - Some(c) => tracks - .get(track_index) - .map(|track|{ - if let Some((_, Some(clip))) = track.sequencer().play_clip() { - *clip.read().unwrap() == *c.read().unwrap() - } else { - false - } - }) - .unwrap_or(false), - None => true - }) - } - pub fn clip (&self, index: usize) -> Option<&Arc>> { - match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None } - } - fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } - fn _todo_usize_stub_ (&self) -> usize { todo!() } - fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } - fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } -} - -pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + Send + Sync { - fn h_scenes (&self) -> u16; - fn w_side (&self) -> u16; - fn w_mid (&self) -> u16; - - fn view_scenes_names (&self) -> impl Draw { - view_scenes_names( - self.scenes_with_sizes(), self.selection(), self.editor(), self.is_editing() - ) - } - 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 { - H_SCENE - }; - if y + height <= self.clips_size().h() as usize { - let data = (s, scene, y, y + height); - y += height; - Some(data) - } else { - None - } - }) - } -} - -pub trait HasSceneScroll: HasScenes { - fn scene_scroll (&self) -> usize; -} - -pub trait HasScene: AsRefOpt + AsMutOpt { - fn scene_mut (&mut self) -> Option<&mut Scene> { self.as_mut_opt() } - fn scene (&self) -> Option<&Scene> { self.as_ref_opt() } -} - -pub trait HasScenes: AsRef> + AsMut> { - fn scenes (&self) -> &Vec { self.as_ref() } - fn scenes_mut (&mut self) -> &mut Vec { self.as_mut() } - /// Generate the default name for a new scene - fn scene_default_name (&self) -> Arc { format!("s{:3>}", self.scenes().len() + 1).into() } - fn scene_longest_name (&self) -> usize { self.scenes().iter().map(|s|s.name.len()).fold(0, usize::max) } - /// Add multiple scenes - fn scenes_add (&mut self, n: usize) -> Usually<()> where Self: HasTracks { - let scene_color_1 = ItemColor::random(); - let scene_color_2 = ItemColor::random(); - for i in 0..n { - let _ = self.scene_add(None, Some( - scene_color_1.mix(scene_color_2, i as f32 / n as f32).into() - ))?; - } - Ok(()) - } - /// Add a scene - fn scene_add (&mut self, name: Option<&str>, color: Option) - -> Usually<(usize, &mut Scene)> where Self: HasTracks - { - let scene = Scene { - name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()), - clips: vec![None;self.tracks().len()], - color: color.unwrap_or_else(ItemTheme::random), - }; - self.scenes_mut().push(scene); - let index = self.scenes().len() - 1; - Ok((index, &mut self.scenes_mut()[index])) - } -} - -impl HasSceneScroll for Arrangement { - fn scene_scroll (&self) -> usize { self.scene_scroll } -} - -impl HasSceneScroll for App { - fn scene_scroll (&self) -> usize { self.project.scene_scroll() } -} - -impl ScenesView for Arrangement { - fn h_scenes (&self) -> u16 { (self.size.h() as u16).saturating_sub(20) } - fn w_side (&self) -> u16 { (self.size.w() as u16 * 2 / 10).max(20) } - fn w_mid (&self) -> u16 { (self.size.w() as u16).saturating_sub(2 * self.w_side()).max(40) } -} - -pub type SceneWith<'a, T> = - (usize, &'a Scene, usize, usize, T); - -def_command!(SceneCommand: |scene: Scene| { - SetSize { size: usize } => { todo!() }, - SetZoom { size: usize } => { todo!() }, - SetName { name: Arc } => - swap_value(&mut scene.name, name, |name|Self::SetName{name}), - SetColor { color: ItemTheme } => - swap_value(&mut scene.color, color, |color|Self::SetColor{color}), -}); - -#[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: App| self.project.as_ref_opt()); -#[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: App| self.project.as_mut_opt()); -#[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: Arrangement| self.selected_scene()); -#[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: Arrangement| self.selected_scene_mut()); -impl_has!(Vec: |self: Arrangement| self.scenes); -impl ClipsView for T {} -impl>+AsMut>> HasScenes for T {} -impl+AsMutOpt+Send+Sync> HasScene for T {} - -impl ScenesView for App { - fn w_mid (&self) -> u16 { - (self.size.w() as u16).saturating_sub(self.w_side()) - } - fn w_side (&self) -> u16 { - 20 - } - fn h_scenes (&self) -> u16 { - (self.size.h() as u16).saturating_sub(20) - } -} diff --git a/src/device/arrange/select.rs b/src/device/arrange/select.rs deleted file mode 100644 index ae748e5d..00000000 --- a/src/device/arrange/select.rs +++ /dev/null @@ -1,161 +0,0 @@ -use crate::*; - -/// Represents the current user selection in the arranger -#[derive(PartialEq, Clone, Copy, Debug, Default)] pub enum Selection { - #[default] - /// Nothing is selected - Nothing, - /// The whole mix is selected - Mix, - /// A MIDI input is selected. - Input(usize), - /// A MIDI output is selected. - Output(usize), - /// A scene is selected. - #[cfg(feature = "scene")] Scene(usize), - /// A track is selected. - #[cfg(feature = "track")] Track(usize), - /// A clip (track × scene) is selected. - #[cfg(feature = "track")] TrackClip { track: usize, scene: usize }, - /// A track's MIDI input connection is selected. - #[cfg(feature = "track")] TrackInput { track: usize, port: usize }, - /// A track's MIDI output connection is selected. - #[cfg(feature = "track")] TrackOutput { track: usize, port: usize }, - /// A track device slot is selected. - #[cfg(feature = "track")] TrackDevice { track: usize, device: usize }, -} - -impl Selection { - pub fn describe ( - &self, - #[cfg(feature = "track")] tracks: &[Track], - #[cfg(feature = "scene")] scenes: &[Scene], - ) -> Arc { - use Selection::*; - format!("{}", match self { - Mix => "Everything".to_string(), - #[cfg(feature = "scene")] Scene(s) => - scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name)).unwrap_or_else(||"S??".into()), - #[cfg(feature = "track")] Track(t) => - tracks.get(*t).map(|track|format!("T{t}: {}", &track.name)).unwrap_or_else(||"T??".into()), - TrackClip { track, scene } => match (tracks.get(*track), scenes.get(*scene)) { - (Some(_), Some(s)) => match s.clip(*track) { - Some(clip) => format!("T{track} S{scene} C{}", &clip.read().unwrap().name), - None => format!("T{track} S{scene}: Empty") - }, - _ => format!("T{track} S{scene}: Empty"), - }, - _ => todo!() - }).into() - } - #[cfg(feature = "scene")] pub fn scene (&self) -> Option { - use Selection::*; - match self { Scene(scene) | TrackClip { scene, .. } => Some(*scene), _ => None } - } - #[cfg(feature = "scene")] pub fn select_scene (&self, scene_count: usize) -> Self { - use Selection::*; - match self { - Mix | Track(_) => Scene(0), - Scene(s) => Scene((s + 1) % scene_count), - TrackClip { scene, .. } => Track(*scene), - _ => todo!(), - } - } - #[cfg(feature = "scene")] pub fn select_scene_next (&self, len: usize) -> Self { - use Selection::*; - match self { - Mix => Scene(0), - Track(t) => TrackClip { track: *t, scene: 0 }, - Scene(s) => if s + 1 < len { Scene(s + 1) } else { Mix }, - TrackClip { track, scene } => if scene + 1 < len { TrackClip { track: *track, scene: scene + 1 } } else { Track(*track) }, - _ => todo!() - } - } - #[cfg(feature = "scene")] pub fn select_scene_prev (&self) -> Self { - use Selection::*; - match self { - Mix | Scene(0) => Mix, - Scene(s) => Scene(s - 1), - Track(t) => Track(*t), - TrackClip { track, scene: 0 } => Track(*track), - TrackClip { track, scene } => TrackClip { track: *track, scene: scene - 1 }, - _ => todo!() - } - } - #[cfg(feature = "track")] pub fn track (&self) -> Option { - use Selection::*; - if let Track(track)|TrackClip{track,..}|TrackInput{track,..}|TrackOutput{track,..}|TrackDevice{track,..} = self { - Some(*track) - } else { - None - } - } - #[cfg(feature = "track")] pub fn select_track (&self, track_count: usize) -> Self { - use Selection::*; - match self { - Mix => Track(0), - Scene(_) => Mix, - Track(t) => Track((t + 1) % track_count), - TrackClip { track, .. } => Track(*track), - _ => todo!(), - } - } - #[cfg(feature = "track")] pub fn select_track_next (&self, len: usize) -> Self { - use Selection::*; - match self { - Mix => Track(0), - Scene(s) => TrackClip { track: 0, scene: *s }, - Track(t) => if t + 1 < len { Track(t + 1) } else { Mix }, - TrackClip {track, scene} => if track + 1 < len { TrackClip { track: track + 1, scene: *scene } } else { Scene(*scene) }, - _ => todo!() - } - } - #[cfg(feature = "track")] pub fn select_track_prev (&self) -> Self { - use Selection::*; - match self { - Mix => Mix, - Scene(s) => Scene(*s), - Track(0) => Mix, - Track(t) => Track(t - 1), - TrackClip { track: 0, scene } => Scene(*scene), - TrackClip { track: t, scene } => TrackClip { track: t - 1, scene: *scene }, - _ => todo!() - } - } -} - -impl +AsMut> HasSelection for T {} - -pub trait HasSelection: AsRef + AsMut { - fn selection (&self) -> &Selection { self.as_ref() } - fn selection_mut (&mut self) -> &mut Selection { self.as_mut() } - /// Get the active track - #[cfg(feature = "track")] - fn selected_track (&self) -> Option<&Track> where Self: HasTracks { - let index = self.selection().track()?; - self.tracks().get(index) - } - /// Get a mutable reference to the active track - #[cfg(feature = "track")] - fn selected_track_mut (&mut self) -> Option<&mut Track> where Self: HasTracks { - let index = self.selection().track()?; - self.tracks_mut().get_mut(index) - } - /// Get the active scene - #[cfg(feature = "scene")] - fn selected_scene (&self) -> Option<&Scene> where Self: HasScenes { - let index = self.selection().scene()?; - self.scenes().get(index) - } - /// Get a mutable reference to the active scene - #[cfg(feature = "scene")] - fn selected_scene_mut (&mut self) -> Option<&mut Scene> where Self: HasScenes { - let index = self.selection().scene()?; - self.scenes_mut().get_mut(index) - } - /// Get the active clip - #[cfg(feature = "clip")] - fn selected_clip (&self) -> Option>> where Self: HasScenes + HasTracks { - self.selected_scene()?.clips.get(self.selection().track()?)?.clone() - } -} diff --git a/src/device/arrange/track.rs b/src/device/arrange/track.rs deleted file mode 100644 index c480f694..00000000 --- a/src/device/arrange/track.rs +++ /dev/null @@ -1,350 +0,0 @@ -use crate::*; - -/// A track consists of a sequencer and zero or more devices chained after it. -/// -/// ``` -/// let track: tek::Track = Default::default(); -/// ``` -#[derive(Debug, Default)] pub struct Track { - /// Name of track - pub name: Arc, - /// Identifying color of track - pub color: ItemTheme, - /// Preferred width of track column - pub width: usize, - /// MIDI sequencer state - pub sequencer: Sequencer, - /// Device chain - pub devices: Vec, -} - -impl Track { - /// Create a new track with only the default [Sequencer]. - pub fn new ( - name: &impl AsRef, - color: Option, - jack: &Jack<'static>, - clock: Option<&Clock>, - clip: Option<&Arc>>, - midi_from: &[Connect], - midi_to: &[Connect], - ) -> Usually { - Ok(Self { - name: name.as_ref().into(), - color: color.unwrap_or_default(), - sequencer: Sequencer::new( - format!("{}/sequencer", name.as_ref()), jack, clock, clip, midi_from, midi_to - )?, - ..Default::default() - }) - } - pub fn audio_ins (&self) -> &[AudioInput] { - self.devices.first().map(|x|x.audio_ins()).unwrap_or_default() - } - pub fn audio_outs (&self) -> &[AudioOutput] { - self.devices.last().map(|x|x.audio_outs()).unwrap_or_default() - } - fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } - fn _todo_usize_stub_ (&self) -> usize { todo!() } - fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } - fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } - - pub fn per <'a, T: Draw + 'a, U: TracksSizes<'a>> ( - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a - ) -> impl Draw + 'a { - view_track_per(tracks, callback) - } -} - -#[cfg(feature = "sampler")] -impl Track { - - /// Create a new track connecting the [Sequencer] to a [Sampler]. - pub fn new_with_sampler ( - name: &impl AsRef, - color: Option, - jack: &Jack<'static>, - clock: Option<&Clock>, - clip: Option<&Arc>>, - midi_from: &[Connect], - midi_to: &[Connect], - audio_from: &[&[Connect];2], - audio_to: &[&[Connect];2], - ) -> Usually { - let mut track = Self::new(name, color, jack, clock, clip, midi_from, midi_to)?; - let client_name = jack.with_client(|c|c.name().to_string()); - let port_name = track.sequencer.midi_outs[0].port_name(); - let connect = [Connect::exact(format!("{client_name}:{}", port_name))]; - track.devices.push(Device::Sampler(Sampler::new( - jack, &format!("{}/sampler", name.as_ref()), &connect, audio_from, audio_to - )?)); - Ok(track) - } - - pub fn sampler (&self, mut nth: usize) -> Option<&Sampler> { - for device in self.devices.iter() { - match device { - Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; }, - _ => {} - } - } - None - } - - pub fn sampler_mut (&mut self, mut nth: usize) -> Option<&mut Sampler> { - for device in self.devices.iter_mut() { - match device { - Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; }, - _ => {} - } - } - None - } -} - -impl HasWidth for Track { - const MIN_WIDTH: usize = 9; - fn width_inc (&mut self) { self.width += 1; } - fn width_dec (&mut self) { if self.width > Track::MIN_WIDTH { self.width -= 1; } } -} - -pub trait HasTracks: AsRef> + AsMut> { - fn tracks (&self) -> &Vec { self.as_ref() } - fn tracks_mut (&mut self) -> &mut Vec { self.as_mut() } - /// Run audio callbacks for every track and every device - fn process_tracks (&mut self, client: &Client, scope: &ProcessScope) -> Control { - for track in self.tracks_mut().iter_mut() { - if Control::Quit == Audio::process(&mut track.sequencer, client, scope) { - return Control::Quit - } - for device in track.devices.iter_mut() { - if Control::Quit == DeviceAudio(device).process(client, scope) { - return Control::Quit - } - } - } - Control::Continue - } - fn track_longest_name (&self) -> usize { - self.tracks().iter().map(|s|s.name.len()).fold(0, usize::max) - } - /// Stop all playing clips - fn tracks_stop_all (&mut self) { - for track in self.tracks_mut().iter_mut() { - track.sequencer.enqueue_next(None); - } - } - /// Stop all playing clips - fn tracks_launch (&mut self, clips: Option>>>>) { - if let Some(clips) = clips { - for (clip, track) in clips.iter().zip(self.tracks_mut()) { - track.sequencer.enqueue_next(clip.as_ref()); - } - } else { - for track in self.tracks_mut().iter_mut() { - track.sequencer.enqueue_next(None); - } - } - } - /// Spacing between tracks. - const TRACK_SPACING: usize = 0; -} - -pub trait HasTrack: AsRefOpt + AsMutOpt { - fn track (&self) -> Option<&Track> { self.as_ref_opt() } - fn track_mut (&mut self) -> Option<&mut Track> { self.as_mut_opt() } - - #[cfg(feature = "port")] - fn view_midi_ins_status <'a> (&'a self, theme: ItemTheme) -> impl Draw + 'a { - view_midi_ins_status(theme, self.track()) - } - - #[cfg(feature = "port")] - fn view_midi_outs_status (&self, theme: ItemTheme) -> impl Draw + '_ { - view_midi_outs_status(theme, self.track()) - } - - #[cfg(feature = "port")] - fn view_audio_ins_status (&self, theme: ItemTheme) -> impl Draw { - view_audio_ins_status(theme, self.track()) - } - - #[cfg(feature = "port")] - fn view_audio_outs_status (&self, theme: ItemTheme) -> impl Draw { - view_audio_outs_status(theme, self.track()) - } -} - -pub trait HasTrackScroll: HasTracks { - fn track_scroll (&self) -> usize; -} - -pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll { - /// Draw name of each track - fn view_track_names (&self, theme: ItemTheme) -> impl Draw { - view_track_names( - theme, self.tracks_with_sizes(), self.tracks().len(), self.scenes().len(), self.selection() - ) - } - /// Draw outputs per track - fn view_track_outputs <'a> (&'a self, theme: ItemTheme, _h: u16) -> impl Draw { - view_track_outputs( - theme, self.tracks_with_sizes(), self.midi_outs().iter() - ) - } - /// Draw inputs per track - fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Draw { - let mut h = 0u16; - for track in self.tracks().iter() { - h = h.max(track.sequencer.midi_ins.len() as u16); - } - view_track_inputs(theme, self.tracks_with_sizes(), h) - } - /// Iterate over tracks with their corresponding sizes. - fn tracks_with_sizes (&self) -> impl TracksSizes<'_> { - let _editor_width = self.editor().map(|e|e.size.w()); - let _active_track = self.selection().track(); - let mut x = 0; - let w = self.clips_size().w() as usize; - self.tracks().iter().enumerate().map_while(move |(index, track)|{ - let width = track.width.max(8); - if x + width < w { - let data = (index, track, x, x + width); - x += width + Self::TRACK_SPACING; - Some(data) - } else { - None - } - }) - } - -} - -impl HasTrackScroll for Arrangement { - fn track_scroll (&self) -> usize { self.track_scroll } -} - -def_command!(TrackCommand: |track: Track| { - Stop => { track.sequencer.enqueue_next(None); Ok(None) }, - 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 }), - 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 }), -}); - -impl>+AsMut>> HasTracks for T {} -impl+AsMutOpt+Send+Sync> HasTrack for T {} -impl TracksView for T {} -impl_has!(Vec: |self: Arrangement| self.tracks); -impl_as_ref_opt!(Track: |self: Arrangement| self.selected_track()); -impl_as_mut_opt!(Track: |self: Arrangement| self.selected_track_mut()); -impl_as_ref!(Vec: |self: App| self.project.as_ref()); -impl_as_mut!(Vec: |self: App| self.project.as_mut()); -#[cfg(feature = "select")] impl_as_ref_opt!(Track: |self: App| self.project.as_ref_opt()); -#[cfg(feature = "select")] impl_as_mut_opt!(Track: |self: App| self.project.as_mut_opt()); - -impl Arrangement { - - pub fn view_inputs (&self, _theme: ItemTheme) -> impl Draw + '_ { - view_inputs( - self.tracks_with_sizes(), self.midi_ins().as_slice() - ) - } - - pub fn view_outputs (&self, theme: ItemTheme) -> impl Draw { - view_outputs( - theme, self.tracks_with_sizes(), self.midi_outs(), self.outputs_height() - ) - } - - pub fn view_track_devices (&self, theme: ItemTheme) -> impl Draw { - view_track_devices( - theme, self.tracks_with_sizes(), self.track(), self.devices_height() - ) - } - - fn devices_height (&self) -> u16 { - let mut h = 2; - for track in self.tracks().iter() { - h = h.max(track.devices.len() * 2); - } - h as u16 - } - - fn outputs_height (&self) -> u16 { - let mut h = 1; - for output in self.midi_outs().iter() { - h += 1 + output.connections.len(); - } - h as u16 - } - - /// 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])) - } -} - -impl HasTrackScroll for App { - fn track_scroll (&self) -> usize { - self.project.track_scroll() - } -} - -pub(crate) fn track_width (_index: usize, track: &Track) -> u16 { - track.width as u16 -} diff --git a/src/device/dialog.rs b/src/device/dialog.rs index b68b1138..b007ffa2 100644 --- a/src/device/dialog.rs +++ b/src/device/dialog.rs @@ -55,12 +55,15 @@ 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({ + MenuItem("Resume session".into(), Arc::new(Box::new(|_|Ok(())))), + MenuItem("New session".into(), Arc::new(Box::new(|app|Ok({ app.dialog = Dialog::None; app.mode = app.config.modes.get(":arranger").unwrap(); })))), - MenuItem("Load old session".into(), Arc::new(Box::new(|_|Ok(())))), + MenuItem("Load session".into(), Arc::new(Box::new(|_|Ok(())))), + MenuItem("Exit".into(), Arc::new(Box::new(|_|Ok({ + () + })))), ].into())) } /// FIXME: generalize diff --git a/src/device/midi.rs b/src/device/midi.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/src/device/port.rs b/src/device/port.rs deleted file mode 100644 index eb8cf92a..00000000 --- a/src/device/port.rs +++ /dev/null @@ -1,136 +0,0 @@ -use crate::*; -use ConnectName::*; -use ConnectScope::*; -use ConnectStatus::*; - -mod audio; pub use self::audio::*; -mod midi; pub use self::midi::*; -mod connect; pub use self::connect::*; -mod register; pub use self::register::*; - -pub trait JackPort: HasJack<'static> { - type Port: PortSpec + Default; - type Pair: PortSpec + Default; - - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized; - - fn register (jack: &Jack<'static>, name: &impl AsRef) -> Usually> { - jack.with_client(|c|c.register_port::(name.as_ref(), Default::default())) - .map_err(|e|e.into()) - } - - fn port_name (&self) -> &Arc; - - fn connections (&self) -> &[Connect]; - - fn port (&self) -> &Port; - - fn port_mut (&mut self) -> &mut Port; - - fn into_port (self) -> Port where Self: Sized; - - fn close (self) -> Usually<()> where Self: Sized { - let jack = self.jack().clone(); - Ok(jack.with_client(|c|c.unregister_port(self.into_port()))?) - } - - fn ports (&self, re_name: Option<&str>, re_type: Option<&str>, flags: PortFlags) -> Vec { - self.with_client(|c|c.ports(re_name, re_type, flags)) - } - - fn port_by_id (&self, id: u32) -> Option> { - self.with_client(|c|c.port_by_id(id)) - } - - fn port_by_name (&self, name: impl AsRef) -> Option> { - self.with_client(|c|c.port_by_name(name.as_ref())) - } - - fn connect_to_matching <'k> (&'k self) -> Usually<()> { - for connect in self.connections().iter() { - match &connect.name { - Some(Exact(name)) => { - *connect.status.write().unwrap() = self.connect_exact(name)?; - }, - Some(RegExp(re)) => { - *connect.status.write().unwrap() = self.connect_regexp(re, connect.scope)?; - }, - _ => {}, - }; - } - Ok(()) - } - - fn connect_exact <'k> (&'k self, name: &str) -> - Usually, Arc, ConnectStatus)>> - { - self.with_client(move|c|{ - let mut status = vec![]; - for port in c.ports(None, None, PortFlags::empty()).iter() { - if port.as_str() == &*name { - if let Some(port) = c.port_by_name(port.as_str()) { - let port_status = self.connect_to_unowned(&port)?; - let name = port.name()?.into(); - status.push((port, name, port_status)); - if port_status == Connected { - break - } - } - } - } - Ok(status) - }) - } - - fn connect_regexp <'k> ( - &'k self, re: &str, scope: Option - ) -> Usually, Arc, ConnectStatus)>> { - self.with_client(move|c|{ - let mut status = vec![]; - let ports = c.ports(Some(&re), None, PortFlags::empty()); - for port in ports.iter() { - if let Some(port) = c.port_by_name(port.as_str()) { - let port_status = self.connect_to_unowned(&port)?; - let name = port.name()?.into(); - status.push((port, name, port_status)); - if port_status == Connected && scope == Some(One) { - break - } - } - } - Ok(status) - }) - } - - /** Connect to a matching port by name. */ - fn connect_to_name (&self, name: impl AsRef) -> Usually { - self.with_client(|c|if let Some(ref port) = c.port_by_name(name.as_ref()) { - self.connect_to_unowned(port) - } else { - Ok(Missing) - }) - } - - /** Connect to a matching port by reference. */ - fn connect_to_unowned (&self, port: &Port) -> Usually { - self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { - Connected - } else if let Ok(_) = c.connect_ports(port, self.port()) { - Connected - } else { - Mismatch - })) - } - - /** Connect to an owned matching port by reference. */ - fn connect_to_owned (&self, port: &Port) -> Usually { - self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { - Connected - } else if let Ok(_) = c.connect_ports(port, self.port()) { - Connected - } else { - Mismatch - })) - } -} diff --git a/src/device/port/audio.rs b/src/device/port/audio.rs deleted file mode 100644 index bb1db676..00000000 --- a/src/device/port/audio.rs +++ /dev/null @@ -1,103 +0,0 @@ -use crate::*; - -def_command!(AudioInputCommand: |port: AudioInput| { - Close => todo!(), - Connect { audio_out: Arc } => todo!(), -}); - -def_command!(AudioOutputCommand: |port: AudioOutput| { - Close => todo!(), - Connect { audio_in: Arc } => todo!(), -}); - -impl HasJack<'static> for AudioInput { fn jack (&self) -> &Jack<'static> { &self.jack } } -impl HasJack<'static> for AudioOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } - -/// Audio input port. -#[derive(Debug)] pub struct AudioInput { - /// Handle to JACK client, for receiving reconnect events. - pub jack: Jack<'static>, - /// Port name - pub name: Arc, - /// Port handle. - pub port: Port, - /// List of ports to connect to. - pub connections: Vec, -} - -/// Audio output port. -#[derive(Debug)] pub struct AudioOutput { - /// Handle to JACK client, for receiving reconnect events. - pub jack: Jack<'static>, - /// Port name - pub name: Arc, - /// Port handle. - pub port: Port, - /// List of ports to connect to. - pub connections: Vec, -} - - -impl JackPort for AudioInput { - type Port = AudioIn; - type Pair = AudioOut; - fn port_name (&self) -> &Arc { - &self.name - } - fn port (&self) -> &Port { - &self.port - } - fn port_mut (&mut self) -> &mut Port { - &mut self.port - } - fn into_port (self) -> Port { - self.port - } - fn connections (&self) -> &[Connect] { - self.connections.as_slice() - } - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized - { - let port = Self { - port: Self::register(jack, name)?, - jack: jack.clone(), - name: name.as_ref().into(), - connections: connect.to_vec() - }; - port.connect_to_matching()?; - Ok(port) - } -} - -impl JackPort for AudioOutput { - type Port = AudioOut; - type Pair = AudioIn; - fn port_name (&self) -> &Arc { - &self.name - } - fn port (&self) -> &Port { - &self.port - } - fn port_mut (&mut self) -> &mut Port { - &mut self.port - } - fn into_port (self) -> Port { - self.port - } - fn connections (&self) -> &[Connect] { - self.connections.as_slice() - } - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized - { - let port = Self { - port: Self::register(jack, name)?, - jack: jack.clone(), - name: name.as_ref().into(), - connections: connect.to_vec() - }; - port.connect_to_matching()?; - Ok(port) - } -} diff --git a/src/device/port/connect.rs b/src/device/port/connect.rs deleted file mode 100644 index 8571fa2e..00000000 --- a/src/device/port/connect.rs +++ /dev/null @@ -1,83 +0,0 @@ -use crate::*; -use ConnectName::*; -use ConnectScope::*; -use ConnectStatus::*; - -#[derive(Clone, Debug, PartialEq)] pub enum ConnectName { - /** Exact match */ - Exact(Arc), - /** Match regular expression */ - RegExp(Arc), -} - -#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectScope { - One, - All -} - -#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectStatus { - Missing, - Disconnected, - Connected, - Mismatch, -} - -/// Port connection manager. -/// -/// ``` -/// let connect = tek::Connect::default(); -/// ``` -#[derive(Clone, Debug, Default)] pub struct Connect { - pub name: Option, - pub scope: Option, - pub status: Arc, Arc, ConnectStatus)>>>, - pub info: Arc, -} - -impl Connect { - pub fn collect (exact: &[impl AsRef], re: &[impl AsRef], re_all: &[impl AsRef]) - -> Vec - { - let mut connections = vec![]; - for port in exact.iter() { connections.push(Self::exact(port)) } - for port in re.iter() { connections.push(Self::regexp(port)) } - for port in re_all.iter() { connections.push(Self::regexp_all(port)) } - connections - } - /// Connect to this exact port - pub fn exact (name: impl AsRef) -> Self { - let info = format!("=:{}", name.as_ref()).into(); - let name = Some(Exact(name.as_ref().into())); - Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } - } - pub fn regexp (name: impl AsRef) -> Self { - let info = format!("~:{}", name.as_ref()).into(); - let name = Some(RegExp(name.as_ref().into())); - Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } - } - pub fn regexp_all (name: impl AsRef) -> Self { - let info = format!("+:{}", name.as_ref()).into(); - let name = Some(RegExp(name.as_ref().into())); - Self { name, scope: Some(All), status: Arc::new(RwLock::new(vec![])), info } - } - pub fn info (&self) -> Arc { - format!(" ({}) {} {}", { - let status = self.status.read().unwrap(); - let mut ok = 0; - for (_, _, state) in status.iter() { - if *state == Connected { - ok += 1 - } - } - format!("{ok}/{}", status.len()) - }, match self.scope { - None => "x", - Some(One) => " ", - Some(All) => "*", - }, match &self.name { - None => format!("x"), - Some(Exact(name)) => format!("= {name}"), - Some(RegExp(name)) => format!("~ {name}"), - }).into() - } -} diff --git a/src/device/port/midi.rs b/src/device/port/midi.rs deleted file mode 100644 index eb9160cc..00000000 --- a/src/device/port/midi.rs +++ /dev/null @@ -1,256 +0,0 @@ -use crate::*; - -impl HasJack<'static> for MidiInput { fn jack (&self) -> &Jack<'static> { &self.jack } } - -impl HasJack<'static> for MidiOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } - -/// MIDI input port. -#[derive(Debug)] pub struct MidiInput { - /// Handle to JACK client, for receiving reconnect events. - pub jack: Jack<'static>, - /// Port name - pub name: Arc, - /// Port handle. - pub port: Port, - /// List of currently held notes. - pub held: Arc>, - /// List of ports to connect to. - pub connections: Vec, -} - -/// MIDI output port. -#[derive(Debug)] pub struct MidiOutput { - /// Handle to JACK client, for receiving reconnect events. - pub jack: Jack<'static>, - /// Port name - pub name: Arc, - /// Port handle. - pub port: Port, - /// List of ports to connect to. - pub connections: Vec, - /// List of currently held notes. - pub held: Arc>, - /// Buffer - pub note_buffer: Vec, - /// Buffer - pub output_buffer: Vec>>, -} - -def_command!(MidiInputCommand: |port: MidiInput| { - Close => todo!(), - Connect { midi_out: Arc } => todo!(), -}); - -def_command!(MidiOutputCommand: |port: MidiOutput| { - Close => todo!(), - Connect { midi_in: Arc } => todo!(), -}); -/// Trait for thing that may receive MIDI. -pub trait HasMidiIns { - fn midi_ins (&self) -> &Vec; - fn midi_ins_mut (&mut self) -> &mut Vec; - /// Collect MIDI input from app ports (TODO preallocate large buffers) - fn midi_input_collect <'a> (&'a self, scope: &'a ProcessScope) -> CollectedMidiInput<'a> { - self.midi_ins().iter() - .map(|port|port.port().iter(scope) - .map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes))) - .collect::>()) - .collect::>() - } - fn midi_ins_with_sizes <'a> (&'a self) -> - impl Iterator, &'a [Connect], usize, usize)> + Send + Sync + 'a - { - let mut y = 0; - self.midi_ins().iter().enumerate().map(move|(i, input)|{ - let height = 1 + input.connections().len(); - let data = (i, input.port_name(), input.connections(), y, y + height); - y += height; - data - }) - } -} -/// Trait for thing that may output MIDI. -pub trait HasMidiOuts { - fn midi_outs (&self) -> &Vec; - fn midi_outs_mut (&mut self) -> &mut Vec; - fn midi_outs_with_sizes <'a> (&'a self) -> - impl Iterator, &'a [Connect], usize, usize)> + Send + Sync + 'a - { - let mut y = 0; - self.midi_outs().iter().enumerate().map(move|(i, output)|{ - let height = 1 + output.connections().len(); - let data = (i, output.port_name(), output.connections(), y, y + height); - y += height; - data - }) - } - fn midi_outs_emit (&mut self, scope: &ProcessScope) { - for port in self.midi_outs_mut().iter_mut() { - port.buffer_emit(scope) - } - } -} - -impl JackPort for MidiInput { - type Port = MidiIn; - type Pair = MidiOut; - fn port_name (&self) -> &Arc { - &self.name - } - fn port (&self) -> &Port { - &self.port - } - fn port_mut (&mut self) -> &mut Port { - &mut self.port - } - fn into_port (self) -> Port { - self.port - } - fn connections (&self) -> &[Connect] { - self.connections.as_slice() - } - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized - { - let port = Self { - port: Self::register(jack, name)?, - jack: jack.clone(), - name: name.as_ref().into(), - connections: connect.to_vec(), - held: Arc::new(RwLock::new([false;128])) - }; - port.connect_to_matching()?; - Ok(port) - } -} - -impl JackPort for MidiOutput { - type Port = MidiOut; - type Pair = MidiIn; - fn port_name (&self) -> &Arc { - &self.name - } - fn port (&self) -> &Port { - &self.port - } - fn port_mut (&mut self) -> &mut Port { - &mut self.port - } - fn into_port (self) -> Port { - self.port - } - fn connections (&self) -> &[Connect] { - self.connections.as_slice() - } - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized - { - let port = Self::register(jack, name)?; - let jack = jack.clone(); - let name = name.as_ref().into(); - let connections = connect.to_vec(); - let port = Self { - jack, - port, - name, - connections, - held: Arc::new([false;128].into()), - note_buffer: vec![0;8], - output_buffer: vec![vec![];65536], - }; - port.connect_to_matching()?; - Ok(port) - } -} - -impl View for MidiInput { - fn view (&self) -> impl Draw { - "TODO: MIDI IN" - } -} - -impl View for MidiOutput { - fn view (&self) -> impl Draw { - "TODO: MIDI OUT" - } -} - -impl MidiOutput { - /// Clear the section of the output buffer that we will be using, - /// emitting "all notes off" at start of buffer if requested. - pub fn buffer_clear (&mut self, scope: &ProcessScope, reset: bool) { - let n_frames = (scope.n_frames() as usize).min(self.output_buffer.len()); - for frame in &mut self.output_buffer[0..n_frames] { - frame.clear(); - } - if reset { - all_notes_off(&mut self.output_buffer); - } - } - /// Write a note to the output buffer - pub fn buffer_write <'a> ( - &'a mut self, - sample: usize, - event: LiveEvent, - ) { - self.note_buffer.fill(0); - event.write(&mut self.note_buffer).expect("failed to serialize MIDI event"); - self.output_buffer[sample].push(self.note_buffer.clone()); - // Update the list of currently held notes. - if let LiveEvent::Midi { ref message, .. } = event { - update_keys(&mut*self.held.write().unwrap(), message); - } - } - /// Write a chunk of MIDI data from the output buffer to the output port. - pub fn buffer_emit (&mut self, scope: &ProcessScope) { - let samples = scope.n_frames() as usize; - let mut writer = self.port.writer(scope); - for (time, events) in self.output_buffer.iter().enumerate().take(samples) { - for bytes in events.iter() { - writer.write(&RawMidi { time: time as u32, bytes }).unwrap_or_else(|_|{ - panic!("Failed to write MIDI data: {bytes:?}"); - }); - } - } - } -} -impl MidiInput { - pub fn parsed <'a> (&'a self, scope: &'a ProcessScope) -> impl Iterator, &'a [u8])> { - parse_midi_input(self.port().iter(scope)) - } -} -impl> + AsMut>> HasMidiIns for T { - fn midi_ins (&self) -> &Vec { self.as_ref() } - fn midi_ins_mut (&mut self) -> &mut Vec { self.as_mut() } -} -impl> + AsMut>> HasMidiOuts for T { - fn midi_outs (&self) -> &Vec { self.as_ref() } - fn midi_outs_mut (&mut self) -> &mut Vec { self.as_mut() } -} -impl> AddMidiIn for T { - fn midi_in_add (&mut self) -> Usually<()> { - let index = self.midi_ins().len(); - let port = MidiInput::new(self.jack(), &format!("M/{index}"), &[])?; - self.midi_ins_mut().push(port); - Ok(()) - } -} -/// Trail for thing that may gain new MIDI ports. -impl> AddMidiOut for T { - fn midi_out_add (&mut self) -> Usually<()> { - let index = self.midi_outs().len(); - let port = MidiOutput::new(self.jack(), &format!("{index}/M"), &[])?; - self.midi_outs_mut().push(port); - Ok(()) - } -} - -/// May create new MIDI input ports. -pub trait AddMidiIn { - fn midi_in_add (&mut self) -> Usually<()>; -} - -/// May create new MIDI output ports. -pub trait AddMidiOut { - fn midi_out_add (&mut self) -> Usually<()>; -} diff --git a/src/device/port/register.rs b/src/device/port/register.rs deleted file mode 100644 index 0702a21a..00000000 --- a/src/device/port/register.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::*; - -pub trait RegisterPorts: HasJack<'static> { - /// Register a MIDI input port. - fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; - /// Register a MIDI output port. - fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; - /// Register an audio input port. - fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; - /// Register an audio output port. - fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; -} - -impl> RegisterPorts for J { - fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - MidiInput::new(self.jack(), name, connect) - } - fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - MidiOutput::new(self.jack(), name, connect) - } - fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - AudioInput::new(self.jack(), name, connect) - } - fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - AudioOutput::new(self.jack(), name, connect) - } -} diff --git a/src/device/sequence.rs b/src/device/sequence.rs index 3e280965..8088a404 100644 --- a/src/device/sequence.rs +++ b/src/device/sequence.rs @@ -1,5 +1,6 @@ use crate::{*, clock::*, device::*}; + impl +AsMut> HasSequencer for T {} /// A MIDI sequence. @@ -229,7 +230,6 @@ pub trait MidiRecord: MidiMonitor + HasClock + HasPlayClip { pub type MidiData = Vec>; pub type ClipPool = Vec>>; -pub type CollectedMidiInput<'a> = Vec, MidiError>)>>; pub trait HasClips { fn clips <'a> (&'a self) -> std::sync::RwLockReadGuard<'a, ClipPool>; @@ -592,35 +592,6 @@ pub(crate) fn note_y_iter (note_lo: usize, note_hi: usize, y0: u16) (note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n)) } -/// Return boxed iterator of MIDI events -pub fn parse_midi_input <'a> (input: ::tengri::jack::MidiIter<'a>) - -> Box, &'a [u8])> + 'a> -{ - Box::new(input.map(|::tengri::jack::RawMidi { time, bytes }|( - time as usize, - LiveEvent::parse(bytes).unwrap(), - bytes - ))) -} - -/// Add "all notes off" to the start of a buffer. -pub fn all_notes_off (output: &mut [Vec>]) { - let mut buf = vec![]; - let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() }; - let evt = LiveEvent::Midi { channel: 0.into(), message: msg }; - evt.write(&mut buf).unwrap(); - output[0].push(buf); -} - -/// Update notes_in array -pub fn update_keys (keys: &mut[bool;128], message: &MidiMessage) { - match message { - MidiMessage::NoteOn { key, .. } => { keys[key.as_int() as usize] = true; } - MidiMessage::NoteOff { key, .. } => { keys[key.as_int() as usize] = false; }, - _ => {} - } -} - /// Returns the next shorter length pub fn note_duration_prev (pulses: usize) -> usize { for (length, _) in NOTE_DURATIONS.iter().rev() { if *length < pulses { return *length } } diff --git a/src/tek.edn b/src/tek.edn index 02a8069c..2affc5b5 100644 --- a/src/tek.edn +++ b/src/tek.edn @@ -10,25 +10,18 @@ (keys :axis/i2 (@lt i2/dec) (@gt z2/inc)) (keys :axis/w (@openbracket w/dec) (@closebracket w/inc)) (keys :axis/w2 (@openbrace w2/dec) (@closebrace w2/inc)) - -(mode :menu - (name Menu) - (info Mode selector.) - (keys :axis/y :confirm) - (view (bg (g 0) (bsp/s - (bg (g 10) :ports/out) - (bsp/n - (bg (g 10) :ports/in) - (bg (g 30) (bsp/s - :logo - :dialog/menu))))))) - -(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)))))))) -(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)))))))) +(mode :menu (name Menu) (info Mode selector.) (keys :axis/y :confirm) (view (bg (g 0) + (bsp/s (exact/y 2 :transport) + (bsp/s (exact/y 3 (bg (g 80) :ports/out)) + (bsp/n (exact/y 3 (bg (g 80) :ports/in)) + (bg (g 30) (bsp/s (exact/y 6 (bg (g 70) :logo)) :dialog/menu)))))))) +(view :logo (bsp/s (exact/y 1 (text ~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~)) + (bsp/s (exact/y 1 (text ~~~~ ║ ~ ╟─╌ ~╟─< ~~ v0.3.0 ~~)) + (bsp/s (exact/y 1 (text ~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~)) (text dig?))))) +(view :ports/out (fill/x (bsp/s (fill/x (align/w (text L-AUDIO-OUT))) + (bsp/e (text MIDI-OUT) (fill/x (align/e (text AUDIO-OUT-R))))))) +(view :ports/in (fill/x (bsp/s (fill/x (align/w (text L-AUDIO-IN))) + (bsp/e (text MIDI-IN) (fill/x (align/e (text AUDIO-IN-R))))))) (mode :transport (name Transport) (info JACK transport controller.) (keys :clock :global) :transport) diff --git a/src/tek.rs b/src/tek.rs index 11b6dc8a..1f291ac9 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -6,37 +6,295 @@ trait_alias, type_changing_struct_update )] - pub extern crate atomic_float; pub extern crate xdg; -pub extern crate midly; pub extern crate tengri; - /// Command-line entrypoint. #[cfg(feature = "cli")] pub fn main () -> Usually<()> { - tui_setup_panic(); - Config::watch(|config|{ - tui_run_main(Jack::new_run("tek", move|jack|{ - let clock = Clock::new(&jack, Some(51.))?; - let project = Arrangement::new(&jack, None, clock, vec![], vec![], vec![], vec![]); - let app = App::new(&jack, project, config, ":menu"); - Ok(app) - })?) - }).map(|_|()) + tengri::Tui::setup_panic(); + let name = "tek"; + let mode = ":menu"; + let title = "untitled!"; + let bpm = 74.; + Config::watch(|config|tengri::Tui::run_main(Jack::new_run(name, move|jack|{ + let clock = Clock::new(&jack, Some(bpm))?; + let tracks = []; + let scenes = []; + let project = Arrangement::new( + &jack, title.into(), clock, + tracks.into_iter(), + scenes.into_iter(), + Connect::midi_ins(&jack, &"M", &[], None)?.into_iter(), + Connect::midi_outs(&jack, &"M", &[], None)?.into_iter(), + [].into_iter() + .chain(Connect::audio_ins(&jack, &"L", &[], None)?.into_iter()) + .chain(Connect::audio_ins(&jack, &"R", &[], None)?.into_iter()), + [].into_iter() + .chain(Connect::audio_outs(&jack, &"L", &[], None)?.into_iter()) + .chain(Connect::audio_outs(&jack, &"R", &[], None)?.into_iter()), + ); + Ok(App::new(&jack, project, config, mode)) + })?)).map(|_|()) } - pub mod deps; pub(crate) use self::deps::*; - pub mod device; pub use self::device::*; - pub mod app; pub use self::app::*; - /// CLI banner. pub(crate) const HEADER: &'static str = r#" - ~ █▀█▀█ █▀▀█ █ █ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~ █ █▀ █▀▀▄ ~ v0.4.0, 2026 heatwave edition ~ ~ ▀ █▀▀█ ▀ ▀ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ "#; +#[cfg(feature = "cli")] +mod cli { + + use crate::*; + + /// The command-line interface descriptor. + /// + /// ``` + /// let cli: tek::Cli = Default::default(); + /// + /// use clap::CommandFactory; + /// tek::Cli::command().debug_assert(); + /// ``` + #[derive(Parser)] + #[command(name = "tek", version, about = Some(HEADER), long_about = Some(HEADER))] + #[derive(Debug, Default)] pub struct Cli { + /// Pre-defined configuration modes. + /// + /// TODO: Replace these with scripted configurations. + #[command(subcommand)] pub action: Action, + } + + /// Application modes that can be passed to the mommand line interface. + /// + /// ``` + /// let action: tek::Action = Default::default(); + /// ``` + #[derive(Debug, Clone, Subcommand, Default)] pub enum Action { + /// Continue where you left off + #[default] Resume, + /// Run headlessly in current session. + Headless, + /// Show status of current session. + Status, + /// List known sessions. + List, + /// Continue work in a copy of the current session. + Fork, + /// Create a new empty session. + New { + /// Name of JACK client + #[arg(short='n', long)] name: Option, + /// Whether to attempt to become transport master + #[arg(short='Y', long, default_value_t = false)] sync_lead: bool, + /// Whether to sync to external transport master + #[arg(short='y', long, default_value_t = true)] sync_follow: bool, + /// Initial tempo in beats per minute + #[arg(short='b', long, default_value = None)] bpm: Option, + /// Whether to include a transport toolbar (default: true) + #[arg(short='c', long, default_value_t = true)] show_clock: bool, + /// MIDI outs to connect to (multiple instances accepted) + #[arg(short='I', long)] midi_from: Vec, + /// MIDI outs to connect to (multiple instances accepted) + #[arg(short='i', long)] midi_from_re: Vec, + /// MIDI ins to connect to (multiple instances accepted) + #[arg(short='O', long)] midi_to: Vec, + /// MIDI ins to connect to (multiple instances accepted) + #[arg(short='o', long)] midi_to_re: Vec, + /// Audio outs to connect to left input + #[arg(short='l', long)] left_from: Vec, + /// Audio outs to connect to right input + #[arg(short='r', long)] right_from: Vec, + /// Audio ins to connect from left output + #[arg(short='L', long)] left_to: Vec, + /// Audio ins to connect from right output + #[arg(short='R', long)] right_to: Vec, + /// Tracks to creat + #[arg(short='t', long)] tracks: Option, + /// Scenes to create + #[arg(short='s', long)] scenes: Option, + }, + /// Import media as new session. + Import, + /// Show configuration. + Config, + /// Show version. + Version, + } + + /// Command-line configuration. + #[cfg(feature = "cli")] + impl Cli { + pub fn run (&self) -> Usually<()> { + self.action.run(Config::init_new(None)?) + } + } + + #[cfg(feature = "cli")] + impl Action { + fn run (&self, config: Config) -> Usually<()> { + use Action::*; + match self { + Version => show_version(), + Config => print_config(&config), + List => todo!("list sessions"), + Resume => todo!("resume session"), + New { + name, bpm, tracks, scenes, + sync_lead, sync_follow, + midi_from: mf, midi_from_re: mfr, midi_to: mt, midi_to_re: mtr, + left_from: lf, right_from: rf, left_to: lt, right_to: rt, .. + } => { + let name = name.as_ref().map_or("tek", |x|x.as_str()); + let jack = Jack::new(&name)?; + let mut proj = Arrangement::new( + &jack, + name.into(), + Clock::new(&jack, None)?, + [].into_iter(), + [].into_iter(), + Connect::midi_ins(&jack, &"M", &[], None)?.into_iter(), + Connect::midi_outs(&jack, &"M", &[], None)?.into_iter(), + [].into_iter() + .chain(Connect::audio_ins(&jack, &"L", &[], None)?.into_iter()) + .chain(Connect::audio_ins(&jack, &"R", &[], None)?.into_iter()), + [].into_iter() + .chain(Connect::audio_outs(&jack, &"L", &[], None)?.into_iter()) + .chain(Connect::audio_outs(&jack, &"R", &[], None)?.into_iter()), + ); + //&jack, Clock::new(&jack, *bpm)?, &lf, <, &rf, &rt, &mf, &mt, &mfr, &mtr)?; + proj.tracks_add(tracks.unwrap_or(0), None, &[], &[])?; + proj.scenes_add(scenes.unwrap_or(0))?; + //if matches!(self, Action::Status) { + //// Show status and exit + //tek_print_status(&proj); + //return Ok(()) + //} + // Initialize the app state + let app = Arc::new(RwLock::new(App::new(&jack, proj, config, ":menu"))); + //if matches!(self, Action::Headless) { + //// TODO: Headless mode (daemon + client over IPC, then over network...) + //println!("todo headless"); + //return Ok(()) + //} + let (_keyboard, _terminal) = Exit::run(|exited|Tui::io( + exited.as_ref(), + &app, + Duration::from_millis(100), + Duration::from_millis(10), + std::io::stdout() + ))?; + }, + _ => todo!() + } + Ok(()) + } + } + + //pub fn tui ( + //app: Arc>, + //jack: Jack, + //sync_lead: &bool, + //sync_follow: &bool, + //) -> Usually<()> { + //// Run the [Tui] and [Jack] threads with the [App] state. + //Tui::run_main(&jack.run(move|jack|{ + //// Between jack init and app's first cycle: + ////jack.sync_lead(*sync_lead, |mut state|{ + ////let clock = app.write().unwrap().clock(); + ////clock.playhead.update_from_sample(state.position.frame() as f64); + ////state.position.bbt = Some(clock.bbt()); + ////state.position + ////})?; + ////jack.sync_follow(*sync_follow)?; + //// FIXME: They don't work properly. + //Ok(app) + //})?)? + //} + + pub fn show_version () { + println!("todo version"); + } + + pub fn print_config (config: &Config) { + use ::ansi_term::Color::*; + println!("{:?}", config.dirs); + for (k, v) in config.views.read().unwrap().iter() { + println!("{} {} {v}", Green.paint("VIEW"), Green.bold().paint(format!("{k:<16}"))); + } + for (k, v) in config.binds.read().unwrap().iter() { + println!("{} {}", Green.paint("BIND"), Green.bold().paint(format!("{k:<16}"))); + for (k, v) in v.0.iter() { + print!("{} ", &Yellow.paint(match &k.0 { + Event::Key(KeyEvent { modifiers, .. }) => + format!("{:>16}", format!("{modifiers}")), + _ => unimplemented!() + })); + print!("{}", &Yellow.bold().paint(match &k.0 { + Event::Key(KeyEvent { code, .. }) => + format!("{:<10}", format!("{code}")), + _ => unimplemented!() + })); + for v in v.iter() { + print!(" => {:?}", v.commands); + print!(" {}", v.condition.as_ref().map(|x|format!("{x:?}")).unwrap_or_default()); + println!(" {}", v.description.as_ref().map(|x|x.as_ref()).unwrap_or_default()); + //println!(" {:?}", v.source); + } + } + } + config.modes.for_each(|k, v|{ + println!(); + for v in v.name.iter() { print!("{}", Green.bold().paint(format!("{v} "))); } + for v in v.info.iter() { print!("\n{}", Green.paint(format!("{v}"))); } + print!("\n{} {}", Blue.paint("TOOL"), Green.bold().paint(format!("{k:<16}"))); + print!("\n{}", Blue.paint("KEYS")); + for v in v.keys.iter() { print!("{}", Green.paint(format!(" {v}"))); } + println!(); + v.modes.for_each(|k, v|{ + print!("{} {} {:?}", Blue.paint("MODE"), Green.bold().paint(format!("{k:<16}")), v.name); + print!( " INFO={:?}", v.info); + print!( " VIEW={:?}", v.view); + println!(" KEYS={:?}", v.keys); + }); + print!("{}", Blue.paint("VIEW")); + for v in v.view.iter() { print!("{}", Green.paint(format!(" {v}"))); } + println!(); + }); + } + + pub fn print_status (project: &Arrangement) { + println!("Name: {:?}", &project.name); + println!("JACK: {:?}", &project.jack); + println!("Buffer: {:?}", &project.clock.chunk); + println!("Sample rate: {:?}", &project.clock.timebase.sr); + println!("MIDI PPQ: {:?}", &project.clock.timebase.ppq); + println!("Tempo: {:?}", &project.clock.timebase.bpm); + println!("Quantize: {:?}", &project.clock.quant); + println!("Launch: {:?}", &project.clock.sync); + println!("Playhead: {:?}us", &project.clock.playhead.usec); + println!("Playhead: {:?}s", &project.clock.playhead.sample); + println!("Playhead: {:?}p", &project.clock.playhead.pulse); + println!("Started: {:?}", &project.clock.started); + println!("Tracks:"); + for (i, t) in project.tracks.iter().enumerate() { + println!(" Track {i}: {} {} {:?} {:?}", t.name, t.width, + &t.sequencer.play_clip, &t.sequencer.next_clip); + } + println!("Scenes:"); + for (i, t) in project.scenes.iter().enumerate() { + println!(" Scene {i}: {} {:?}", &t.name, &t.clips); + } + println!("MIDI Ins: {:?}", &project.midi_ins); + println!("MIDI Outs: {:?}", &project.midi_outs); + println!("Audio Ins: {:?}", &project.audio_ins); + println!("Audio Outs: {:?}", &project.audio_outs); + // TODO git integration + // TODO dawvert integration + } + +} diff --git a/tengri b/tengri index ad1a514e..c0d6d017 160000 --- a/tengri +++ b/tengri @@ -1 +1 @@ -Subproject commit ad1a514eddbf3104b7631dedef786389863988bf +Subproject commit c0d6d0174e8858108ec053f4ac486cf6229980e1