diff --git a/.dockerignore b/.dockerignore index 72e8ffc0..e69de29b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +0,0 @@ -* diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 7b08ef33..00000000 --- a/.editorconfig +++ /dev/null @@ -1,3 +0,0 @@ -root = true -[*] -max_line_length = 132 diff --git a/.envrc b/.envrc deleted file mode 100644 index 1d953f4b..00000000 --- a/.envrc +++ /dev/null @@ -1 +0,0 @@ -use nix diff --git a/.forgejo/workflows/build.nix b/.forgejo/workflows/build.nix new file mode 100644 index 00000000..cb702884 --- /dev/null +++ b/.forgejo/workflows/build.nix @@ -0,0 +1,38 @@ +{pkgs?import{}}: pkgs.mkShell (with pkgs; { + nativeBuildInputs = [ + cargo + pkg-config + freetype + libclang + cloc + #bear + ]; + buildInputs = [ + jack2 + lilv + serd + libclang + #suil + glib + gtk3 + ]; + LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ + pipewire.jack + # for ChowKick.lv2: + freetype + libgcc.lib + # for Panagement + xorg.libX11 + xorg.libXcursor + xorg.libXi + libxkbcommon + #suil + # for Helm: + alsa-lib + curl + libglvnd + #xorg_sys_opengl + ]; + VST3_SDK_DIR = "/home/user/Lab/Music/tek/vst3sdk/"; + LIBCLANG_PATH = "${pkgs.libclang.lib.outPath}/lib"; +}) diff --git a/.forgejo/workflows/build.yaml b/.forgejo/workflows/build.yaml new file mode 100644 index 00000000..2eed869f --- /dev/null +++ b/.forgejo/workflows/build.yaml @@ -0,0 +1,11 @@ +on: [push] +jobs: + build: + container: + image: nixos/nix:latest + steps: + - run: nix-channel --list && nix-channel --update + - run: nix-shell -p git --command 'git clone --recursive $GITHUB_SERVER_URL/$GITHUB_REPOSITORY .' + - run: whoami && pwd && ls -al + - run: nix-shell --command 'cargo version -vv && cargo test && cargo build --release && cloc crates/tek/src' .forgejo/workflows/build.nix + - run: nix-shell -p docker --command "docker run --security-opt seccomp=unconfined -v $PWD:/volume xd009642/tarpaulin cargo tarpaulin --out Html --all-features" diff --git a/.forgejo/workflows/release.yml.off b/.forgejo/workflows/release.yml.off deleted file mode 100644 index e2371720..00000000 --- a/.forgejo/workflows/release.yml.off +++ /dev/null @@ -1,36 +0,0 @@ -on: - push: - tags: '*' -jobs: - build: - runs-on: codeberg-small-lazy - container: { image: "alpine:edge" } - steps: - - - name: install deps - run: apk add --no-cache bash nodejs tree rustup git just cloc build-base clang20-dev pipewire-jack-dev lilv-dev serd-dev - - - run: git clone --depth 1 --recursive $GITHUB_SERVER_URL/$GITHUB_REPOSITORY . - - run: whoami && pwd && tree && cloc src/ && cloc . - - - run: rustup-init -y - - run: source "$HOME/.cargo/env" && rustup install nightly && rustup default nightly && cargo version -vv - #- run: source "$HOME/.cargo/env" && RUSTFLAGS="-Ctarget-feature=-crt-static" just doc - - run: source "$HOME/.cargo/env" && RUSTFLAGS="-Ctarget-feature=-crt-static" just test - - run: source "$HOME/.cargo/env" && RUSTFLAGS="-Ctarget-feature=-crt-static" just build-release - - - run: tree && mkdir -p .release && mv -v target/release/tek .release - - - name: publish release - uses: https://data.forgejo.org/actions/forgejo-release@v2.6.0 - with: - url: "https://codeberg.org" - direction: upload - tag: "${{ github.ref_name }}" - sha: "${{ github.sha }}" - release-dir: .release - override: true - verbose: true - #hide-archive-link: true - #token: ${{ secrets.TOKEN }} - #release-notes-assistant: true diff --git a/.forgejo/workflows/test.yaml b/.forgejo/workflows/test.yaml deleted file mode 100644 index 16d33122..00000000 --- a/.forgejo/workflows/test.yaml +++ /dev/null @@ -1,49 +0,0 @@ -on: - push: - branches: '*' -jobs: - build: - container: { image: "alpine:edge" } - steps: - - - name: install deps - run: apk add --no-cache nodejs tree rustup git just cloc build-base clang20-dev pipewire-jack-dev lilv-dev serd-dev - - - run: git clone --depth 1 --recursive $GITHUB_SERVER_URL/$GITHUB_REPOSITORY . - - run: whoami && pwd && tree && cloc src/ && cloc . - - #- id: cache - #name: cache restore - #uses: https://data.forgejo.org/actions/cache/restore@v4 - #with: - #key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - #path: | - #~/.cargo/bin/ - #~/.cargo/registry/index/ - #~/.cargo/registry/cache/ - #~/.cargo/git/db/ - #target/ - - #- name: cache hit - #if: steps.cache.outputs.cache-hit == 'true' - #run: echo "cache hit! :)" - #- name: cache miss - #if: steps.cache.outputs.cache-miss != 'true' - #run: echo "cache miss! :(" - - - run: cloc src/ && cloc . - - run: rustup-init -y - - run: source "$HOME/.cargo/env" && rustup install nightly && rustup default nightly && cargo version -vv - - run: source "$HOME/.cargo/env" && RUSTFLAGS="-Ctarget-feature=-crt-static" just test - - run: tree - - #- name: cache save - #uses: https://data.forgejo.org/actions/cache/save@v4 - #with: - #key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - #path: | - #~/.cargo/bin/ - #~/.cargo/registry/index/ - #~/.cargo/registry/cache/ - #~/.cargo/git/db/ - #target/ diff --git a/.gitignore b/.gitignore index e5790860..1a417612 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,5 @@ -target/* -!target/.gitkeep +target perf.data* flamegraph*.svg vgcore* example.mid -cov -*/cov -*.profraw -build/* -!build/README.md -!build/*.sh -!build/Dockerfile.* -.misc -.direnv diff --git a/.gitmodules b/.gitmodules index 15f065ba..3c5a123f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,9 +2,3 @@ path = rust-jack url = https://codeberg.org/unspeaker/rust-jack branch = timebase -[submodule "tengri"] - path = deps/tengri - url = ../tengri/ -[submodule "deps/rust-jack"] - path = deps/rust-jack - url = https://codeberg.org/unspeaker/rust-jack diff --git a/.old/from_arranger.rs b/.old/from_arranger.rs deleted file mode 100644 index 07502a6b..00000000 --- a/.old/from_arranger.rs +++ /dev/null @@ -1,188 +0,0 @@ - - -//pub struct ArrangerVCursor { - //cols: Vec<(usize, usize)>, - //rows: Vec<(usize, usize)>, - //color: ItemPalette, - //reticle: Reticle, - //selected: ArrangerSelection, - //scenes_w: u16, -//} - -//pub(crate) const HEADER_H: u16 = 0; // 5 -//pub(crate) const SCENES_W_OFFSET: u16 = 0; -//from!(|args:(&Arranger, usize)|ArrangerVCursor = Self { - //cols: Arranger::track_widths(&args.0.tracks), - //rows: Arranger::scene_heights(&args.0.scenes, args.1), - //selected: args.0.selected(), - //scenes_w: SCENES_W_OFFSET + ArrangerScene::longest_name(&args.0.scenes) as u16, - //color: args.0.color, - //reticle: Reticle(Style { - //fg: Some(args.0.color.lighter.rgb), - //bg: None, - //underline_color: None, - //add_modifier: Modifier::empty(), - //sub_modifier: Modifier::DIM - //}), -//}); -//impl Content for ArrangerVCursor { - //fn render (&self, to: &mut TuiOut) { - //let area = to.area(); - //let focused = true; - //let selected = self.selected; - //let get_track_area = |t: usize| [ - //self.scenes_w + area.x() + self.cols[t].1 as u16, area.y(), - //self.cols[t].0 as u16, area.h(), - //]; - //let get_scene_area = |s: usize| [ - //area.x(), HEADER_H + area.y() + (self.rows[s].1 / PPQ) as u16, - //area.w(), (self.rows[s].0 / PPQ) as u16 - //]; - //let get_clip_area = |t: usize, s: usize| [ - //(self.scenes_w + area.x() + self.cols[t].1 as u16).saturating_sub(1), - //HEADER_H + area.y() + (self.rows[s].1/PPQ) as u16, - //self.cols[t].0 as u16 + 2, - //(self.rows[s].0 / PPQ) as u16 - //]; - //let mut track_area: Option<[u16;4]> = None; - //let mut scene_area: Option<[u16;4]> = None; - //let mut clip_area: Option<[u16;4]> = None; - //let area = match selected { - //ArrangerSelection::Mix => area, - //ArrangerSelection::Track(t) => { - //track_area = Some(get_track_area(t)); - //area - //}, - //ArrangerSelection::Scene(s) => { - //scene_area = Some(get_scene_area(s)); - //area - //}, - //ArrangerSelection::Clip(t, s) => { - //track_area = Some(get_track_area(t)); - //scene_area = Some(get_scene_area(s)); - //clip_area = Some(get_clip_area(t, s)); - //area - //}, - //}; - //let bg = self.color.lighter.rgb;//Color::Rgb(0, 255, 0); - //if let Some([x, y, width, height]) = track_area { - //to.fill_fg([x, y, 1, height], bg); - //to.fill_fg([x + width, y, 1, height], bg); - //} - //if let Some([_, y, _, height]) = scene_area { - //to.fill_ul([area.x(), y - 1, area.w(), 1], bg); - //to.fill_ul([area.x(), y + height - 1, area.w(), 1], bg); - //} - //if focused { - //to.place(if let Some(clip_area) = clip_area { - //clip_area - //} else if let Some(track_area) = track_area { - //track_area.clip_h(HEADER_H) - //} else if let Some(scene_area) = scene_area { - //scene_area.clip_w(self.scenes_w) - //} else { - //area.clip_w(self.scenes_w).clip_h(HEADER_H) - //}, &self.reticle) - //}; - //} -//} -//impl Arranger { - //fn render_mode (state: &Self) -> impl Content + use<'_> { - //match state.mode { - //ArrangerMode::H => todo!("horizontal arranger"), - //ArrangerMode::V(factor) => Self::render_mode_v(state, factor), - //} - //} -//} -//render!(TuiOut: (self: Arranger) => { - //let pool_w = if self.pool.visible { self.splits[1] } else { 0 }; - //let color = self.color; - //let layout = Bsp::a(Fill::xy(ArrangerStatus::from(self)), - //Bsp::n(Fixed::x(pool_w, PoolView(self.pool.visible, &self.pool)), - //Bsp::n(TransportView::new(true, &self.clock), - //Bsp::s(Fixed::y(1, MidiEditStatus(&self.editor)), - //Bsp::n(Fill::x(Fixed::y(20, - //Bsp::a(Fill::xy(Tui::bg(color.darkest.rgb, "background")), - //Bsp::a( - //Fill::xy(Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb))), - //Self::render_mode(self))))), Fill::y(&"fixme: self.editor")))))); - //self.size.of(layout) -//}); - //Align::n(Fill::xy(lay!( - //Align::n(Fill::xy(Tui::bg(self.color.darkest.rgb, " "))), - //Align::n(Fill::xy(ArrangerVRowSep::from((self, 1)))), - //Align::n(Fill::xy(ArrangerVColSep::from(self))), - //Align::n(Fill::xy(ArrangerVClips::new(self, 1))), - //Align::n(Fill::xy(ArrangerVCursor::from((self, 1)))))))))))))))); - //Align::n(Fill::xy(":"))))))))))))); - //"todo:")))))))); - //Bsp::s( - //Align::n(Fixed::y(1, Fill::x(ArrangerVIns::from(self)))), - //Bsp::s( - //Fixed::y(20, Align::n(ArrangerVClips::new(self, 1))), - //Fill::x(Fixed::y(1, ArrangerVOuts::from(self))))))))))))); - //Bsp::s( - //Bsp::s( - //Bsp::s( - //Fill::xy(ArrangerVClips::new(self, 1)), - //Fill::x(ArrangerVOuts::from(self))))) - - //let cell = phat_sel_3( - //selected_track == Some(i) && selected_scene == Some(j), - //Tui::fg(TuiTheme::g(64), Push::x(1, name)), - //Tui::fg(TuiTheme::g(64), Push::x(1, name)), - //if selected_track == Some(i) && selected_scene.map(|s|s+1) == Some(j) { - //None - //} else { - //Some(TuiTheme::g(32).into()) - //}, - //TuiTheme::g(32).into(), - //TuiTheme::g(32).into(), - //); - // TODO: port per track: - //for connection in midi_from.iter() { - //let mut split = connection.as_ref().split("="); - //let number = split.next().unwrap().trim(); - //if let Ok(track) = number.parse::() { - //if track < 1 { - //panic!("Tracks start from 1") - //} - //if track > count { - //panic!("Tried to connect track {track} or {count}. Pass -t {track} to increase number of tracks.") - //} - //if let Some(port) = split.next() { - //if let Some(port) = jack.read().unwrap().client().port_by_name(port).as_ref() { - ////jack.read().unwrap().client().connect_ports(port, &self.tracks[track-1].player.midi_ins[0])?; - //} else { - //panic!("Missing MIDI output: {port}. Use jack_lsp to list all port names."); - //} - //} else { - //panic!("No port specified for track {track}. Format is TRACK_NUMBER=PORT_NAME") - //} - //} else { - //panic!("Failed to parse track number: {number}") - //} - //} - //for connection in midi_to.iter() { - //let mut split = connection.as_ref().split("="); - //let number = split.next().unwrap().trim(); - //if let Ok(track) = number.parse::() { - //if track < 1 { - //panic!("Tracks start from 1") - //} - //if track > count { - //panic!("Tried to connect track {track} or {count}. Pass -t {track} to increase number of tracks.") - //} - //if let Some(port) = split.next() { - //if let Some(port) = jack.read().unwrap().client().port_by_name(port).as_ref() { - ////jack.read().unwrap().client().connect_ports(&self.tracks[track-1].player.midi_outs[0], port)?; - //} else { - //panic!("Missing MIDI input: {port}. Use jack_lsp to list all port names."); - //} - //} else { - //panic!("No port specified for track {track}. Format is TRACK_NUMBER=PORT_NAME") - //} - //} else { - //panic!("Failed to parse track number: {number}") - //} - //} diff --git a/.old/midi.scratch.rs b/.old/midi.scratch.rs deleted file mode 100644 index 8602766a..00000000 --- a/.old/midi.scratch.rs +++ /dev/null @@ -1,31 +0,0 @@ -/////////////////////////////////////////////////////////////////////////////////////////////////// -//keymap!(KEYS_MIDI_EDITOR = |s: MidiEditor, _input: Event| MidiEditCommand { - //key(Up) => SetNoteCursor(s.note_pos() + 1), - //key(Char('w')) => SetNoteCursor(s.note_pos() + 1), - //key(Down) => SetNoteCursor(s.note_pos().saturating_sub(1)), - //key(Char('s')) => SetNoteCursor(s.note_pos().saturating_sub(1)), - //key(Left) => SetTimeCursor(s.time_pos().saturating_sub(s.note_len())), - //key(Char('a')) => SetTimeCursor(s.time_pos().saturating_sub(s.note_len())), - //key(Right) => SetTimeCursor((s.time_pos() + s.note_len()) % s.clip_length()), - //ctrl(alt(key(Up))) => SetNoteScroll(s.note_pos() + 3), - //ctrl(alt(key(Down))) => SetNoteScroll(s.note_pos().saturating_sub(3)), - //ctrl(alt(key(Left))) => SetTimeScroll(s.time_pos().saturating_sub(s.time_zoom().get())), - //ctrl(alt(key(Right))) => SetTimeScroll((s.time_pos() + s.time_zoom().get()) % s.clip_length()), - //ctrl(key(Up)) => SetNoteScroll(s.note_lo().get() + 1), - //ctrl(key(Down)) => SetNoteScroll(s.note_lo().get().saturating_sub(1)), - //ctrl(key(Left)) => SetTimeScroll(s.time_start().get().saturating_sub(s.note_len())), - //ctrl(key(Right)) => SetTimeScroll(s.time_start().get() + s.note_len()), - //alt(key(Up)) => SetNoteCursor(s.note_pos() + 3), - //alt(key(Down)) => SetNoteCursor(s.note_pos().saturating_sub(3)), - //alt(key(Left)) => SetTimeCursor(s.time_pos().saturating_sub(s.time_zoom().get())), - //alt(key(Right)) => SetTimeCursor((s.time_pos() + s.time_zoom().get()) % s.clip_length()), - //key(Char('d')) => SetTimeCursor((s.time_pos() + s.note_len()) % s.clip_length()), - //key(Char('z')) => SetTimeLock(!s.time_lock().get()), - //key(Char('-')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::next(s.time_zoom().get()) }), - //key(Char('_')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::next(s.time_zoom().get()) }), - //key(Char('=')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::prev(s.time_zoom().get()) }), - //key(Char('+')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::prev(s.time_zoom().get()) }), - ////// TODO: kpat!(Char('/')) => // toggle 3plet - ////// TODO: kpat!(Char('?')) => // toggle dotted -//}); - diff --git a/.old/midi_import.rs b/.old/midi_import.rs deleted file mode 100644 index d2cceae2..00000000 --- a/.old/midi_import.rs +++ /dev/null @@ -1,20 +0,0 @@ -use tek::*; -use tengri::input::*; -use std::sync::*; -struct ExampleClips(Arc>>>>); -impl HasClips for ExampleClips { - fn clips (&self) -> RwLockReadGuard<'_, Vec>>> { - self.0.read().unwrap() - } - fn clips_mut (&self) -> RwLockWriteGuard<'_, Vec>>> { - self.0.write().unwrap() - } -} -fn main () -> Result<(), Box> { - let mut clips = Pool::default();//ExampleClips(Arc::new(vec![].into())); - PoolClipCommand::Import { - index: 0, - path: std::path::PathBuf::from("./example.mid") - }.execute(&mut clips)?; - Ok(()) -} diff --git a/.old/sampler_scratch.rs b/.old/sampler_scratch.rs deleted file mode 100644 index 82009355..00000000 --- a/.old/sampler_scratch.rs +++ /dev/null @@ -1,105 +0,0 @@ -//handle!(TuiIn: |self: Sampler, input|SamplerCommand::execute_with_state(self, input.event())); -//input_to_command!(SamplerCommand: |state: Sampler, input: Event|match state.mode{ - //Some(SamplerMode::Import(..)) => Self::Import( - //FileBrowserCommand::input_to_command(state, input)? - //), - //_ => match input { - //// load sample - //kpat!(Shift-Char('L')) => Self::Import(FileBrowserCommand::Begin), - //kpat!(KeyCode::Up) => Self::Select(state.note_pos().overflowing_add(1).0.min(127)), - //kpat!(KeyCode::Down) => Self::Select(state.note_pos().overflowing_sub(1).0.min(127)), - //_ => return None - //} -//}); -//impl Handle for AddSampleModal { - //fn handle (&mut self, from: &TuiIn) -> Perhaps { - //if from.handle_keymap(self, KEYMAP_ADD_SAMPLE)? { - //return Ok(Some(true)) - //} - //Ok(Some(true)) - //} -//} -//pub const KEYMAP_ADD_SAMPLE: &'static [KeyBinding] = keymap!(AddSampleModal { - //[Esc, NONE, "sampler/add/close", "close help dialog", |modal: &mut AddSampleModal|{ - //modal.exit(); - //Ok(true) - //}], - //[Up, NONE, "sampler/add/prev", "select previous entry", |modal: &mut AddSampleModal|{ - //modal.prev(); - //Ok(true) - //}], - //[Down, NONE, "sampler/add/next", "select next entry", |modal: &mut AddSampleModal|{ - //modal.next(); - //Ok(true) - //}], - //[Enter, NONE, "sampler/add/enter", "activate selected entry", |modal: &mut AddSampleModal|{ - //if modal.pick()? { - //modal.exit(); - //} - //Ok(true) - //}], - //[Char('p'), NONE, "sampler/add/preview", "preview selected entry", |modal: &mut AddSampleModal|{ - //modal.try_preview()?; - //Ok(true) - //}] -//}); -//from_atom!("sampler" => |jack: &Jack, args| -> crate::Sampler { - //let mut name = String::new(); - //let mut dir = String::new(); - //let mut samples = BTreeMap::new(); - //atom!(atom in args { - //Atom::Map(map) => { - //if let Some(Atom::Str(n)) = map.get(&Atom::Key(":name")) { - //name = String::from(*n); - //} - //if let Some(Atom::Str(n)) = map.get(&Atom::Key(":dir")) { - //dir = String::from(*n); - //} - //}, - //Atom::List(args) => match args.first() { - //Some(Atom::Symbol("sample")) => { - //let (midi, sample) = MidiSample::from_atom((jack, &dir), &args[1..])?; - //if let Some(midi) = midi { - //samples.insert(midi, sample); - //} else { - //panic!("sample without midi binding: {}", sample.read().unwrap().name); - //} - //}, - //_ => panic!("unexpected in sampler {name}: {args:?}") - //}, - //_ => panic!("unexpected in sampler {name}: {atom:?}") - //}); - //Self::new(jack, &name) -//}); -//from_atom!("sample" => |(_jack, dir): (&Jack, &str), args| -> MidiSample { - //let mut name = String::new(); - //let mut file = String::new(); - //let mut midi = None; - //let mut start = 0usize; - //atom!(atom in args { - //Atom::Map(map) => { - //if let Some(Atom::Str(n)) = map.get(&Atom::Key(":name")) { - //name = String::from(*n); - //} - //if let Some(Atom::Str(f)) = map.get(&Atom::Key(":file")) { - //file = String::from(*f); - //} - //if let Some(Atom::Int(i)) = map.get(&Atom::Key(":start")) { - //start = *i as usize; - //} - //if let Some(Atom::Int(m)) = map.get(&Atom::Key(":midi")) { - //midi = Some(u7::from(*m as u8)); - //} - //}, - //_ => panic!("unexpected in sample {name}"), - //}); - //let (end, data) = Sample::read_data(&format!("{dir}/{file}"))?; - //Ok((midi, Arc::new(RwLock::new(crate::Sample { - //name, - //start, - //end, - //channels: data, - //rate: None, - //gain: 1.0 - //})))) -//}); diff --git a/.old/tek.rs.old b/.old/tek.rs.old deleted file mode 100644 index e64fd51b..00000000 --- a/.old/tek.rs.old +++ /dev/null @@ -1,2113 +0,0 @@ -/////////////////////////////////////////////////////////////////////////////////////////////////// - -//#[cfg(test)] mod test_focus { - //use super::focus::*; - //#[test] fn test_focus () { - - //struct FocusTest { - //focused: char, - //cursor: (usize, usize) - //} - - //impl HasFocus for FocusTest { - //type Item = char; - //fn focused (&self) -> Self::Item { - //self.focused - //} - //fn set_focused (&mut self, to: Self::Item) { - //self.focused = to - //} - //} - - //impl FocusGrid for FocusTest { - //fn focus_cursor (&self) -> (usize, usize) { - //self.cursor - //} - //fn focus_cursor_mut (&mut self) -> &mut (usize, usize) { - //&mut self.cursor - //} - //fn focus_layout (&self) -> &[&[Self::Item]] { - //&[ - //&['a', 'a', 'a', 'b', 'b', 'd'], - //&['a', 'a', 'a', 'b', 'b', 'd'], - //&['a', 'a', 'a', 'c', 'c', 'd'], - //&['a', 'a', 'a', 'c', 'c', 'd'], - //&['e', 'e', 'e', 'e', 'e', 'e'], - //] - //} - //} - - //let mut tester = FocusTest { focused: 'a', cursor: (0, 0) }; - - //tester.focus_right(); - //assert_eq!(tester.cursor.0, 3); - //assert_eq!(tester.focused, 'b'); - - //tester.focus_down(); - //assert_eq!(tester.cursor.1, 2); - //assert_eq!(tester.focused, 'c'); - - //} -//} -//use crate::*; - -//struct TestEngine([u16;4], Vec>); - -//impl Engine for TestEngine { - //type Unit = u16; - //type Size = [Self::Unit;2]; - //type Area = [Self::Unit;4]; - //type Input = Self; - //type Handled = bool; - //fn exited (&self) -> bool { - //true - //} -//} - -//#[derive(Copy, Clone)] -//struct TestArea(u16, u16); - -//impl Render for TestArea { - //fn min_size (&self, to: [u16;2]) -> Perhaps<[u16;2]> { - //Ok(Some([to[0], to[1], self.0, self.1])) - //} - //fn render (&self, to: &mut TestEngine) -> Perhaps<[u16;4]> { - //if let Some(layout) = self.layout(to.area())? { - //for y in layout.y()..layout.y()+layout.h()-1 { - //for x in layout.x()..layout.x()+layout.w()-1 { - //to.1[y as usize][x as usize] = '*'; - //} - //} - //Ok(Some(layout)) - //} else { - //Ok(None) - //} - //} -//} - -//#[test] -//fn test_plus_minus () -> Usually<()> { - //let area = [0, 0, 10, 10]; - //let engine = TestEngine(area, vec![vec![' ';10];10]); - //let test = TestArea(4, 4); - //assert_eq!(test.layout(area)?, Some([0, 0, 4, 4])); - //assert_eq!(Push::X(1, test).layout(area)?, Some([1, 0, 4, 4])); - //Ok(()) -//} - -//#[test] -//fn test_outset_align () -> Usually<()> { - //let area = [0, 0, 10, 10]; - //let engine = TestEngine(area, vec![vec![' ';10];10]); - //let test = TestArea(4, 4); - //assert_eq!(test.layout(area)?, Some([0, 0, 4, 4])); - //assert_eq!(Margin::X(1, test).layout(area)?, Some([0, 0, 6, 4])); - //assert_eq!(Align::X(test).layout(area)?, Some([3, 0, 4, 4])); - //assert_eq!(Align::X(Margin::X(1, test)).layout(area)?, Some([2, 0, 6, 4])); - //assert_eq!(Margin::X(1, Align::X(test)).layout(area)?, Some([2, 0, 6, 4])); - //Ok(()) -//} - -////#[test] -////fn test_misc () -> Usually<()> { - ////let area: [u16;4] = [0, 0, 10, 10]; - ////let test = TestArea(4, 4); - ////assert_eq!(test.layout(area)?, - ////Some([0, 0, 4, 4])); - ////assert_eq!(Align::Center(test).layout(area)?, - ////Some([3, 3, 4, 4])); - ////assert_eq!(Align::Center(Stack::down(|add|{ - ////add(&test)?; - ////add(&test) - ////})).layout(area)?, - ////Some([3, 1, 4, 8])); - ////assert_eq!(Align::Center(Stack::down(|add|{ - ////add(&Margin::XY(2, 2, test))?; - ////add(&test) - ////})).layout(area)?, - ////Some([2, 0, 6, 10])); - ////assert_eq!(Align::Center(Stack::down(|add|{ - ////add(&Margin::XY(2, 2, test))?; - ////add(&Padding::XY(2, 2, test)) - ////})).layout(area)?, - ////Some([2, 1, 6, 8])); - ////assert_eq!(Stack::down(|add|{ - ////add(&Margin::XY(2, 2, test))?; - ////add(&Padding::XY(2, 2, test)) - ////}).layout(area)?, - ////Some([0, 0, 6, 8])); - ////assert_eq!(Stack::right(|add|{ - ////add(&Stack::down(|add|{ - ////add(&Margin::XY(2, 2, test))?; - ////add(&Padding::XY(2, 2, test)) - ////}))?; - ////add(&Align::Center(TestArea(2 ,2))) - ////}).layout(area)?, - ////Some([0, 0, 8, 8])); - ////Ok(()) -////} - -////#[test] -////fn test_offset () -> Usually<()> { - ////let area: [u16;4] = [50, 50, 100, 100]; - ////let test = TestArea(3, 3); - ////assert_eq!(Push::X(1, test).layout(area)?, Some([51, 50, 3, 3])); - ////assert_eq!(Push::Y(1, test).layout(area)?, Some([50, 51, 3, 3])); - ////assert_eq!(Push::XY(1, 1, test).layout(area)?, Some([51, 51, 3, 3])); - ////Ok(()) -////} - -////#[test] -////fn test_outset () -> Usually<()> { - ////let area: [u16;4] = [50, 50, 100, 100]; - ////let test = TestArea(3, 3); - ////assert_eq!(Margin::X(1, test).layout(area)?, Some([49, 50, 5, 3])); - ////assert_eq!(Margin::Y(1, test).layout(area)?, Some([50, 49, 3, 5])); - ////assert_eq!(Margin::XY(1, 1, test).layout(area)?, Some([49, 49, 5, 5])); - ////Ok(()) -////} - -////#[test] -////fn test_padding () -> Usually<()> { - ////let area: [u16;4] = [50, 50, 100, 100]; - ////let test = TestArea(3, 3); - ////assert_eq!(Padding::X(1, test).layout(area)?, Some([51, 50, 1, 3])); - ////assert_eq!(Padding::Y(1, test).layout(area)?, Some([50, 51, 3, 1])); - ////assert_eq!(Padding::XY(1, 1, test).layout(area)?, Some([51, 51, 1, 1])); - ////Ok(()) -////} - -////#[test] -////fn test_stuff () -> Usually<()> { - ////let area: [u16;4] = [0, 0, 100, 100]; - ////assert_eq!("1".layout(area)?, - ////Some([0, 0, 1, 1])); - ////assert_eq!("333".layout(area)?, - ////Some([0, 0, 3, 1])); - ////assert_eq!(Layers::new(|add|{add(&"1")?;add(&"333")}).layout(area)?, - ////Some([0, 0, 3, 1])); - ////assert_eq!(Stack::down(|add|{add(&"1")?;add(&"333")}).layout(area)?, - ////Some([0, 0, 3, 2])); - ////assert_eq!(Stack::right(|add|{add(&"1")?;add(&"333")}).layout(area)?, - ////Some([0, 0, 4, 1])); - ////assert_eq!(Stack::down(|add|{ - ////add(&Stack::right(|add|{add(&"1")?;add(&"333")}))?; - ////add(&"55555") - ////}).layout(area)?, - ////Some([0, 0, 5, 2])); - ////let area: [u16;4] = [1, 1, 100, 100]; - ////assert_eq!(Margin::X(1, Stack::right(|add|{add(&"1")?;add(&"333")})).layout(area)?, - ////Some([0, 1, 6, 1])); - ////assert_eq!(Margin::Y(1, Stack::right(|add|{add(&"1")?;add(&"333")})).layout(area)?, - ////Some([1, 0, 4, 3])); - ////assert_eq!(Margin::XY(1, 1, Stack::right(|add|{add(&"1")?;add(&"333")})).layout(area)?, - ////Some([0, 0, 6, 3])); - ////assert_eq!(Stack::down(|add|{ - ////add(&Margin::XY(1, 1, "1"))?; - ////add(&Margin::XY(1, 1, "333")) - ////}).layout(area)?, - ////Some([1, 1, 5, 6])); - ////let area: [u16;4] = [1, 1, 95, 100]; - ////assert_eq!(Align::Center(Stack::down(|add|{ - ////add(&Margin::XY(1, 1, "1"))?; - ////add(&Margin::XY(1, 1, "333")) - ////})).layout(area)?, - ////Some([46, 48, 5, 6])); - ////assert_eq!(Align::Center(Stack::down(|add|{ - ////add(&Layers::new(|add|{ - //////add(&Margin::XY(1, 1, Background(Color::Rgb(0,128,0))))?; - ////add(&Margin::XY(1, 1, "1"))?; - ////add(&Margin::XY(1, 1, "333"))?; - //////add(&Background(Color::Rgb(0,128,0)))?; - ////Ok(()) - ////}))?; - ////add(&Layers::new(|add|{ - //////add(&Margin::XY(1, 1, Background(Color::Rgb(0,0,128))))?; - ////add(&Margin::XY(1, 1, "555"))?; - ////add(&Margin::XY(1, 1, "777777"))?; - //////add(&Background(Color::Rgb(0,0,128)))?; - ////Ok(()) - ////})) - ////})).layout(area)?, - ////Some([46, 48, 5, 6])); - ////Ok(()) -////} - -//#[derive(Default)] pub struct Sequencer { - //pub jack: Arc>, - //pub compact: bool, - //pub editor: MidiEditor, - //pub midi_buf: Vec>>, - //pub note_buf: Vec, - //pub perf: PerfModel, - //pub player: MidiPlayer, - //pub pool: MidiPool, - //pub selectors: bool, - //pub size: Measure, - //pub status: bool, - //pub transport: bool, -//} -//has_size!(|self:Sequencer|&self.size); -//has_clock!(|self:Sequencer|&self.player.clock); -//has_clips!(|self:Sequencer|self.pool.clips); -//has_editor!(|self:Sequencer|self.editor); -//has_player!(|self:Sequencer|self.player); - -//#[derive(Default)] pub struct Groovebox { - //pub jack: Arc>, - //pub compact: bool, - //pub editor: MidiEditor, - //pub midi_buf: Vec>>, - //pub note_buf: Vec, - //pub perf: PerfModel, - //pub player: MidiPlayer, - //pub pool: MidiPool, - //pub sampler: Sampler, - //pub size: Measure, - //pub status: bool, -//} -//has_clock!(|self: Groovebox|self.player.clock()); - -//#[derive(Default)] pub struct Arranger { - //pub clock: Clock, - //pub color: ItemPalette, - //pub compact: bool, - //pub editing: AtomicBool, - //pub editor: MidiEditor, - //pub jack: Arc>, - //pub midi_buf: Vec>>, - //pub midi_ins: Vec>, - //pub midi_outs: Vec>, - //pub note_buf: Vec, - //pub perf: PerfModel, - //pub pool: MidiPool, - //pub scenes: Vec, - //pub selected: ArrangerSelection, - //pub size: Measure, - //pub splits: [u16;2], - //pub tracks: Vec, -//} -//has_clock!(|self: Arranger|&self.clock); -//has_clips!(|self: Arranger|self.pool.clips); -//has_editor!(|self: Arranger|self.editor); - -/////////////////////////////////////////////////////////////////////////////////////////////////// - -//render!(TuiOut: (self: Sequencer) => self.size.of(EdnView::from_source(self, Self::EDN))); -//impl EdnViewData for &Sequencer { - //fn get_content <'a> (&'a self, item: EdnItem<&'a str>) -> RenderBox<'a, TuiOut> { - //use EdnItem::*; - //match item { - //Nil => Box::new(()), - //Exp(items) => Box::new(EdnView::from_items(*self, items.as_slice())), - //Sym(":editor") => (&self.editor).boxed(), - //Sym(":pool") => self.pool_view().boxed(), - //Sym(":status") => self.status_view().boxed(), - //Sym(":toolbar") => self.toolbar_view().boxed(), - //_ => panic!("no content for {item:?}") - //} - //} -//} -//impl Sequencer { - //const EDN: &'static str = include_str!("../edn/sequencer.edn"); - //fn toolbar_view (&self) -> impl Content + use<'_> { - //Fill::x(Fixed::y(2, Align::x(ClockView::new(true, &self.player.clock)))) - //} - //fn status_view (&self) -> impl Content + use<'_> { - //Bsp::e( - //When(self.selectors, Bsp::e( - //self.player.play_status(), - //self.player.next_status(), - //)), - //Bsp::e( - //self.editor.clip_status(), - //self.editor.edit_status(), - //) - //) - //} - //fn pool_view (&self) -> impl Content + use<'_> { - //let w = self.size.w(); - //let clip_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; - //let pool_w = if self.pool.visible { clip_w } else { 0 }; - //let pool = Pull::y(1, Fill::y(Align::e(PoolView(self.pool.visible, &self.pool)))); - //Fixed::x(pool_w, Align::e(Fill::y(PoolView(self.compact, &self.pool)))) - //} - //fn help () -> impl Content { - //let single = |binding, command|row!(" ", col!( - //Tui::fg(TuiTheme::yellow(), binding), - //command - //)); - //let double = |(b1, c1), (b2, c2)|col!( - //row!(" ", Tui::fg(TuiTheme::yellow(), b1), " ", c1,), - //row!(" ", Tui::fg(TuiTheme::yellow(), b2), " ", c2,), - //); - //Tui::fg_bg(TuiTheme::g(255), TuiTheme::g(50), row!( - //single("SPACE", "play/pause"), - //double(("▲▼▶◀", "cursor"), ("Ctrl", "scroll"), ), - //double(("a", "append"), ("s", "set note"),), - //double((",.", "length"), ("<>", "triplet"), ), - //double(("[]", "clip"), ("{}", "order"), ), - //double(("q", "enqueue"), ("e", "edit"), ), - //double(("c", "color"), ("", ""),), - //)) - //} -//} -///////////////////////////////////////////////////////////////////////////////////////////////////// -//render!(TuiOut: (self: Groovebox) => self.size.of(EdnView::from_source(self, Self::EDN))); -//impl EdnViewData for &Groovebox { - //fn get_content <'a> (&'a self, item: EdnItem<&'a str>) -> RenderBox<'a, TuiOut> { - //use EdnItem::*; - //match item { - //Nil => Box::new(()), - //Exp(items) => Box::new(EdnView::from_items(*self, items.as_slice())), - //Sym(":editor") => (&self.editor).boxed(), - //Sym(":pool") => self.pool().boxed(), - //Sym(":status") => self.status().boxed(), - //Sym(":toolbar") => self.toolbar().boxed(), - //Sym(":sampler") => self.sampler().boxed(), - //Sym(":sample") => self.sample().boxed(), - //_ => panic!("no content for {item:?}") - //} - //} - //fn get_unit (&self, item: EdnItem<&str>) -> u16 { - //use EdnItem::*; - //match item.to_str() { - //":sample-h" => if self.compact { 0 } else { 5 }, - //":samples-w" => if self.compact { 4 } else { 11 }, - //":samples-y" => if self.compact { 1 } else { 0 }, - //":pool-w" => if self.compact { 5 } else { - //let w = self.size.w(); - //if w > 60 { 20 } else if w > 40 { 15 } else { 10 } - //}, - //_ => 0 - //} - //} -//} -//impl Groovebox { - //const EDN: &'static str = include_str!("../edn/groovebox.edn"); - //fn toolbar (&self) -> impl Content + use<'_> { - //Fill::x(Fixed::y(2, lay!( - //Fill::x(Align::w(Meter("L/", self.sampler.input_meter[0]))), - //Fill::x(Align::e(Meter("R/", self.sampler.input_meter[1]))), - //Align::x(ClockView::new(true, &self.player.clock)), - //))) - //} - //fn status (&self) -> impl Content + use<'_> { - //row!( - //self.player.play_status(), - //self.player.next_status(), - //self.editor.clip_status(), - //self.editor.edit_status(), - //) - //} - //fn sample (&self) -> impl Content + use<'_> { - //let note_pt = self.editor.note_point(); - //let sample_h = if self.compact { 0 } else { 5 }; - //Max::y(sample_h, Fill::xy( - //Bsp::a( - //Fill::x(Align::w(Fixed::y(1, self.sampler.status(note_pt)))), - //self.sampler.viewer(note_pt)))) - //} - //fn pool (&self) -> impl Content + use<'_> { - //let w = self.size.w(); - //let pool_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; - //Fixed::x(if self.compact { 5 } else { pool_w }, - //PoolView(self.compact, &self.pool)) - //} - //fn sampler (&self) -> impl Content + use<'_> { - //let note_pt = self.editor.note_point(); - //let sampler_w = if self.compact { 4 } else { 40 }; - //let sampler_y = if self.compact { 1 } else { 0 }; - //Fixed::x(sampler_w, Push::y(sampler_y, Fill::y(self.sampler.list(self.compact, &self.editor)))) - //} -//} - -///// Status bar for sequencer app -//#[derive(Clone)] -//pub struct GrooveboxStatus { - //pub(crate) width: usize, - //pub(crate) cpu: Option, - //pub(crate) size: String, - //pub(crate) playing: bool, -//} -//from!(|state: &Groovebox|GrooveboxStatus = { - //let samples = state.clock().chunk.load(Relaxed); - //let rate = state.clock().timebase.sr.get(); - //let buffer = samples as f64 / rate; - //let width = state.size.w(); - //Self { - //width, - //playing: state.clock().is_rolling(), - //cpu: state.perf.percentage().map(|cpu|format!("│{cpu:.01}%")), - //size: format!("{}x{}│", width, state.size.h()), - //} -//}); -//render!(TuiOut: (self: GrooveboxStatus) => Fixed::y(2, lay!( - //Self::help(), - //Fill::xy(Align::se(Tui::fg_bg(TuiTheme::orange(), TuiTheme::g(25), self.stats()))), -//))); -//impl GrooveboxStatus { - //fn help () -> impl Content { - //let single = |binding, command|row!(" ", col!( - //Tui::fg(TuiTheme::yellow(), binding), - //command - //)); - //let double = |(b1, c1), (b2, c2)|col!( - //row!(" ", Tui::fg(TuiTheme::yellow(), b1), " ", c1,), - //row!(" ", Tui::fg(TuiTheme::yellow(), b2), " ", c2,), - //); - //Tui::fg_bg(TuiTheme::g(255), TuiTheme::g(50), row!( - //single("SPACE", "play/pause"), - //double(("▲▼▶◀", "cursor"), ("Ctrl", "scroll"), ), - //double(("a", "append"), ("s", "set note"),), - //double((",.", "length"), ("<>", "triplet"), ), - //double(("[]", "phrase"), ("{}", "order"), ), - //double(("q", "enqueue"), ("e", "edit"), ), - //double(("c", "color"), ("", ""),), - //)) - //} - //fn stats (&self) -> impl Content + use<'_> { - //row!(&self.cpu, &self.size) - //} -//} -//macro_rules! edn_context { - //($Struct:ident |$l:lifetime, $state:ident| { - //$($key:literal = $field:ident: $Type:ty => $expr:expr,)* - //}) => { - - //#[derive(Default)] - //pub struct EdnView<$l> { $($field: Option<$Type>),* } - - //impl<$l> EdnView<$l> { - //pub fn parse <'e> (edn: &[Edn<'e>]) -> impl Fn(&$Struct) + use<'e> { - //let imports = Self::imports_all(edn); - //move |state| { - //let mut context = EdnView::default(); - //for import in imports.iter() { - //context.import(state, import) - //} - //} - //} - //fn imports_all <'e> (edn: &[Edn<'e>]) -> Vec<&'e str> { - //let mut imports = vec![]; - //for edn in edn.iter() { - //for import in Self::imports_one(edn) { - //imports.push(import); - //} - //} - //imports - //} - //fn imports_one <'e> (edn: &Edn<'e>) -> Vec<&'e str> { - //match edn { - //Edn::Symbol(import) => vec![import], - //Edn::List(edn) => Self::imports_all(edn.as_slice()), - //_ => vec![], - //} - //} - //pub fn import (&mut self, $state: &$l$Struct, key: &str) { - //match key { - //$($key => self.$field = Some($expr),)* - //_ => {} - //} - //} - //} - //} -//} - -////impl Groovebox { - ////fn status (&self) -> impl Content + use<'_> { - ////let note_pt = self.editor.note_point(); - ////Align::w(Fixed::y(1, )) - ////} - ////fn pool (&self) -> impl Content + use<'_> { - ////let w = self.size.w(); - ////let pool_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; - ////Fixed::x(if self.compact { 5 } else { pool_w }, - ////) - ////} - ////fn sampler (&self) -> impl Content + use<'_> { - ////let sampler_w = if self.compact { 4 } else { 11 }; - ////let sampler_y = if self.compact { 1 } else { 0 }; - ////Fixed::x(sampler_w, Push::y(sampler_y, Fill::y( - ////SampleList::new(self.compact, &self.sampler, &self.editor)))) - ////} -////} -/////////////////////////////////////////////////////////////////////////////////////////////////// -//render!(TuiOut: (self: Arranger) => self.size.of(EdnView::from_source(self, Self::EDN))); -//impl EdnViewData for &Arranger { - //fn get_content <'a> (&'a self, item: EdnItem<&'a str>) -> RenderBox<'a, TuiOut> { - //use EdnItem::*; - //let tracks_w = self.tracks_with_sizes().last().unwrap().3 as u16; - //match item { - //Nil => Box::new(()), - //Exp(items) => Box::new(EdnView::from_items(*self, items.as_slice())), - //Sym(":editor") => (&self.editor).boxed(), - //Sym(":pool") => self.pool().boxed(), - //Sym(":status") => self.status().boxed(), - //Sym(":toolbar") => self.toolbar().boxed(), - //Sym(":tracks") => self.track_row(tracks_w).boxed(), - //Sym(":scenes") => self.scene_row(tracks_w).boxed(), - //Sym(":inputs") => self.input_row(tracks_w).boxed(), - //Sym(":outputs") => self.output_row(tracks_w).boxed(), - //_ => panic!("no content for {item:?}") - //} - //} -//} -//impl Arranger { - //const EDN: &'static str = include_str!("../edn/arranger.edn"); - //pub const LEFT_SEP: char = '▎'; - - //fn toolbar (&self) -> impl Content + use<'_> { - //Fill::x(Fixed::y(2, Align::x(ClockView::new(true, &self.clock)))) - //} - //fn pool (&self) -> impl Content + use<'_> { - //Align::e(Fixed::x(self.sidebar_w(), PoolView(self.compact, &self.pool))) - //} - //fn status (&self) -> impl Content + use<'_> { - //Bsp::e(self.editor.clip_status(), self.editor.edit_status()) - //} - //fn is_editing (&self) -> bool { - // !self.pool.visible - //} - //fn sidebar_w (&self) -> u16 { - //let w = self.size.w(); - //let w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; - //let w = if self.pool.visible { w } else { 8 }; - //w - //} - //fn editor_w (&self) -> usize { - ////self.editor.note_len() / self.editor.note_zoom().get() - //(5 + (self.editor.time_len().get() / self.editor.time_zoom().get())) - //.min(self.size.w().saturating_sub(20)) - //.max(16) - ////self.editor.time_axis().get().max(16) - ////50 - //} - //pub fn scenes_with_sizes (&self, h: usize) - //-> impl Iterator - //{ - //let mut y = 0; - //let editing = self.is_editing(); - //let (selected_track, selected_scene) = match self.selected { - //ArrangerSelection::Clip(t, s) => (Some(t), Some(s)), - //_ => (None, None) - //}; - //self.scenes.iter().enumerate().map(move|(s, scene)|{ - //let active = editing && selected_track.is_some() && selected_scene == Some(s); - //let height = if active { 15 } else { h }; - //let data = (s, scene, y, y + height); - //y += height; - //data - //}) - //} - //pub fn tracks_with_sizes (&self) - //-> impl Iterator - //{ - //tracks_with_sizes(self.tracks.iter(), match self.selected { - //ArrangerSelection::Track(t) if self.is_editing() => Some(t), - //ArrangerSelection::Clip(t, _) if self.is_editing() => Some(t), - //_ => None - //}, self.editor_w()) - //} - - //fn play_row (&self, tracks_w: u16) -> impl Content + '_ { - //let h = 2; - //Fixed::y(h, Bsp::e( - //Fixed::xy(self.sidebar_w() as u16, h, self.play_header()), - //Fill::x(Align::c(Fixed::xy(tracks_w, h, self.play_cells()))) - //)) - //} - //fn play_header (&self) -> BoxThunk { - //(||Tui::bold(true, Tui::fg(TuiTheme::g(128), "Playing")).boxed()).into() - //} - //fn play_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { - //(move||Align::x(Map::new(||self.tracks_with_sizes(), move|(_, track, x1, x2), i| { - ////let color = track.color; - //let color: ItemPalette = track.color.dark.into(); - //let timebase = self.clock().timebase(); - //let value = Tui::fg_bg(color.lightest.rgb, color.base.rgb, - //if let Some((_, Some(clip))) = track.player.play_clip().as_ref() { - //let length = clip.read().unwrap().length; - //let elapsed = track.player.pulses_since_start().unwrap() as usize; - //format!("+{:>}", timebase.format_beats_1_short((elapsed % length) as f64)) - //} else { - //String::new() - //}); - //let cell = Bsp::s(value, phat_hi(color.dark.rgb, color.darker.rgb)); - //Tui::bg(color.base.rgb, map_east(x1 as u16, (x2 - x1) as u16, cell)) - //})).boxed()).into() - //} - - //fn next_row (&self, tracks_w: u16) -> impl Content + '_ { - //let h = 2; - //Fixed::y(h, Bsp::e( - //Fixed::xy(self.sidebar_w() as u16, h, self.next_header()), - //Fill::x(Align::c(Fixed::xy(tracks_w, h, self.next_cells()))) - //)) - //} - //fn next_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { - //(||Tui::bold(true, Tui::fg(TuiTheme::g(128), "Next")).boxed()).into() - //} - //fn next_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { - //(move||Align::x(Map::new(||self.tracks_with_sizes(), move|(_, track, x1, x2), i| { - //let color: ItemPalette = track.color; - //let color: ItemPalette = track.color.dark.into(); - //let current = &self.clock().playhead; - //let timebase = ¤t.timebase; - //let cell = Self::cell(color, Tui::bold(true, { - //let mut result = String::new(); - //if let Some((t, _)) = track.player.next_clip().as_ref() { - //let target = t.pulse.get(); - //let current = current.pulse.get(); - //if target > current { - //result = format!("-{:>}", timebase.format_beats_0_short(target - current)) - //} - //} - //result - //})); - //let cell = Tui::fg_bg(color.lightest.rgb, color.base.rgb, cell); - //let cell = Bsp::s(cell, phat_hi(color.dark.rgb, color.darker.rgb)); - //Tui::bg(color.base.rgb, map_east(x1 as u16, (x2 - x1) as u16, cell)) - //})).boxed()).into() - //} - ///// beats until switchover - //fn cell_until_next (track: &ArrangerTrack, current: &Arc) - //-> Option> - //{ - //let timebase = ¤t.timebase; - //let mut result = String::new(); - //if let Some((t, _)) = track.player.next_clip().as_ref() { - //let target = t.pulse.get(); - //let current = current.pulse.get(); - //if target > current { - //result = format!("-{:>}", timebase.format_beats_0_short(target - current)) - //} - //} - //Some(result) - //} - - //fn track_row (&self, tracks_w: u16) -> impl Content + '_ { - //let h = 3; - //let border = |x|x;//Rugged(Style::default().fg(Color::Rgb(0,0,0)).bg(Color::Reset)).enclose2(x); - //Fixed::y(h, Bsp::e( - //Fixed::xy(self.sidebar_w() as u16, h, self.track_header()), - //Fill::x(Align::c(Fixed::xy(tracks_w, h, border(self.track_cells())))) - //)) - //} - //fn track_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { - //(||Tui::bold(true, Bsp::s( - //row!( - //Tui::fg(TuiTheme::g(128), "add "), - //Tui::fg(TuiTheme::orange(), "t"), - //Tui::fg(TuiTheme::g(128), "rack"), - //), - //row!( - //Tui::fg(TuiTheme::orange(), "a"), - //Tui::fg(TuiTheme::g(128), "dd scene"), - //), - //).boxed())).into() - //} - //fn track_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { - //let iter = ||self.tracks_with_sizes(); - //(move||Align::x(Map::new(iter, move|(_, track, x1, x2), i| { - //let name = Push::x(1, &track.name); - //let color = track.color; - //let fg = color.lightest.rgb; - //let bg = color.base.rgb; - //let active = self.selected.track() == Some(i); - //let bfg = if active { Color::Rgb(255,255,255) } else { Color::Rgb(0,0,0) }; - //let border = Style::default().fg(bfg).bg(bg); - //Tui::bg(bg, map_east(x1 as u16, (x2 - x1) as u16, - //Outer(border).enclose(Tui::fg_bg(fg, bg, Tui::bold(true, Fill::x(Align::x(name))))) - //)) - //})).boxed()).into() - //} - - //fn input_row (&self, tracks_w: u16) -> impl Content + '_ { - //let h = 2 + self.midi_ins[0].connect.len() as u16; - //Fixed::y(h, Bsp::e( - //Fixed::xy(self.sidebar_w() as u16, h, self.input_header()), - //Fill::x(Align::c(Fixed::xy(tracks_w, h, self.input_cells()))) - //)) - //} - //fn input_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { - //(||Bsp::s( - //Tui::bold(true, row!( - //Tui::fg(TuiTheme::g(128), "midi "), - //Tui::fg(TuiTheme::orange(), "I"), - //Tui::fg(TuiTheme::g(128), "ns"), - //)), - //Bsp::s( - //Fill::x(Tui::bold(true, Tui::fg_bg(TuiTheme::g(224), TuiTheme::g(64), - //Align::w(&self.midi_ins[0].name)))), - //self.midi_ins.get(0) - //.and_then(|midi_in|midi_in.connect.get(0)) - //.map(|connect|Fill::x(Align::w( - //Tui::bold(false, Tui::fg_bg(TuiTheme::g(224), TuiTheme::g(64), connect.info()))))) - //) - //).boxed()).into() - //} - //fn input_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { - //(move||Align::x(Map::new(||self.tracks_with_sizes(), move|(_, track, x1, x2), i| { - //let w = (x2 - x1) as u16; - //let color: ItemPalette = track.color.dark.into(); - //map_east(x1 as u16, w, Fixed::x(w, Self::cell(color, Bsp::n( - //Self::rec_mon(color.base.rgb, false, false), - //phat_hi(color.base.rgb, color.dark.rgb) - //)))) - //})).boxed()).into() - //} - //fn rec_mon (bg: Color, rec: bool, mon: bool) -> impl Content { - //row!( - //Tui::fg_bg(if rec { Color::Red } else { bg }, bg, "▐"), - //Tui::fg_bg(if rec { Color::White } else { Color::Rgb(0,0,0) }, bg, "REC"), - //Tui::fg_bg(if rec { Color::White } else { bg }, bg, "▐"), - //Tui::fg_bg(if mon { Color::White } else { Color::Rgb(0,0,0) }, bg, "MON"), - //Tui::fg_bg(if mon { Color::White } else { bg }, bg, "▌"), - //) - //} - - //fn output_row (&self, tracks_w: u16) -> impl Content + '_ { - //let h = 2 + self.midi_outs[0].connect.len() as u16; - //Fixed::y(h, Bsp::e( - //Fixed::xy(self.sidebar_w() as u16, h, self.output_header()), - //Fill::x(Align::c(Fixed::xy(tracks_w, h, self.output_cells()))) - //)) - //} - //fn output_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { - //(||Bsp::s( - //Tui::bold(true, row!( - //Tui::fg(TuiTheme::g(128), "midi "), - //Tui::fg(TuiTheme::orange(), "O"), - //Tui::fg(TuiTheme::g(128), "uts"), - //)), - //Bsp::s( - //Fill::x(Tui::bold(true, Tui::fg_bg(TuiTheme::g(224), TuiTheme::g(64), - //Align::w(&self.midi_outs[0].name)))), - //self.midi_outs.get(0) - //.and_then(|midi_out|midi_out.connect.get(0)) - //.map(|connect|Fill::x(Align::w( - //Tui::bold(false, Tui::fg_bg(TuiTheme::g(224), TuiTheme::g(64), connect.info()))))) - //), - //).boxed()).into() - //} - //fn output_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { - //(move||Align::x(Map::new(||self.tracks_with_sizes(), move|(_, track, x1, x2), i| { - //let w = (x2 - x1) as u16; - //let color: ItemPalette = track.color.dark.into(); - //map_east(x1 as u16, w, Fixed::x(w, Self::cell(color, Bsp::n( - //Self::mute_solo(color.base.rgb, false, false), - //phat_hi(color.dark.rgb, color.darker.rgb) - //)))) - //})).boxed()).into() - //} - //fn mute_solo (bg: Color, mute: bool, solo: bool) -> impl Content { - //row!( - //Tui::fg_bg(if mute { Color::White } else { Color::Rgb(0,0,0) }, bg, "MUTE"), - //Tui::fg_bg(if mute { Color::White } else { bg }, bg, "▐"), - //Tui::fg_bg(if solo { Color::White } else { Color::Rgb(0,0,0) }, bg, "SOLO"), - //) - //} - - //fn scene_row (&self, tracks_w: u16) -> impl Content + '_ { - //let h = (self.size.h() as u16).saturating_sub(8).max(8); - //let border = |x|x;//Skinny(Style::default().fg(Color::Rgb(0,0,0)).bg(Color::Reset)).enclose2(x); - //Bsp::e( - //Tui::bg(Color::Reset, Fixed::xy(self.sidebar_w() as u16, h, self.scene_headers())), - //Tui::bg(Color::Reset, Fill::x(Align::c(Fixed::xy(tracks_w, h, border(self.scene_cells()))))) - //) - //} - //fn scene_headers <'a> (&'a self) -> BoxThunk<'a, TuiOut> { - //(||{ - //let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0)))); - //let selected = self.selected.scene(); - //Fill::y(Align::c(Map::new(||self.scenes_with_sizes(2), move|(_, scene, y1, y2), i| { - //let h = (y2 - y1) as u16; - //let name = format!("🭬{}", &scene.name); - //let color = scene.color; - //let active = selected == Some(i); - //let mid = if active { color.light } else { color.base }; - //let top = Some(last_color.read().unwrap().base.rgb); - //let cell = phat_sel_3( - //active, - //Tui::bold(true, name.clone()), - //Tui::bold(true, name), - //top, - //mid.rgb, - //Color::Rgb(0, 0, 0) - //); - //*last_color.write().unwrap() = color; - //map_south(y1 as u16, h + 1, Fixed::y(h + 1, cell)) - //}))).boxed() - //}).into() - //} - //fn scene_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { - //let editing = self.is_editing(); - //let tracks = move||self.tracks_with_sizes(); - //let scenes = ||self.scenes_with_sizes(2); - //let selected_track = self.selected.track(); - //let selected_scene = self.selected.scene(); - //(move||Fill::y(Align::c(Map::new(tracks, move|(_, track, x1, x2), t| { - //let w = (x2 - x1) as u16; - //let color: ItemPalette = track.color.dark.into(); - //let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0)))); - //let cells = Map::new(scenes, move|(_, scene, y1, y2), s| { - //let h = (y2 - y1) as u16; - //let color = scene.color; - //let (name, fg, bg) = if let Some(c) = &scene.clips[t] { - //let c = c.read().unwrap(); - //(c.name.to_string(), c.color.lightest.rgb, c.color.base.rgb) - //} else { - //("⏹ ".to_string(), TuiTheme::g(64), TuiTheme::g(32)) - //}; - //let last = last_color.read().unwrap().clone(); - //let active = editing && selected_scene == Some(s) && selected_track == Some(t); - //let editor = Thunk::new(||&self.editor); - //let cell = Thunk::new(move||phat_sel_3( - //selected_track == Some(t) && selected_scene == Some(s), - //Tui::fg(fg, Push::x(1, Tui::bold(true, name.to_string()))), - //Tui::fg(fg, Push::x(1, Tui::bold(true, name.to_string()))), - //if selected_track == Some(t) && selected_scene.map(|s|s+1) == Some(s) { - //None - //} else { - //Some(bg.into()) - //}, - //bg.into(), - //bg.into(), - //)); - //let cell = Either(active, editor, cell); - //*last_color.write().unwrap() = bg.into(); - //map_south( - //y1 as u16, - //h + 1, - //Fill::x(Fixed::y(h + 1, cell)) - //) - //}); - //let column = Fixed::x(w, Tui::bg(Color::Reset, Align::y(cells)).boxed()); - //Fixed::x(w, map_east(x1 as u16, w, column)) - //}))).boxed()).into() - //} - //fn cell_clip <'a> ( - //scene: &'a ArrangerScene, index: usize, track: &'a ArrangerTrack, w: u16, h: u16 - //) -> impl Content + use<'a> { - //scene.clips.get(index).map(|clip|clip.as_ref().map(|clip|{ - //let clip = clip.read().unwrap(); - //let mut bg = TuiTheme::border_bg(); - //let name = clip.name.to_string(); - //let max_w = name.len().min((w as usize).saturating_sub(2)); - //let color = clip.color; - //bg = color.dark.rgb; - //if let Some((_, Some(ref playing))) = track.player.play_clip() { - //if *playing.read().unwrap() == *clip { - //bg = color.light.rgb - //} - //}; - //Fixed::xy(w, h, &Tui::bg(bg, Push::x(1, Fixed::x(w, &name.as_str()[0..max_w])))); - //})) - //} - - //fn track_column_separators <'a> (&'a self) -> impl Content + 'a { - //let scenes_w = 16;//.max(SCENES_W_OFFSET + ArrangerScene::longest_name(&self.scenes) as u16); - //let fg = Color::Rgb(64,64,64); - //Map::new(move||self.tracks_with_sizes(), move|(_n, _track, x1, x2), _i|{ - //Push::x(scenes_w, map_east(x1 as u16, (x2 - x1) as u16, - //Fixed::x((x2 - x1) as u16, Tui::fg(fg, RepeatV(&"·"))))) - //}) - //} - - //pub fn track_widths (tracks: &[ArrangerTrack]) -> Vec<(usize, usize)> { - //let mut widths = vec![]; - //let mut total = 0; - //for track in tracks.iter() { - //let width = track.width; - //widths.push((width, total)); - //total += width; - //} - //widths.push((0, total)); - //widths - //} - - //fn scene_row_sep <'a> (&'a self) -> impl Content + 'a { - //let fg = Color::Rgb(255,255,255); - //Map::new(move||self.scenes_with_sizes(1), |_, _|"") - ////Map(||rows.iter(), |(_n, _scene, y1, _y2), _i| { - ////let y = to.area().y() + (y / PPQ) as u16 + 1; - ////if y >= to.buffer.area.height { break } - ////for x in to.area().x()..to.area().x2().saturating_sub(2) { - //////if x < to.buffer.area.x && y < to.buffer.area.y { - ////if let Some(cell) = to.buffer.cell_mut(ratatui::prelude::Position::from((x, y))) { - ////cell.modifier = Modifier::UNDERLINED; - ////cell.underline_color = fg; - ////} - //////} - ////} - ////}) - //} - - //pub fn scene_heights (scenes: &[ArrangerScene], factor: usize) -> Vec<(usize, usize)> { - //let mut total = 0; - //if factor == 0 { - //scenes.iter().map(|scene|{ - //let pulses = scene.pulses().max(PPQ); - //total += pulses; - //(pulses, total - pulses) - //}).collect() - //} else { - //(0..=scenes.len()).map(|i|{ - //(factor*PPQ, factor*PPQ*i) - //}).collect() - //} - //} - //fn cursor (&self) -> impl Content + '_ { - //let color = self.color; - //let bg = color.lighter.rgb;//Color::Rgb(0, 255, 0); - //let selected = self.selected(); - //let cols = Arranger::track_widths(&self.tracks); - //let rows = Arranger::scene_heights(&self.scenes, 1); - //let scenes_w = 16.max(SCENES_W_OFFSET + ArrangerScene::longest_name(&self.scenes) as u16); - //let focused = true; - //let reticle = Reticle(Style { - //fg: Some(self.color.lighter.rgb), - //bg: None, - //underline_color: None, - //add_modifier: Modifier::empty(), - //sub_modifier: Modifier::DIM - //}); - //RenderThunk::new(move|to: &mut TuiOut|{ - //let area = to.area(); - //let [x, y, w, h] = area.xywh(); - //let mut track_area: Option<[u16;4]> = match selected { - //ArrangerSelection::Track(t) | ArrangerSelection::Clip(t, _) => Some([ - //x + scenes_w + cols[t].1 as u16, y, - //cols[t].0 as u16, h, - //]), - //_ => None - //}; - //let mut scene_area: Option<[u16;4]> = match selected { - //ArrangerSelection::Scene(s) | ArrangerSelection::Clip(_, s) => Some([ - //x, y + HEADER_H + (rows[s].1 / PPQ) as u16, - //w, (rows[s].0 / PPQ) as u16 - //]), - //_ => None - //}; - //let mut clip_area: Option<[u16;4]> = match selected { - //ArrangerSelection::Clip(t, s) => Some([ - //(scenes_w + x + cols[t].1 as u16).saturating_sub(1), - //HEADER_H + y + (rows[s].1/PPQ) as u16, - //cols[t].0 as u16 + 2, - //(rows[s].0 / PPQ) as u16 - //]), - //_ => None - //}; - //if let Some([x, y, width, height]) = track_area { - //to.fill_fg([x, y, 1, height], bg); - //to.fill_fg([x + width, y, 1, height], bg); - //} - //if let Some([_, y, _, height]) = scene_area { - //to.fill_ul([x, y - 1, w, 1], bg); - //to.fill_ul([x, y + height - 1, w, 1], bg); - //} - //if focused { - //to.place(if let Some(clip_area) = clip_area { - //clip_area - //} else if let Some(track_area) = track_area { - //track_area.clip_h(HEADER_H) - //} else if let Some(scene_area) = scene_area { - //scene_area.clip_w(scenes_w) - //} else { - //area.clip_w(scenes_w).clip_h(HEADER_H) - //}, &reticle) - //}; - //}) - //} - - ///// A 1-row cell. - //fn cell > (color: ItemPalette, field: T) -> impl Content { - //Tui::fg_bg(color.lightest.rgb, color.base.rgb, Fixed::y(1, field)) - //} -//} - -/////////////////////////////////////////////////////////////////////////////////////////////////// -//handle!(TuiIn: |self: Sequencer, input|SequencerCommand::execute_with_state(self, input.event())); -//handle!(TuiIn: |self: Groovebox, input|GrooveboxCommand::execute_with_state(self, input.event())); -//handle!(TuiIn: |self: Arranger, input|ArrangerCommand::execute_with_state(self, input.event())); -//use SequencerCommand as SeqCmd; -//use GrooveboxCommand as GrvCmd; -//use ArrangerCommand as ArrCmd; -//#[derive(Clone, Debug)] pub enum SequencerCommand { - //Compact(bool), - //History(isize), - //Clock(ClockCommand), - //Pool(PoolCommand), - //Editor(MidiEditCommand), - //Enqueue(Option>>), -//} -//#[derive(Clone, Debug)] pub enum GrooveboxCommand { - //Compact(bool), - //History(isize), - //Clock(ClockCommand), - //Pool(PoolCommand), - //Editor(MidiEditCommand), - //Enqueue(Option>>), - //Sampler(SamplerCommand), -//} -//#[derive(Clone, Debug)] pub enum ArrangerCommand { - //History(isize), - //Color(ItemPalette), - //Clock(ClockCommand), - //Scene(SceneCommand), - //Track(TrackCommand), - //Clip(ClipCommand), - //Select(ArrangerSelection), - //Zoom(usize), - //Pool(PoolCommand), - //Editor(MidiEditCommand), - //StopAll, - //Clear, -//} - -//command!(|self: SequencerCommand, state: Sequencer|match self { - //Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?, - //Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?, - //Self::Enqueue(clip) => { state.player.enqueue_next(clip.as_ref()); None }, - //Self::History(delta) => { todo!("undo/redo") }, - - //Self::Pool(cmd) => match cmd { - //// autoselect: automatically load selected clip in editor - //PoolCommand::Select(_) => { - //let undo = cmd.delegate(&mut state.pool, Self::Pool)?; - //state.editor.set_clip(state.pool.clip().as_ref()); - //undo - //}, - //// update color in all places simultaneously - //PoolCommand::Clip(PoolCmd::SetColor(index, _)) => { - //let undo = cmd.delegate(&mut state.pool, Self::Pool)?; - //state.editor.set_clip(state.pool.clip().as_ref()); - //undo - //}, - //_ => cmd.delegate(&mut state.pool, Self::Pool)? - //}, - //Self::Compact(compact) => if state.compact != compact { - //state.compact = compact; - //Some(Self::Compact(!compact)) - //} else { - //None - //}, -//}); -//command!(|self: GrooveboxCommand, state: Groovebox|match self { - //Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?, - //Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?, - //Self::Enqueue(clip) => { state.player.enqueue_next(clip.as_ref()); None }, - //Self::History(delta) => { todo!("undo/redo") }, - //Self::Sampler(cmd) => cmd.delegate(&mut state.sampler, Self::Sampler)?, - - //Self::Pool(cmd) => match cmd { - //// autoselect: automatically load selected clip in editor - //PoolCommand::Select(_) => { - //let undo = cmd.delegate(&mut state.pool, Self::Pool)?; - //state.editor.set_clip(state.pool.clip().as_ref()); - //undo - //}, - //// update color in all places simultaneously - //PoolCommand::Clip(PoolCmd::SetColor(index, _)) => { - //let undo = cmd.delegate(&mut state.pool, Self::Pool)?; - //state.editor.set_clip(state.pool.clip().as_ref()); - //undo - //}, - //_ => cmd.delegate(&mut state.pool, Self::Pool)? - //}, - //Self::Compact(compact) => if state.compact != compact { - //state.compact = compact; - //Some(Self::Compact(!compact)) - //} else { - //None - //}, -//}); -//command!(|self: ArrangerCommand, state: Arranger|match self { - //Self::Clear => { todo!() }, - //Self::Clip(cmd) => cmd.delegate(state, Self::Clip)?, - //Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?, - //Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?, - //Self::History(_) => { todo!() }, - //Self::Scene(cmd) => cmd.delegate(state, Self::Scene)?, - //Self::Select(s) => { state.selected = s; None }, - //Self::Track(cmd) => cmd.delegate(state, Self::Track)?, - //Self::Zoom(_) => { todo!(); }, - - //Self::StopAll => { - //for track in 0..state.tracks.len() { state.tracks[track].player.enqueue_next(None); } - //None - //}, - //Self::Color(palette) => { - //let old = state.color; - //state.color = palette; - //Some(Self::Color(old)) - //}, - //Self::Pool(cmd) => { - //match cmd { - //// autoselect: automatically load selected clip in editor - //PoolCommand::Select(_) => { - //let undo = cmd.delegate(&mut state.pool, Self::Pool)?; - //state.editor.set_clip(state.pool.clip().as_ref()); - //undo - //}, - //// reload clip in editor to update color - //PoolCommand::Clip(PoolClipCommand::SetColor(index, _)) => { - //let undo = cmd.delegate(&mut state.pool, Self::Pool)?; - //state.editor.set_clip(state.pool.clip().as_ref()); - //undo - //}, - //_ => cmd.delegate(&mut state.pool, Self::Pool)? - //} - //}, -//}); -//command!(|self: SceneCommand, state: Arranger|match self { - //Self::Add => { state.scene_add(None, None)?; None } - //Self::Del(index) => { state.scene_del(index); None }, - //Self::SetColor(index, color) => { - //let old = state.scenes[index].color; - //state.scenes[index].color = color; - //Some(Self::SetColor(index, old)) - //}, - //Self::Enqueue(scene) => { - //for track in 0..state.tracks.len() { - //state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref()); - //} - //None - //}, - //_ => None -//}); -//command!(|self: TrackCommand, state: Arranger|match self { - //Self::Add => { state.track_add(None, None)?; None }, - //Self::Del(index) => { state.track_del(index); None }, - //Self::Stop(track) => { state.tracks[track].player.enqueue_next(None); None }, - //Self::SetColor(index, color) => { - //let old = state.tracks[index].color; - //state.tracks[index].color = color; - //Some(Self::SetColor(index, old)) - //}, - //_ => None -//}); -//command!(|self: ClipCommand, state: Arranger|match self { - //Self::Get(track, scene) => { todo!() }, - //Self::Put(track, scene, clip) => { - //let old = state.scenes[scene].clips[track].clone(); - //state.scenes[scene].clips[track] = clip; - //Some(Self::Put(track, scene, old)) - //}, - //Self::Enqueue(track, scene) => { - //state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref()); - //None - //}, - //_ => None -//}); -//keymap!(KEYS_SEQUENCER = |state: Sequencer, input: Event| SequencerCommand { - //// TODO: k: toggle on-screen keyboard - //ctrl(key(Char('k'))) => { todo!("keyboard") }, - //// Transport: Play/pause - //key(Char(' ')) => SeqCmd::Clock(if state.clock().is_stopped() { Play(None) } else { Pause(None) }), - //// Transport: Play from start or rewind to start - //shift(key(Char(' '))) => SeqCmd::Clock(if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }), - //// u: undo - //key(Char('u')) => SeqCmd::History(-1), - //// Shift-U: redo - //key(Char('U')) => SeqCmd::History( 1), - //// Tab: Toggle compact mode - //key(Tab) => SeqCmd::Compact(!state.compact), - //// q: Enqueue currently edited clip - //key(Char('q')) => SeqCmd::Enqueue(state.pool.clip().clone()), - //// 0: Enqueue clip 0 (stop all) - //key(Char('0')) => SeqCmd::Enqueue(Some(state.clips()[0].clone())), - //// e: Toggle between editing currently playing or other clip - ////key(Char('e')) => if let Some((_, Some(playing))) = state.player.play_clip() { - ////let editing = state.editor.clip().as_ref().map(|p|p.read().unwrap().clone()); - ////let selected = state.pool.clip().clone(); - ////SeqCmd::Editor(Show(Some(if Some(selected.read().unwrap().clone()) != editing { - ////selected - ////} else { - ////playing.clone() - ////}))) - ////} else { - ////return None - ////} -//}, if let Some(command) = MidiEditCommand::input_to_command(&state.editor, input) { - //SeqCmd::Editor(command) -//} else if let Some(command) = PoolCommand::input_to_command(&state.pool, input) { - //SeqCmd::Pool(command) -//} else { - //return None -//}); -//keymap!(<'a> KEYS_GROOVEBOX = |state: Groovebox, input: Event| GrooveboxCommand { - //// Tab: Toggle compact mode - //key(Tab) => GrvCmd::Compact(!state.compact), - //// q: Enqueue currently edited clip - //key(Char('q')) => GrvCmd::Enqueue(state.pool.clip().clone()), - //// 0: Enqueue clip 0 (stop all) - //key(Char('0')) => GrvCmd::Enqueue(Some(state.pool.clips()[0].clone())), - //// TODO: k: toggle on-screen keyboard - //ctrl(key(Char('k'))) => todo!("keyboard"), - //// Transport: Play from start or rewind to start - //ctrl(key(Char(' '))) => GrvCmd::Clock( - //if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) } - //), - //// Shift-R: toggle recording - //shift(key(Char('R'))) => GrvCmd::Sampler(if state.sampler.recording.is_some() { - //SmplCmd::RecordFinish - //} else { - //SmplCmd::RecordBegin(u7::from(state.editor.note_point() as u8)) - //}), - //// Shift-Del: delete sample - //shift(key(Delete)) => GrvCmd::Sampler( - //SmplCmd::SetSample(u7::from(state.editor.note_point() as u8), None) - //), - //// e: Toggle between editing currently playing or other clip - ////shift(key(Char('e'))) => if let Some((_, Some(playing))) = state.player.play_clip() { - ////let editing = state.editor.clip().as_ref().map(|p|p.read().unwrap().clone()); - ////let selected = state.pool.clip().clone().map(|s|s.read().unwrap().clone()); - ////GrvCmd::Editor(Show(if selected != editing { - ////selected - ////} else { - ////Some(playing.clone()) - ////})) - ////} else { - ////return None - ////}, -//}, if let Some(command) = MidiEditCommand::input_to_command(&state.editor, input) { - //GrvCmd::Editor(command) -//} else if let Some(command) = PoolCommand::input_to_command(&state.pool, input) { - //GrvCmd::Pool(command) -//} else { - //return None -//}); -//keymap!(KEYS_ARRANGER = |state: Arranger, input: Event| ArrangerCommand { - //key(Char('u')) => ArrCmd::History(-1), - //key(Char('U')) => ArrCmd::History(1), - //// TODO: k: toggle on-screen keyboard - //ctrl(key(Char('k'))) => { todo!("keyboard") }, - //// Transport: Play/pause - //key(Char(' ')) => ArrCmd::Clock(if state.clock().is_stopped() { Play(None) } else { Pause(None) }), - //// Transport: Play from start or rewind to start - //shift(key(Char(' '))) => ArrCmd::Clock(if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }), - //key(Char('e')) => ArrCmd::Editor(MidiEditCommand::Show(state.pool.clip().clone())), - //ctrl(key(Char('a'))) => ArrCmd::Scene(SceneCommand::Add), - //ctrl(key(Char('A'))) => return None,//ArrCmd::Scene(SceneCommand::Add), - //ctrl(key(Char('t'))) => ArrCmd::Track(TrackCommand::Add), - //// Tab: Toggle visibility of clip pool column - //key(Tab) => ArrCmd::Pool(PoolCommand::Show(!state.pool.visible)), -//}, { - //use ArrangerSelection as Selected; - //use SceneCommand as Scene; - //use TrackCommand as Track; - //use ClipCommand as Clip; - //let t_len = state.tracks.len(); - //let s_len = state.scenes.len(); - //match state.selected { - //Selected::Clip(t, s) => clip_keymap(state, input, t, s), - //Selected::Scene(s) => scene_keymap(state, input, s), - //Selected::Track(t) => track_keymap(state, input, t), - //Selected::Mix => match input { - - //kpat!(Delete) => Some(ArrCmd::Clear), - //kpat!(Char('0')) => Some(ArrCmd::StopAll), - //kpat!(Char('c')) => Some(ArrCmd::Color(ItemPalette::random())), - - //kpat!(Up) => return None, - //kpat!(Down) => Some(ArrCmd::Select(Selected::Scene(0))), - //kpat!(Left) => return None, - //kpat!(Right) => Some(ArrCmd::Select(Selected::Track(0))), - - //_ => None - //}, - //} -//}.or_else(||if let Some(command) = MidiEditCommand::input_to_command(&state.editor, input) { - //Some(ArrCmd::Editor(command)) -//} else if let Some(command) = PoolCommand::input_to_command(&state.pool, input) { - //Some(ArrCmd::Pool(command)) -//} else { - //None -//})?); - -//fn clip_keymap (state: &Arranger, input: &Event, t: usize, s: usize) -> Option { - //use ArrangerSelection as Selected; - //use SceneCommand as Scene; - //use TrackCommand as Track; - //use ClipCommand as Clip; - //let t_len = state.tracks.len(); - //let s_len = state.scenes.len(); - //Some(match input { - - //kpat!(Char('g')) => ArrCmd::Pool(PoolCommand::Select(0)), - //kpat!(Char('q')) => ArrCmd::Clip(Clip::Enqueue(t, s)), - //kpat!(Char('l')) => ArrCmd::Clip(Clip::SetLoop(t, s, false)), - - //kpat!(Enter) => if state.scenes[s].clips[t].is_none() { - //// FIXME: get this clip from the pool (autoregister via intmut) - //let (_, clip) = state.add_clip(); - //ArrCmd::Clip(Clip::Put(t, s, Some(clip))) - //} else { - //return None - //}, - //kpat!(Delete) => ArrCmd::Clip(Clip::Put(t, s, None)), - //kpat!(Char('p')) => ArrCmd::Clip(Clip::Put(t, s, state.pool.clip().clone())), - //kpat!(Char(',')) => ArrCmd::Clip(Clip::Put(t, s, None)), - //kpat!(Char('.')) => ArrCmd::Clip(Clip::Put(t, s, None)), - //kpat!(Char('<')) => ArrCmd::Clip(Clip::Put(t, s, None)), - //kpat!(Char('>')) => ArrCmd::Clip(Clip::Put(t, s, None)), - - //kpat!(Up) => ArrCmd::Select(if s > 0 { Selected::Clip(t, s - 1) } else { Selected::Track(t) }), - //kpat!(Down) => ArrCmd::Select(Selected::Clip(t, (s + 1).min(s_len.saturating_sub(1)))), - //kpat!(Left) => ArrCmd::Select(if t > 0 { Selected::Clip(t - 1, s) } else { Selected::Scene(s) }), - //kpat!(Right) => ArrCmd::Select(Selected::Clip((t + 1).min(t_len.saturating_sub(1)), s)), - - //_ => return None - //}) -//} -//fn scene_keymap (state: &Arranger, input: &Event, s: usize) -> Option { - //use ArrangerSelection as Selected; - //use SceneCommand as Scene; - //use TrackCommand as Track; - //use ClipCommand as Clip; - //let t_len = state.tracks.len(); - //let s_len = state.scenes.len(); - //Some(match input { - - //kpat!(Char(',')) => ArrCmd::Scene(Scene::Swap(s, s - 1)), - //kpat!(Char('.')) => ArrCmd::Scene(Scene::Swap(s, s + 1)), - //kpat!(Char('<')) => ArrCmd::Scene(Scene::Swap(s, s - 1)), - //kpat!(Char('>')) => ArrCmd::Scene(Scene::Swap(s, s + 1)), - //kpat!(Char('q')) => ArrCmd::Scene(Scene::Enqueue(s)), - //kpat!(Delete) => ArrCmd::Scene(Scene::Del(s)), - //kpat!(Char('c')) => ArrCmd::Scene(Scene::SetColor(s, ItemPalette::random())), - - //kpat!(Up) => ArrCmd::Select(if s > 0 { Selected::Scene(s - 1) } else { Selected::Mix }), - //kpat!(Down) => ArrCmd::Select(Selected::Scene((s + 1).min(s_len.saturating_sub(1)))), - //kpat!(Left) => return None, - //kpat!(Right) => ArrCmd::Select(Selected::Clip(0, s)), - - //_ => return None - //}) -//} -//fn track_keymap (state: &Arranger, input: &Event, t: usize) -> Option { - //use ArrangerSelection as Selected; - //use SceneCommand as Scene; - //use TrackCommand as Track; - //use ClipCommand as Clip; - //let t_len = state.tracks.len(); - //let s_len = state.scenes.len(); - //Some(match input { - - //kpat!(Char(',')) => ArrCmd::Track(Track::Swap(t, t - 1)), - //kpat!(Char('.')) => ArrCmd::Track(Track::Swap(t, t + 1)), - //kpat!(Char('<')) => ArrCmd::Track(Track::Swap(t, t - 1)), - //kpat!(Char('>')) => ArrCmd::Track(Track::Swap(t, t + 1)), - //kpat!(Delete) => ArrCmd::Track(Track::Del(t)), - //kpat!(Char('c')) => ArrCmd::Track(Track::SetColor(t, ItemPalette::random())), - - //kpat!(Up) => return None, - //kpat!(Down) => ArrCmd::Select(Selected::Clip(t, 0)), - //kpat!(Left) => ArrCmd::Select(if t > 0 { Selected::Track(t - 1) } else { Selected::Mix }), - //kpat!(Right) => ArrCmd::Select(Selected::Track((t + 1).min(t_len.saturating_sub(1)))), - - //_ => return None - //}) -//} -//impl HasJack for Arranger { - //fn jack (&self) -> &Arc> { &self.jack } -//} - -//audio!(|self: Sequencer, client, scope|{ - //// Start profiling cycle - //let t0 = self.perf.get_t0(); - - //// Update transport clock - //if Control::Quit == ClockAudio(self).process(client, scope) { - //return Control::Quit - //} - //// Update MIDI sequencer - //if Control::Quit == PlayerAudio( - //&mut self.player, &mut self.note_buf, &mut self.midi_buf - //).process(client, scope) { - //return Control::Quit - //} - - //// End profiling cycle - //self.perf.update(t0, scope); - - //Control::Continue -//}); - -//audio!(|self: Groovebox, client, scope|{ - //// Start profiling cycle - //let t0 = self.perf.get_t0(); - - //// Update transport clock - //if Control::Quit == ClockAudio(&mut self.player).process(client, scope) { - //return Control::Quit - //} - - //// Update MIDI sequencer - //if Control::Quit == PlayerAudio( - //&mut self.player, &mut self.note_buf, &mut self.midi_buf - //).process(client, scope) { - //return Control::Quit - //} - - //// Update sampler - //if Control::Quit == SamplerAudio(&mut self.sampler).process(client, scope) { - //return Control::Quit - //} - - //// TODO move these to editor and sampler: - //for RawMidi { time, bytes } in self.player.midi_ins[0].port.iter(scope) { - //if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() { - //match message { - //MidiMessage::NoteOn { ref key, .. } => { - //self.editor.set_note_point(key.as_int() as usize); - //}, - //MidiMessage::Controller { controller, value } => { - //if let Some(sample) = &self.sampler.mapped[self.editor.note_point()] { - //sample.write().unwrap().handle_cc(controller, value) - //} - //} - //_ => {} - //} - //} - //} - - //// End profiling cycle - //self.perf.update(t0, scope); - - //Control::Continue -//}); - -//audio!(|self: Arranger, client, scope|{ - //// Start profiling cycle - //let t0 = self.perf.get_t0(); - - //// Update transport clock - //if Control::Quit == ClockAudio(self).process(client, scope) { - //return Control::Quit - //} - - ////// Update MIDI sequencers - ////let tracks = &mut self.tracks; - ////let note_buf = &mut self.note_buf; - ////let midi_buf = &mut self.midi_buf; - ////if Control::Quit == TracksAudio(tracks, note_buf, midi_buf).process(client, scope) { - ////return Control::Quit - ////} - - //// FIXME: one of these per playing track - ////self.now.set(0.); - ////if let ArrangerSelection::Clip(t, s) = self.selected { - ////let clip = self.scenes.get(s).map(|scene|scene.clips.get(t)); - ////if let Some(Some(Some(clip))) = clip { - ////if let Some(track) = self.tracks().get(t) { - ////if let Some((ref started_at, Some(ref playing))) = track.player.play_clip { - ////let clip = clip.read().unwrap(); - ////if *playing.read().unwrap() == *clip { - ////let pulse = self.current().pulse.get(); - ////let start = started_at.pulse.get(); - ////let now = (pulse - start) % clip.length as f64; - ////self.now.set(now); - ////} - ////} - ////} - ////} - ////} - - //// End profiling cycle - //self.perf.update(t0, scope); - //return Control::Continue -//}); - //fn get_device_mut (&self, i: usize) -> Option>>> { - //self.devices.get(i).map(|d|d.state.write().unwrap()) - //} - //pub fn device_mut (&self) -> Option>>> { - //self.get_device_mut(self.device) - //} - ///// Add a device to the end of the chain. - //pub fn append_device (&mut self, device: JackDevice) -> Usually<&mut JackDevice> { - //self.devices.push(device); - //let index = self.devices.len() - 1; - //Ok(&mut self.devices[index]) - //} - //pub fn add_device (&mut self, device: JackDevice) { - //self.devices.push(device); - //} - //pub fn connect_first_device (&self) -> Usually<()> { - //if let (Some(port), Some(device)) = (&self.midi_out, self.devices.get(0)) { - //device.client.as_client().connect_ports(&port, &device.midi_ins()?[0])?; - //} - //Ok(()) - //} - //pub fn connect_last_device (&self, app: &Track) -> Usually<()> { - //Ok(match self.devices.get(self.devices.len().saturating_sub(1)) { - //Some(device) => { - //app.audio_out(0).map(|left|device.connect_audio_out(0, &left)).transpose()?; - //app.audio_out(1).map(|right|device.connect_audio_out(1, &right)).transpose()?; - //() - //}, - //None => () - //}) - //} -use crate::*; -impl MixerTrack { - pub fn new (name: &str) -> Usually { - Ok(Self { - name: name.to_string().into(), - audio_ins: vec![], - audio_outs: vec![], - devices: vec![], - //ports: JackPorts::default(), - //devices: vec![], - //device: 0, - }) - } -} - -pub struct TrackView<'a> { - pub chain: Option<&'a MixerTrack>, - pub direction: Direction, - pub focused: bool, - pub entered: bool, -} - -impl<'a> Content for TrackView<'a> { - fn render (&self, to: &mut TuiOut) { - todo!(); - //let mut area = to.area(); - //if let Some(chain) = self.chain { - //match self.direction { - //Direction::Down => area.width = area.width.min(40), - //Direction::Right => area.width = area.width.min(10), - //_ => { unimplemented!() }, - //} - //to.fill_bg(to.area(), Nord::bg_lo(self.focused, self.entered)); - //let mut split = Stack::new(self.direction); - //for device in chain.devices.as_slice().iter() { - //split = split.add_ref(device); - //} - //let (area, areas) = split.render_areas(to)?; - //if self.focused && self.entered && areas.len() > 0 { - //Corners(Style::default().green().not_dim()).draw(to.with_rect(areas[0]))?; - //} - //Ok(Some(area)) - //} else { - //let [x, y, width, height] = area; - //let label = "No chain selected"; - //let x = x + (width - label.len() as u16) / 2; - //let y = y + height / 2; - //to.blit(&label, x, y, Some(Style::default().dim().bold()))?; - //Ok(Some(area)) - //} - } -} - -//impl Content for Mixer { - //fn content (&self) -> impl Content { - //Stack::right(|add| { - //for channel in self.tracks.iter() { - //add(channel)?; - //} - //Ok(()) - //}) - //} -//} - -//impl Content for Track { - //fn content (&self) -> impl Content { - //TrackView { - //chain: Some(&self), - //direction: tek_core::Direction::Right, - //focused: true, - //entered: true, - ////pub channels: u8, - ////pub input_ports: Vec>, - ////pub pre_gain_meter: f64, - ////pub gain: f64, - ////pub insert_ports: Vec>, - ////pub return_ports: Vec>, - ////pub post_gain_meter: f64, - ////pub post_insert_meter: f64, - ////pub level: f64, - ////pub pan: f64, - ////pub output_ports: Vec>, - ////pub post_fader_meter: f64, - ////pub route: String, - //} - //} -//} - -handle!(TuiIn: |self: Mixer, engine|{ - if let crossterm::event::Event::Key(event) = engine.event() { - - match event.code { - //KeyCode::Char('c') => { - //if event.modifiers == KeyModifiers::CONTROL { - //self.exit(); - //} - //}, - KeyCode::Down => { - self.selected_track = (self.selected_track + 1) % self.tracks.len(); - println!("{}", self.selected_track); - return Ok(Some(true)) - }, - KeyCode::Up => { - if self.selected_track == 0 { - self.selected_track = self.tracks.len() - 1; - } else { - self.selected_track -= 1; - } - println!("{}", self.selected_track); - return Ok(Some(true)) - }, - KeyCode::Left => { - if self.selected_column == 0 { - self.selected_column = 6 - } else { - self.selected_column -= 1; - } - return Ok(Some(true)) - }, - KeyCode::Right => { - if self.selected_column == 6 { - self.selected_column = 0 - } else { - self.selected_column += 1; - } - return Ok(Some(true)) - }, - _ => { - println!("\n{event:?}"); - } - } - - } - Ok(None) -}); - -handle!(TuiIn: |self:MixerTrack,from|{ - match from.event() { - //, NONE, "chain_cursor_up", "move cursor up", || { - kpat!(KeyCode::Up) => { - Ok(Some(true)) - }, - // , NONE, "chain_cursor_down", "move cursor down", || { - kpat!(KeyCode::Down) => { - Ok(Some(true)) - }, - // Left, NONE, "chain_cursor_left", "move cursor left", || { - kpat!(KeyCode::Left) => { - //if let Some(track) = app.arranger.track_mut() { - //track.device = track.device.saturating_sub(1); - //return Ok(true) - //} - Ok(Some(true)) - }, - // , NONE, "chain_cursor_right", "move cursor right", || { - kpat!(KeyCode::Right) => { - //if let Some(track) = app.arranger.track_mut() { - //track.device = (track.device + 1).min(track.devices.len().saturating_sub(1)); - //return Ok(true) - //} - Ok(Some(true)) - }, - // , NONE, "chain_mode_switch", "switch the display mode", || { - kpat!(KeyCode::Char('`')) => { - //app.chain_mode = !app.chain_mode; - Ok(Some(true)) - }, - _ => Ok(None) - } -}); - -pub enum MixerTrackCommand {} - -//impl MixerTrackDevice for LV2Plugin {} - -pub trait MixerTrackDevice: Debug + Send + Sync { - fn boxed (self) -> Box where Self: Sized + 'static { - Box::new(self) - } -} - -impl MixerTrackDevice for Sampler {} - -impl MixerTrackDevice for Plugin {} - -const SYM_NAME: &str = ":name"; -const SYM_GAIN: &str = ":gain"; -const SYM_SAMPLER: &str = "sampler"; -const SYM_LV2: &str = "lv2"; - -from_edn!("mixer/track" => |jack: &Arc>, args| -> MixerTrack { - let mut _gain = 0.0f64; - let mut track = MixerTrack { - name: "".into(), - audio_ins: vec![], - audio_outs: vec![], - devices: vec![], - }; - edn!(edn in args { - Edn::Map(map) => { - if let Some(Edn::Str(n)) = map.get(&Edn::Key(SYM_NAME)) { - track.name = n.to_string(); - } - if let Some(Edn::Double(g)) = map.get(&Edn::Key(SYM_GAIN)) { - _gain = f64::from(*g); - } - }, - Edn::List(args) => match args.first() { - // Add a sampler device to the track - Some(Edn::Symbol(SYM_SAMPLER)) => { - track.devices.push( - Box::new(Sampler::from_edn(jack, &args[1..])?) as Box - ); - panic!( - "unsupported in track {}: {:?}; tek_mixer not compiled with feature \"sampler\"", - &track.name, - args.first().unwrap() - ) - }, - // Add a LV2 plugin to the track. - Some(Edn::Symbol(SYM_LV2)) => { - track.devices.push( - Box::new(Plugin::from_edn(jack, &args[1..])?) as Box - ); - panic!( - "unsupported in track {}: {:?}; tek_mixer not compiled with feature \"plugin\"", - &track.name, - args.first().unwrap() - ) - }, - None => - panic!("empty list track {}", &track.name), - _ => - panic!("unexpected in track {}: {:?}", &track.name, args.first().unwrap()) - }, - _ => {} - }); - Ok(track) -}); - -//impl ArrangerScene { - - ////TODO - ////pub fn from_edn <'a, 'e> (args: &[Edn<'e>]) -> Usually { - ////let mut name = None; - ////let mut clips = vec![]; - ////edn!(edn in args { - ////Edn::Map(map) => { - ////let key = map.get(&Edn::Key(":name")); - ////if let Some(Edn::Str(n)) = key { - ////name = Some(*n); - ////} else { - ////panic!("unexpected key in scene '{name:?}': {key:?}") - ////} - ////}, - ////Edn::Symbol("_") => { - ////clips.push(None); - ////}, - ////Edn::Int(i) => { - ////clips.push(Some(*i as usize)); - ////}, - ////_ => panic!("unexpected in scene '{name:?}': {edn:?}") - ////}); - ////Ok(ArrangerScene { - ////name: Arc::new(name.unwrap_or("").to_string().into()), - ////color: ItemColor::random(), - ////clips, - ////}) - ////} -//} - -// TODO: -//keymap!(TRANSPORT_BPM_KEYS = |state: Clock, input: Event| ClockCommand { - //key(Char(',')) => SetBpm(state.bpm().get() - 1.0), - //key(Char('.')) => SetBpm(state.bpm().get() + 1.0), - //key(Char('<')) => SetBpm(state.bpm().get() - 0.001), - //key(Char('>')) => SetBpm(state.bpm().get() + 0.001), -//}); -//keymap!(TRANSPORT_QUANT_KEYS = |state: Clock, input: Event| ClockCommand { - //key(Char(',')) => SetQuant(state.quant.prev()), - //key(Char('.')) => SetQuant(state.quant.next()), - //key(Char('<')) => SetQuant(state.quant.prev()), - //key(Char('>')) => SetQuant(state.quant.next()), -//}); -//keymap!(TRANSPORT_SYNC_KEYS = |sync: Clock, input: Event | ClockCommand { - //key(Char(',')) => SetSync(state.sync.prev()), - //key(Char('.')) => SetSync(state.sync.next()), - //key(Char('<')) => SetSync(state.sync.prev()), - //key(Char('>')) => SetSync(state.sync.next()), -//}); -//keymap!(TRANSPORT_SEEK_KEYS = |state: Clock, input: Event| ClockCommand { - //key(Char(',')) => todo!("transport seek bar"), - //key(Char('.')) => todo!("transport seek bar"), - //key(Char('<')) => todo!("transport seek beat"), - //key(Char('>')) => todo!("transport seek beat"), -//}); - -//#[derive(Debug)] -//pub struct MIDIPlayer { - ///// Global timebase - //pub clock: Arc, - ///// Start time and clip being played - //pub play_clip: Option<(Moment, Option>>)>, - ///// Start time and next clip - //pub next_clip: Option<(Moment, Option>>)>, - ///// Play input through output. - //pub monitoring: bool, - ///// Write input to sequence. - //pub recording: bool, - ///// Overdub input to sequence. - //pub overdub: bool, - ///// Send all notes off - //pub reset: bool, // TODO?: after Some(nframes) - ///// Record from MIDI ports to current sequence. - //pub midi_inputs: Vec>, - ///// Play from current sequence to MIDI ports - //pub midi_outputs: Vec>, - ///// MIDI output buffer - //pub midi_note: Vec, - ///// MIDI output buffer - //pub midi_chunk: Vec>>, - ///// Notes currently held at input - //pub notes_in: Arc>, - ///// Notes currently held at output - //pub notes_out: Arc>, -//} - -///// Methods used primarily by the process callback -//impl MIDIPlayer { - //pub fn new ( - //jack: &Arc>, - //clock: &Arc, - //name: &str - //) -> Usually { - //let jack = jack.read().unwrap(); - //Ok(Self { - //clock: clock.clone(), - //clip: None, - //next_clip: None, - //notes_in: Arc::new(RwLock::new([false;128])), - //notes_out: Arc::new(RwLock::new([false;128])), - //monitoring: false, - //recording: false, - //overdub: true, - //reset: true, - //midi_note: Vec::with_capacity(8), - //midi_chunk: vec![Vec::with_capacity(16);16384], - //midi_outputs: vec![ - //jack.client().register_port(format!("{name}_out0").as_str(), MidiOut::default())? - //], - //midi_inputs: vec![ - //jack.client().register_port(format!("{name}_in0").as_str(), MidiIn::default())? - //], - //}) - //} -//} -use std::sync::{Arc, RwLock}; -use std::collections::BTreeMap; -pub use clojure_reader::edn::Edn; - -//#[derive(Debug, Copy, Clone, Default, PartialEq)] -//pub struct Items<'a>(&'a [Item<'a>]); -//impl<'a> Items<'a> { - //fn iter (&'a self) -> ItemsIterator<'a> { - //ItemsIterator(0, self.0) - //} -//} - -//pub struct ItemsIterator<'a>(usize, &'a [Item<'a>]); -//impl<'a> Iterator for ItemsIterator<'a> { - //type Item = &'a Item<'a>; - //fn next (&mut self) -> Option { - //let item = self.1.get(self.0); - //self.0 += 1; - //item - //} -//} - -/* - -nice but doesn't work without compile time slice concat -(which i guess could be implemeted using an unsafe linked list?) -never done that one before im ny life, might try - -use konst::slice_concat; - -const fn read <'a> ( - chars: impl Iterator -) -> Result, ParseError> { - use Range::*; - let mut state = Range::Nil; - let mut tokens: &[Range<'a>] = &[]; - while let Some(c) = chars.next() { - state = match state { - // must begin expression - Nil => match c { - ' ' => Nil, - '(' => Exp(&[]), - ':' => Sym(&[]), - '1'..'9' => Num(digit(c)), - 'a'..'z' => Key(&[&[c]]), - _ => return Err(ParseError::Unexpected(c)) - }, - Num(b) => match c { - ' ' => return Ok(Num(digit(c))), - '1'..'9' => Num(b*10+digit(c)), - _ => return Err(ParseError::Unexpected(c)) - } - Sym([]) => match c { - 'a'..'z' => Sym(&[c]), - _ => return Err(ParseError::Unexpected(c)) - }, - Sym([b @ ..]) => match c { - ' ' => return Ok(Sym(&b)), - 'a'..'z' | '0'..'9' | '-' => Sym(&[..b, c]), - _ => return Err(ParseError::Unexpected(c)) - } - Key([[b @ ..]]) => match c { - ' ' => return Ok(Key(&[&b])), - '/' => Key(&[&b, &[]]), - 'a'..'z' | '0'..'9' | '-' => Key(&[&[..b, c], &[]]), - _ => return Err(ParseError::Unexpected(c)) - } - Key([s @ .., []]) => match c { - 'a'..'z' => Key(&[..s, &[c]]), - _ => return Err(ParseError::Unexpected(c)) - } - Key([s @ .., [b @ ..]]) => match c { - '/' => Key([..s, &b, &[]]), - 'a'..'z' | '0'..'9' | '-' => Key(&[..s, &[..b, c]]), - _ => return Err(ParseError::Unexpected(c)) - } - // expression must begin with key or symbol - Exp([]) => match c { - ' ' => Exp(&[]), - ')' => return Err(ParseError::Empty), - ':' => Exp(&[Sym(&[':'])]), - c => Exp(&[Key(&[&[c]])]), - }, - - // expression can't begin with number - Exp([Num(num)]) => return Err(ParseError::Unexpected(c)), - - // symbol begins with : and lowercase a-z - Exp([Sym([':'])]) => match c { - 'a'..'z' => Exp(&[Sym(&[':', c])]), - _ => return Err(ParseError::Unexpected(c)), - }, - - // any other char is part of symbol until space or ) - Exp([Sym([':', b @ ..])]) => match c { - ')' => { tokens = &[..tokens, Exp(&[Sym(&[":", ..b])])]; Nil }, - ' ' => Exp(&[Sym(&[':', ..b]), Nil]), - c => Exp(&[Sym(&[':', ..b, c])]), - }, - - // key begins with lowercase a-z - Exp([Key([])]) => match c { - 'a'..'z' => Exp([Key([[c]])]), - _ => return Err(ParseError::Unexpected(c)), - }, - - // any other char is part of key until slash space or ) - Exp([Key([[b @ ..]])]) => match c { - '/' => Exp(&[Key(&[[..b], []])]), - ' ' => Exp(&[Key(&[[..b]]), Nil]), - ')' => { tokens = &[..tokens, Exp(&[Sym(&[":", ..b])])]; Nil }, - c => Exp(&[Key(&[[..b, c]])]) - } - - // slash adds new section to key - Exp([Key([b @ .., []])]) => match c { - '/' => Exp(&[Key(&[[..b], []])]), - ' ' => Exp(&[Key(&[[..b]]), Nil]), - ')' => { tokens = &[..tokens, Exp(&[Sym(&[":", ..b])])]; Nil }, - c => Exp(&[Key(&[[..b, c]])]) - } - - } - } - Ok(state) -} -*/ - - -/// EDN parsing helper. -#[macro_export] macro_rules! edn { - ($edn:ident { $($pat:pat => $expr:expr),* $(,)? }) => { - match $edn { $($pat => $expr),* } - }; - ($edn:ident in $args:ident { $($pat:pat => $expr:expr),* $(,)? }) => { - for $edn in $args { - edn!($edn { $($pat => $expr),* }) - } - }; -} - -pub trait FromEdn: Sized { - const ID: &'static str; - fn from_edn (context: C, expr: &[Edn<'_>]) -> - std::result::Result>; -} - -/// Implements the [FromEdn] trait. -#[macro_export] macro_rules! from_edn { - ($id:expr => |$context:tt:$Context:ty, $args:ident| -> $T:ty $body:block) => { - impl FromEdn<$Context> for $T { - const ID: &'static str = $id; - fn from_edn <'e> ($context: $Context, $args: &[Edn<'e>]) -> Usually { - $body - } - } - } -} -use crate::*; -use std::sync::Arc; -pub enum EdnIterator { - Nil, - Sym(Arc), - Exp(Vec) -} -impl EdnIterator { - pub fn new (item: &EdnItem) -> Self { - use EdnItem::*; - match item { - Sym(t) => Self::Sym(t.clone()), - Exp(i) => Self::Exp(i.iter().map(EdnIterator::new).collect()), - _ => Self::Nil, - } - } -} -impl Iterator for EdnIterator { - type Item = EdnItem; - fn next (&mut self) -> Option { - use EdnIterator::*; - match self { - Sym(t) => { - let t = *t; - *self = Nil; - Some(Sym(t)) - }, - Exp(v) => match v.as_mut_slice() { - [a] => if let Some(next) = a.next() { - Some(next) - } else { - *self = Exp(v.split_off(1)); - self.next() - }, - _ => { - *self = Nil; - None - } - }, - _ => { - *self = Nil; - None - } - } - } -} diff --git a/.old/todo_arranger.edn b/.old/todo_arranger.edn deleted file mode 100644 index 81fc7dc7..00000000 --- a/.old/todo_arranger.edn +++ /dev/null @@ -1,83 +0,0 @@ - This is the unified Tek Arranger. - - Its appearance is defined by the following view definition: - -{def :view (bsp/s (fixed/y 2 :toolbar) - (fill/x (align/c (bsp/w (fixed/x :pool-w :pool) - (bsp/n (fixed/y 3 :outputs) - (bsp/n (fixed/y 3 :inputs) - (bsp/n (fixed/y 3 :tracks) :scenes)))))))} - - The arranger's behavior is controlled by the - following keymaps: - -{def :keys - (@u undo 1) - (@shift-u redo 1) - (@space clock toggle) - (@shift-space clock toggle 0) - (@ctrl-a scene add) - (@ctrl-t track add) - (@tab edit :clip) - (@c color)} - -{def :keys-mix - (@down select 0 1) - (@s select 0 1) - - (@right select 1 0) - (@d select 1 0)} - -{def :keys-track - (@left select :track-prev :scene) - (@a select :track-prev :scene) - (@right select :track-next :scene) - (@d select :track-next :scene) - (@down select :track :scene-next) - (@s select :track :scene-next) - - (@q track launch) - (@c track color :track) - (@comma track swap-prev) - (@period track swap-next) - (@lt track size-dec) - (@gt track size-inc) - (@delete track delete)} - -{def :keys-scene - (@up select :track :scene-prev) - (@w select :track :scene-prev) - (@down select :track :scene-next) - (@s select :track :scene-next) - (@right select :track-next :scene) - (@d select :track-next :scene) - - (@q scene launch) - (@c scene color :scene) - (@comma scene swap-prev) - (@period scene swap-next) - (@lt scene size-dec) - (@gt scene size-inc) - (@delete scene delete)} - -{def :keys-clip - (@up select :track :scene-prev) - (@w select :track :scene-prev) - (@down select :track :scene-next) - (@s select :track :scene-next) - (@left select :track-prev :scene) - (@a select :track-prev :scene) - (@right select :track-next :scene) - (@d select :track-next :scene) - - (@q enqueue :clip) - (@c clip color :track :scene) - (@g clip get) - (@p clip put) - (@delete clip del) - (@comma clip prev) - (@period clip next) - (@lt clip swap-prev) - (@gt clip swap-next) - (@l clip loop-toggle)} - diff --git a/.old/scratch.rs b/.scratch.rs similarity index 100% rename from .old/scratch.rs rename to .scratch.rs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index f40cf8e8..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,18 +0,0 @@ -## development - -you'll need a Rust toolchain and various system libraries. - -you can obtain the former using `rustup` and the latter using `nix-shell`. -there's a `shell.nix` provided with the project. - -from there, use the commands in the `Justfile`, e.g.: - -```sh -just arranger -``` - -note that `tek > 0.2.0-rc.7` will require rust nightly -for the unstable features `type_alias_impl_trait` and -`impl_trait_in_assoc_type`. make some noise for lucky -[**rust rfc2515**](https://github.com/rust-lang/rust/issues/63063) -if you want to see this buildable with stable/beta. diff --git a/Cargo.lock b/Cargo.lock index 34c84187..c73f9036 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,22 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "ab_glyph" -version = "0.2.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e074464580a518d16a7126262fffaaa47af89d4099d4cb403f8ed938ba12ee7d" -dependencies = [ - "ab_glyph_rasterizer", - "owned_ttf_parser", -] - -[[package]] -name = "ab_glyph_rasterizer" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" - [[package]] name = "addr2line" version = "0.24.2" @@ -29,22 +13,9 @@ dependencies = [ [[package]] name = "adler2" -version = "2.0.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "getrandom 0.3.3", - "once_cell", - "version_check", - "zerocopy", -] +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "allocator-api2" @@ -52,38 +23,11 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" -[[package]] -name = "android-activity" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" -dependencies = [ - "android-properties", - "bitflags 2.9.3", - "cc", - "cesu8", - "jni", - "jni-sys", - "libc", - "log", - "ndk", - "ndk-context", - "ndk-sys", - "num_enum", - "thiserror 1.0.69", -] - -[[package]] -name = "android-properties" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" - [[package]] name = "anstream" -version = "0.6.20" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", @@ -96,37 +40,36 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" dependencies = [ "anstyle", - "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -138,30 +81,12 @@ dependencies = [ "num-traits", ] -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - [[package]] name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" -[[package]] -name = "as-raw-xcb-connection" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - [[package]] name = "atomic_float" version = "1.1.0" @@ -170,15 +95,15 @@ checksum = "628d228f918ac3b82fe590352cc719d30664a0c13ca3a60266fe02c7132d480a" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" -version = "0.3.75" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", "cfg-if", @@ -186,7 +111,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -199,21 +124,6 @@ dependencies = [ "console", ] -[[package]] -name = "bit-set" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" - [[package]] name = "bitflags" version = "1.3.2" @@ -222,24 +132,15 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.3" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" - -[[package]] -name = "block2" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" -dependencies = [ - "objc2", -] +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "by_address" @@ -249,41 +150,15 @@ checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" [[package]] name = "bytemuck" -version = "1.23.2" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" [[package]] -name = "bytes" -version = "1.10.1" +name = "byteorder" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" - -[[package]] -name = "calloop" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" -dependencies = [ - "bitflags 2.9.3", - "log", - "polling", - "rustix 0.38.44", - "slab", - "thiserror 1.0.69", -] - -[[package]] -name = "calloop-wayland-source" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" -dependencies = [ - "calloop", - "rustix 0.38.44", - "wayland-backend", - "wayland-client", -] +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cassowary" @@ -293,47 +168,24 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "castaway" -version = "0.2.4" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" dependencies = [ "rustversion", ] -[[package]] -name = "cc" -version = "1.2.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" -dependencies = [ - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.5.45" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" +checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" dependencies = [ "clap_builder", "clap_derive", @@ -341,9 +193,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.44" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" +checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" dependencies = [ "anstream", "anstyle", @@ -353,9 +205,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.45" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ "heck", "proc-macro2", @@ -365,25 +217,24 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.5" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "clojure-reader" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edf141eea627c101a97509266bc9f6ba8cd408618f5e2ac4a0cb6b64b1d4ea8" +dependencies = [ + "ordered-float", +] [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "compact_str" @@ -399,20 +250,11 @@ dependencies = [ "static_assertions", ] -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "console" -version = "0.15.11" +version = "0.15.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b" dependencies = [ "encode_unicode", "libc", @@ -420,86 +262,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "const_panic" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb8a602185c3c95b52f86dc78e55a6df9a287a7a93ddbcf012509930880cf879" -dependencies = [ - "const_panic_proc_macros", - "typewit", -] - -[[package]] -name = "const_panic_proc_macros" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c5b80a80fb52c1a6ca02e3cd829a76b472ff0a15588196fd8da95221f0c1e4b" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "unicode-xid", -] - -[[package]] -name = "convert_case" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "core-graphics" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "core-graphics-types", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "libc", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -531,29 +293,11 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.6.0", "crossterm_winapi", "mio", - "parking_lot 0.12.4", - "rustix 0.38.44", - "signal-hook", - "signal-hook-mio", - "winapi", -] - -[[package]] -name = "crossterm" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" -dependencies = [ - "bitflags 2.9.3", - "crossterm_winapi", - "derive_more", - "document-features", - "mio", - "parking_lot 0.12.4", - "rustix 1.0.8", + "parking_lot 0.12.3", + "rustix", "signal-hook", "signal-hook-mio", "winapi", @@ -568,27 +312,11 @@ dependencies = [ "winapi", ] -[[package]] -name = "ctor" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" -dependencies = [ - "quote", - "syn", -] - -[[package]] -name = "cursor-icon" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" - [[package]] name = "darling" -version = "0.20.11" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" dependencies = [ "darling_core", "darling_macro", @@ -596,9 +324,9 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.11" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" dependencies = [ "fnv", "ident_case", @@ -610,9 +338,9 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.20.11" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", @@ -620,67 +348,16 @@ dependencies = [ ] [[package]] -name = "derive_more" -version = "2.0.1" +name = "diff" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - -[[package]] -name = "dlib" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" -dependencies = [ - "libloading", -] - -[[package]] -name = "document-features" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" -dependencies = [ - "litrs", -] - -[[package]] -name = "downcast-rs" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" - -[[package]] -name = "dpi" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" [[package]] name = "either" -version = "1.15.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "encode_unicode" @@ -699,18 +376,18 @@ dependencies = [ [[package]] name = "equivalent" -version = "1.0.2" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.13" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -725,12 +402,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - [[package]] name = "fnv" version = "1.0.7" @@ -739,68 +410,19 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" -version = "0.1.5" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "foreign-types" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" -dependencies = [ - "foreign-types-macros", - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "foreign-types-shared" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" - -[[package]] -name = "gethostname" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" -dependencies = [ - "libc", - "windows-targets 0.48.5", -] +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", -] - -[[package]] -name = "getrandom" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasi", ] [[package]] @@ -811,9 +433,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "hashbrown" -version = "0.15.5" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" dependencies = [ "allocator-api2", "equivalent", @@ -826,12 +448,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - [[package]] name = "i24" version = "1.0.1" @@ -850,9 +466,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "indexmap" -version = "2.11.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", "hashbrown", @@ -860,18 +476,19 @@ dependencies = [ [[package]] name = "indoc" -version = "2.0.6" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" [[package]] name = "instability" -version = "0.3.9" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435d80800b936787d62688c927b6490e887c7ef5ff9ce922c6c6050fca75eb9a" +checksum = "898e106451f7335950c9cc64f8ec67b5f65698679ac67ed00619aeef14e1cf75" dependencies = [ "darling", "indoc", + "pretty_assertions", "proc-macro2", "quote", "syn", @@ -901,29 +518,17 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jack" version = "0.13.0" dependencies = [ - "approx", - "bitflags 2.9.3", - "crossbeam-channel", - "ctor", + "bitflags 2.6.0", "jack-sys", "lazy_static", "libc", @@ -934,7 +539,7 @@ dependencies = [ name = "jack-sys" version = "0.5.1" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.6.0", "lazy_static", "libc", "libloading", @@ -942,75 +547,16 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - -[[package]] -name = "jni-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" - -[[package]] -name = "jobserver" -version = "0.1.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" -dependencies = [ - "getrandom 0.3.3", - "libc", -] - [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" dependencies = [ "once_cell", "wasm-bindgen", ] -[[package]] -name = "konst" -version = "0.3.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4381b9b00c55f251f2ebe9473aef7c117e96828def1a7cb3bd3f0f903c6894e9" -dependencies = [ - "const_panic", - "konst_kernel", - "konst_proc_macros", - "typewit", -] - -[[package]] -name = "konst_kernel" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4b1eb7788f3824c629b1116a7a9060d6e898c358ebff59070093d51103dcc3c" -dependencies = [ - "typewit", -] - -[[package]] -name = "konst_proc_macros" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00af7901ba50898c9e545c24d5c580c96a982298134e8037d8978b6594782c07" - [[package]] name = "lazy_static" version = "1.5.0" @@ -1019,29 +565,18 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.175" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libloading" -version = "0.8.8" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets 0.53.3", -] - -[[package]] -name = "libredox" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" -dependencies = [ - "bitflags 2.9.3", - "libc", - "redox_syscall 0.5.17", + "windows-targets", ] [[package]] @@ -1067,21 +602,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.15" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - -[[package]] -name = "linux-raw-sys" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" - -[[package]] -name = "litrs" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "livi" @@ -1098,9 +621,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -1108,9 +631,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.27" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "lru" @@ -1138,18 +661,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.5" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" - -[[package]] -name = "memmap2" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" -dependencies = [ - "libc", -] +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "midly" @@ -1162,53 +676,23 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.8.9" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" dependencies = [ "adler2", ] [[package]] name = "mio" -version = "1.0.4" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", "log", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", -] - -[[package]] -name = "ndk" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" -dependencies = [ - "bitflags 2.9.3", - "jni-sys", - "log", - "ndk-sys", - "num_enum", - "raw-window-handle", - "thiserror 1.0.69", -] - -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - -[[package]] -name = "ndk-sys" -version = "0.6.0+11769913" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" -dependencies = [ - "jni-sys", + "wasi", + "windows-sys 0.52.0", ] [[package]] @@ -1220,231 +704,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "num_enum" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "objc-sys" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" - -[[package]] -name = "objc2" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" -dependencies = [ - "objc-sys", - "objc2-encode", -] - -[[package]] -name = "objc2-app-kit" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" -dependencies = [ - "bitflags 2.9.3", - "block2", - "libc", - "objc2", - "objc2-core-data", - "objc2-core-image", - "objc2-foundation", - "objc2-quartz-core", -] - -[[package]] -name = "objc2-cloud-kit" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" -dependencies = [ - "bitflags 2.9.3", - "block2", - "objc2", - "objc2-core-location", - "objc2-foundation", -] - -[[package]] -name = "objc2-contacts" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" -dependencies = [ - "block2", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-data" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" -dependencies = [ - "bitflags 2.9.3", - "block2", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-image" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" -dependencies = [ - "block2", - "objc2", - "objc2-foundation", - "objc2-metal", -] - -[[package]] -name = "objc2-core-location" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" -dependencies = [ - "block2", - "objc2", - "objc2-contacts", - "objc2-foundation", -] - -[[package]] -name = "objc2-encode" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" - -[[package]] -name = "objc2-foundation" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" -dependencies = [ - "bitflags 2.9.3", - "block2", - "dispatch", - "libc", - "objc2", -] - -[[package]] -name = "objc2-link-presentation" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" -dependencies = [ - "block2", - "objc2", - "objc2-app-kit", - "objc2-foundation", -] - -[[package]] -name = "objc2-metal" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" -dependencies = [ - "bitflags 2.9.3", - "block2", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" -dependencies = [ - "bitflags 2.9.3", - "block2", - "objc2", - "objc2-foundation", - "objc2-metal", -] - -[[package]] -name = "objc2-symbols" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-ui-kit" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" -dependencies = [ - "bitflags 2.9.3", - "block2", - "objc2", - "objc2-cloud-kit", - "objc2-core-data", - "objc2-core-image", - "objc2-core-location", - "objc2-foundation", - "objc2-link-presentation", - "objc2-quartz-core", - "objc2-symbols", - "objc2-uniform-type-identifiers", - "objc2-user-notifications", -] - -[[package]] -name = "objc2-uniform-type-identifiers" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" -dependencies = [ - "block2", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-user-notifications" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" -dependencies = [ - "bitflags 2.9.3", - "block2", - "objc2", - "objc2-core-location", - "objc2-foundation", -] - [[package]] name = "object" version = "0.36.7" @@ -1456,32 +715,17 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] -name = "once_cell_polyfill" -version = "1.70.1" +name = "ordered-float" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" - -[[package]] -name = "orbclient" -version = "0.3.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba0b26cec2e24f08ed8bb31519a9333140a6599b867dac464bb150bdb796fd43" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" dependencies = [ - "libredox", -] - -[[package]] -name = "owned_ttf_parser" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" -dependencies = [ - "ttf-parser", + "num-traits", ] [[package]] @@ -1494,7 +738,7 @@ dependencies = [ "fast-srgb8", "palette_derive", "phf", - "rand 0.8.5", + "rand", ] [[package]] @@ -1522,12 +766,12 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", - "parking_lot_core 0.9.11", + "parking_lot_core 0.9.10", ] [[package]] @@ -1546,15 +790,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.17", + "redox_syscall 0.5.8", "smallvec", - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -1563,17 +807,11 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - [[package]] name = "phf" -version = "0.11.3" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ "phf_macros", "phf_shared", @@ -1581,19 +819,19 @@ dependencies = [ [[package]] name = "phf_generator" -version = "0.11.3" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" dependencies = [ "phf_shared", - "rand 0.8.5", + "rand", ] [[package]] name = "phf_macros" -version = "0.11.3" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" dependencies = [ "phf_generator", "phf_shared", @@ -1604,162 +842,71 @@ dependencies = [ [[package]] name = "phf_shared" -version = "0.11.3" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" dependencies = [ "siphasher", ] -[[package]] -name = "pin-project" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "polling" -version = "3.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5bd19146350fe804f7cb2669c851c03d69da628803dab0d98018142aaa5d829" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi", - "pin-project-lite", - "rustix 1.0.8", - "windows-sys 0.60.2", -] +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "ppv-lite86" -version = "0.2.21" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ "zerocopy", ] [[package]] -name = "proc-macro-crate" -version = "3.3.0" +name = "pretty_assertions" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" dependencies = [ - "toml_edit", + "diff", + "yansi", ] [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] -[[package]] -name = "proptest" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fcdab19deb5195a31cf7726a210015ff1496ba1464fd42cb4f537b8b01b471f" -dependencies = [ - "bit-set", - "bit-vec", - "bitflags 2.9.3", - "lazy_static", - "num-traits", - "rand 0.9.2", - "rand_chacha 0.9.0", - "rand_xorshift", - "regex-syntax", - "rusty-fork", - "tempfile", - "unarray", -] - -[[package]] -name = "proptest-derive" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee1c9ac207483d5e7db4940700de86a9aae46ef90c48b57f99fe7edb8345e49" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "quanta" -version = "0.12.6" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" +checksum = "773ce68d0bb9bc7ef20be3536ffe94e223e1f365bd374108b2659fac0c65cfe6" dependencies = [ "crossbeam-utils", "libc", "once_cell", "raw-cpuid", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "web-sys", "winapi", ] -[[package]] -name = "quick-error" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" - -[[package]] -name = "quick-xml" -version = "0.37.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" -dependencies = [ - "memchr", -] - [[package]] name = "quote" -version = "1.0.40" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - [[package]] name = "rand" version = "0.8.5" @@ -1767,18 +914,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.3", + "rand_chacha", + "rand_core", ] [[package]] @@ -1788,17 +925,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.3", + "rand_core", ] [[package]] @@ -1807,25 +934,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", -] - -[[package]] -name = "rand_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" -dependencies = [ - "getrandom 0.3.3", -] - -[[package]] -name = "rand_xorshift" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" -dependencies = [ - "rand_core 0.9.3", + "getrandom", ] [[package]] @@ -1834,13 +943,13 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.6.0", "cassowary", "compact_str", - "crossterm 0.28.1", + "crossterm", "indoc", "instability", - "itertools 0.13.0", + "itertools", "lru", "paste", "strum", @@ -1851,24 +960,18 @@ dependencies = [ [[package]] name = "raw-cpuid" -version = "11.5.0" +version = "11.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6df7ab838ed27997ba19a4664507e6f82b41fe6e20be42929332156e5e85146" +checksum = "1ab240315c661615f2ee9f0f2cd32d5a7343a84d5ebcccb99d46e6637565e7b0" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.6.0", ] -[[package]] -name = "raw-window-handle" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" - [[package]] name = "rayon" -version = "1.11.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", @@ -1876,9 +979,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.13.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -1895,28 +998,13 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", ] -[[package]] -name = "redox_syscall" -version = "0.5.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" -dependencies = [ - "bitflags 2.9.3", -] - -[[package]] -name = "regex-syntax" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" - [[package]] name = "ringbuf" version = "0.3.3" @@ -1928,74 +1016,34 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.26" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" -version = "0.38.44" +version = "0.38.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.6.0", "errno", "libc", - "linux-raw-sys 0.4.15", + "linux-raw-sys", "windows-sys 0.59.0", ] -[[package]] -name = "rustix" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" -dependencies = [ - "bitflags 2.9.3", - "errno", - "libc", - "linux-raw-sys 0.9.4", - "windows-sys 0.60.2", -] - [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "rusty-fork" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" -dependencies = [ - "fnv", - "quick-error", - "tempfile", - "wait-timeout", -] +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "scopeguard" @@ -2003,33 +1051,20 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sctk-adwaita" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" -dependencies = [ - "ab_glyph", - "log", - "memmap2", - "smithay-client-toolkit", - "tiny-skia", -] - [[package]] name = "serde" -version = "1.0.219" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", @@ -2038,24 +1073,18 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.0" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - [[package]] name = "signal-hook" -version = "0.3.18" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" dependencies = [ "libc", "signal-hook-registry", @@ -2074,64 +1103,24 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] [[package]] name = "siphasher" -version = "1.0.1" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" - -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "smithay-client-toolkit" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" -dependencies = [ - "bitflags 2.9.3", - "calloop", - "calloop-wayland-source", - "cursor-icon", - "libc", - "log", - "memmap2", - "rustix 0.38.44", - "thiserror 1.0.69", - "wayland-backend", - "wayland-client", - "wayland-csd-frame", - "wayland-cursor", - "wayland-protocols", - "wayland-protocols-wlr", - "wayland-scanner", - "xkeysym", -] - -[[package]] -name = "smol_str" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" -dependencies = [ - "serde", -] +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "static_assertions" @@ -2139,12 +1128,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "strict-num" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" - [[package]] name = "strsim" version = "0.11.1" @@ -2370,9 +1353,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "9c786062daee0d6db1132800e623df74274a0a87322d8e183338e01b3d98d058" dependencies = [ "proc-macro2", "quote", @@ -2381,137 +1364,40 @@ dependencies = [ [[package]] name = "tek" -version = "0.3.0" +version = "0.2.0" dependencies = [ "atomic_float", "backtrace", "clap", + "clojure-reader", "jack", - "konst", "livi", "midly", + "once_cell", "palette", - "proptest", - "proptest-derive", - "rand 0.8.5", + "quanta", + "rand", "symphonia", - "tek_device", - "tengri", + "tek_layout", "toml", "uuid", "wavers", - "winit", - "xdg", -] - -[[package]] -name = "tek_device" -version = "0.3.0" -dependencies = [ - "atomic_float", - "backtrace", - "clap", - "jack", - "konst", - "livi", - "midly", - "palette", - "proptest", - "proptest-derive", - "rand 0.8.5", - "symphonia", - "tek_engine", - "tengri", - "toml", - "uuid", - "wavers", - "winit", - "xdg", ] [[package]] name = "tek_engine" -version = "0.3.0" +version = "0.2.0" dependencies = [ - "atomic_float", - "jack", - "midly", - "proptest", - "proptest-derive", - "tengri", -] - -[[package]] -name = "tempfile" -version = "3.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" -dependencies = [ - "fastrand", - "getrandom 0.3.3", - "once_cell", - "rustix 1.0.8", - "windows-sys 0.60.2", -] - -[[package]] -name = "tengri" -version = "0.14.0" -dependencies = [ - "tengri_core", - "tengri_dsl", - "tengri_input", - "tengri_output", - "tengri_tui", -] - -[[package]] -name = "tengri_core" -version = "0.14.0" - -[[package]] -name = "tengri_dsl" -version = "0.14.0" -dependencies = [ - "const_panic", - "itertools 0.14.0", - "konst", - "tengri_core", - "thiserror 2.0.16", -] - -[[package]] -name = "tengri_input" -version = "0.14.0" -dependencies = [ - "tengri_core", -] - -[[package]] -name = "tengri_output" -version = "0.14.0" -dependencies = [ - "tengri_core", - "tengri_dsl", -] - -[[package]] -name = "tengri_tui" -version = "0.14.0" -dependencies = [ - "atomic_float", "better-panic", - "crossterm 0.29.0", - "konst", - "palette", - "quanta", - "rand 0.8.5", + "crossterm", "ratatui", - "tengri_core", - "tengri_dsl", - "tengri_input", - "tengri_output", - "unicode-width 0.2.0", +] + +[[package]] +name = "tek_layout" +version = "0.2.0" +dependencies = [ + "tek_engine", ] [[package]] @@ -2520,16 +1406,7 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" -dependencies = [ - "thiserror-impl 2.0.16", + "thiserror-impl", ] [[package]] @@ -2543,146 +1420,45 @@ dependencies = [ "syn", ] -[[package]] -name = "thiserror-impl" -version = "2.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tiny-skia" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" -dependencies = [ - "arrayref", - "arrayvec", - "bytemuck", - "cfg-if", - "log", - "tiny-skia-path", -] - -[[package]] -name = "tiny-skia-path" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" -dependencies = [ - "arrayref", - "bytemuck", - "strict-num", -] - [[package]] name = "toml" -version = "0.9.5" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ - "indexmap", "serde", "serde_spanned", - "toml_datetime 0.7.0", - "toml_parser", - "toml_writer", - "winnow", + "toml_datetime", + "toml_edit", ] [[package]] name = "toml_datetime" -version = "0.6.11" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" - -[[package]] -name = "toml_datetime" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.27" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap", - "toml_datetime 0.6.11", + "serde", + "serde_spanned", + "toml_datetime", "winnow", ] -[[package]] -name = "toml_parser" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" -dependencies = [ - "winnow", -] - -[[package]] -name = "toml_writer" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" - -[[package]] -name = "tracing" -version = "0.1.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" -dependencies = [ - "pin-project-lite", - "tracing-core", -] - -[[package]] -name = "tracing-core" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" - -[[package]] -name = "ttf-parser" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" - -[[package]] -name = "typewit" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97e72ba082eeb9da9dc68ff5a2bf727ef6ce362556e8d29ec1aed3bd05e7d86a" -dependencies = [ - "typewit_proc_macros", -] - -[[package]] -name = "typewit_proc_macros" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" - -[[package]] -name = "unarray" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" - [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "unicode-segmentation" @@ -2696,7 +1472,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" dependencies = [ - "itertools 0.13.0", + "itertools", "unicode-segmentation", "unicode-width 0.1.14", ] @@ -2713,12 +1489,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "utf8parse" version = "0.2.2" @@ -2727,72 +1497,35 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.18.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" dependencies = [ - "getrandom 0.3.3", - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wait-timeout" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" -dependencies = [ - "libc", -] - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", + "getrandom", ] [[package]] name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" +version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" -dependencies = [ - "wit-bindgen-rt", -] +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" dependencies = [ "cfg-if", "once_cell", - "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.100" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" dependencies = [ "bumpalo", "log", @@ -2802,24 +1535,11 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" -dependencies = [ - "cfg-if", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2827,9 +1547,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", @@ -2840,12 +1560,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" -dependencies = [ - "unicode-ident", -] +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" [[package]] name = "wavers" @@ -2857,133 +1574,14 @@ dependencies = [ "i24", "num-traits", "paste", - "thiserror 1.0.69", -] - -[[package]] -name = "wayland-backend" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" -dependencies = [ - "cc", - "downcast-rs", - "rustix 1.0.8", - "scoped-tls", - "smallvec", - "wayland-sys", -] - -[[package]] -name = "wayland-client" -version = "0.31.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" -dependencies = [ - "bitflags 2.9.3", - "rustix 1.0.8", - "wayland-backend", - "wayland-scanner", -] - -[[package]] -name = "wayland-csd-frame" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" -dependencies = [ - "bitflags 2.9.3", - "cursor-icon", - "wayland-backend", -] - -[[package]] -name = "wayland-cursor" -version = "0.31.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "447ccc440a881271b19e9989f75726d60faa09b95b0200a9b7eb5cc47c3eeb29" -dependencies = [ - "rustix 1.0.8", - "wayland-client", - "xcursor", -] - -[[package]] -name = "wayland-protocols" -version = "0.32.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" -dependencies = [ - "bitflags 2.9.3", - "wayland-backend", - "wayland-client", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols-plasma" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a07a14257c077ab3279987c4f8bb987851bf57081b93710381daea94f2c2c032" -dependencies = [ - "bitflags 2.9.3", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols-wlr" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec" -dependencies = [ - "bitflags 2.9.3", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", -] - -[[package]] -name = "wayland-scanner" -version = "0.31.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" -dependencies = [ - "proc-macro2", - "quick-xml", - "quote", -] - -[[package]] -name = "wayland-sys" -version = "0.31.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" -dependencies = [ - "dlib", - "log", - "once_cell", - "pkg-config", + "thiserror", ] [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" dependencies = [ "js-sys", "wasm-bindgen", @@ -3005,43 +1603,19 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" -dependencies = [ - "windows-sys 0.60.2", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -3050,46 +1624,7 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "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.3", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", + "windows-targets", ] [[package]] @@ -3098,360 +1633,94 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "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", + "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", ] -[[package]] -name = "windows-targets" -version = "0.53.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" - [[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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" - -[[package]] -name = "winit" -version = "0.30.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66d4b9ed69c4009f6321f762d6e61ad8a2389cd431b97cb1e146812e9e6c732" -dependencies = [ - "ahash", - "android-activity", - "atomic-waker", - "bitflags 2.9.3", - "block2", - "bytemuck", - "calloop", - "cfg_aliases", - "concurrent-queue", - "core-foundation", - "core-graphics", - "cursor-icon", - "dpi", - "js-sys", - "libc", - "memmap2", - "ndk", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "objc2-ui-kit", - "orbclient", - "percent-encoding", - "pin-project", - "raw-window-handle", - "redox_syscall 0.4.1", - "rustix 0.38.44", - "sctk-adwaita", - "smithay-client-toolkit", - "smol_str", - "tracing", - "unicode-segmentation", - "wasm-bindgen", - "wasm-bindgen-futures", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-protocols-plasma", - "web-sys", - "web-time", - "windows-sys 0.52.0", - "x11-dl", - "x11rb", - "xkbcommon-dl", -] - [[package]] name = "winnow" -version = "0.7.13" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "yansi" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags 2.9.3", -] - -[[package]] -name = "x11-dl" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" -dependencies = [ - "libc", - "once_cell", - "pkg-config", -] - -[[package]] -name = "x11rb" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" -dependencies = [ - "as-raw-xcb-connection", - "gethostname", - "libc", - "libloading", - "once_cell", - "rustix 0.38.44", - "x11rb-protocol", -] - -[[package]] -name = "x11rb-protocol" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" - -[[package]] -name = "xcursor" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" - -[[package]] -name = "xdg" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fb433233f2df9344722454bc7e96465c9d03bff9d77c248f9e7523fe79585b5" - -[[package]] -name = "xkbcommon-dl" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" -dependencies = [ - "bitflags 2.9.3", - "dlib", - "log", - "once_cell", - "xkeysym", -] - -[[package]] -name = "xkeysym" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ + "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 9f379dc6..3fcd44a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,53 +1,66 @@ -[workspace] -resolver = "2" -members = [ "./app", "./engine", "./device" ] -exclude = [ "./deps/tengri" ] +[package] +name = "tek" +edition = "2021" +version = "0.2.0" -[workspace.package] -edition = "2024" -version = "0.3.0" +[dependencies] +tek_layout = { path = "./layout" } -[profile.release] -lto = true - -[profile.coverage] -inherits = "test" -lto = false - -[workspace.dependencies.tengri] -path = "./deps/tengri/tengri" -features = [ "tui", "dsl" ] - -[workspace.dependencies.tengri_proc] -path = "./deps/tengri/proc" - -[workspace.dependencies.jack] -path = "./deps/rust-jack" - -[workspace.dependencies] -tek = { path = "./tek" } - -atomic_float = { version = "1.0.0" } -backtrace = { version = "0.3.72" } -bumpalo = { version = "3.19.0" } -clap = { version = "4.5.4", features = [ "derive" ] } -gtk = { version = "0.18.1" } -konst = { version = "0.3.16", features = [ "rust_1_83" ] } -livi = { version = "0.7.4" } -midly = { version = "0.5" } -palette = { version = "0.7.6", features = [ "random" ] } -quanta = { version = "0.12.3" } -rand = { version = "0.8.5" } -symphonia = { version = "0.5.4", features = [ "all" ] } -toml = { version = "0.9.2" } -uuid = { version = "1.10.0", features = [ "v4" ] } -wavers = { version = "1.4.3" } -winit = { version = "0.30.4", features = [ "x11" ] } -xdg = { version = "3.0.0" } -#once_cell = "1.19.0" +atomic_float = "1.0.0" +backtrace = "0.3.72" +clap = { version = "4.5.4", features = [ "derive" ] } +clojure-reader = "0.3.0" +jack = { path = "./rust-jack" } +livi = "0.7.4" +midly = "0.5" +once_cell = "1.19.0" +palette = { version = "0.7.6", features = [ "random" ] } +quanta = "0.12.3" +rand = "0.8.5" +symphonia = { version = "0.5.4", features = [ "all" ] } +toml = "0.8.12" +uuid = { version = "1.10.0", features = [ "v4" ] } +wavers = "1.4.3" #no_deadlocks = "1.3.2" #suil-rs = { path = "../suil" } #vst = "0.4.0" #vst3 = "0.1.0" -proptest = { version = "^1" } -proptest-derive = { version = "^0.5.1" } +#winit = { version = "0.30.4", features = [ "x11" ] } + +[[bin]] +name = "tek_arranger" +path = "bin/cli_arranger.rs" + +[[bin]] +name = "tek_sequencer" +path = "bin/cli_sequencer.rs" + +[[bin]] +name = "tek_groovebox" +path = "bin/cli_groovebox.rs" + +[[bin]] +name = "tek_transport" +path = "bin/cli_transport.rs" + +[[bin]] +name = "tek_sampler" +path = "bin/cli_sampler.rs" + +#[[bin]] +#name = "tek_mixer" +#path = "src/cli_mixer.rs" + +#[[bin]] +#name = "tek_track" +#path = "src/cli_track.rs" + +#[[bin]] +#name = "tek_plugin" +#path = "src/cli_plugin.rs" + +[lib] +path = "src/lib.rs" + +[profile.release] +lto = true diff --git a/Justfile b/Justfile index 94551690..6c5ebfa0 100644 --- a/Justfile +++ b/Justfile @@ -1,117 +1,120 @@ -export RUSTFLAGS := "--cfg procmacro2_semver_exempt -Zmacro-backtrace -Clink-arg=-fuse-ld=mold" -export RUST_BACKTRACE := "1" - default: - @just -l + just -l -cloc: - for src in {cli,edn/src,input/src,jack/src,midi/src,output/src,plugin/src,sampler/src,tek/src,time/src,tui/src}; do echo; echo $src; cloc --quiet $src; done - -bacon: - bacon -s - -check: - reset && cargo check - -test: - cargo test --workspace --exclude jack - -covfig := "CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' RUSTDOCFLAGS='-Cinstrument-coverage' LLVM_PROFILE_FILE='cov/cargo-test-%p-%m.profraw'" -grcov-binary := "--binary-path ./target/coverage/deps/" -grcov-ignore := "--ignore-not-existing --ignore '../*' --ignore \"/*\" --ignore 'target/*'" -cov: - {{covfig}} time cargo test -j4 --workspace --exclude jack --profile coverage - rm -rf target/coverage/html || true - {{covfig}} time grcov . -s . {{grcov-binary}} {{grcov-ignore}} -t html -o target/coverage/html -cov-md: - {{covfig}} time cargo test -j4 --workspace --exclude jack --profile coverage - {{covfig}} time grcov . -s . {{grcov-binary}} {{grcov-ignore}} -t markdown | sort -llcov: - time cargo llvm-cov --workspace --exclude jack --profile coverage --no-report - time cargo llvm-cov --workspace --exclude jack --profile coverage --no-report --doc - time cargo llvm-cov report --doctests --html #--output-path target/coverage/html - -build: - reset && cargo build - -debug := "reset && cargo run --" -run: - {{debug}} -run-init: - rm -rf ~/.config/tek && {{debug}} - -prof: - CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph -- - -doc: - cargo doc -j4 --workspace --document-private-items - -release := "reset && cargo run --release --" -release: - {{release}} -build-release: - time cargo build -j4 --release +status: + cargo c + cloc --by-file src/ + git status amend: git commit --amend push: - git push -u codeberg main && git push -u origin main + git push -u codeberg main + git push -u origin main tpush: - git push --tags -u codeberg && git push --tags -u origin + git push --tags -u codeberg + git push --tags -u origin fpush: - git push -fu codeberg main && git push -fu origin main + git push -fu codeberg main + git push -fu origin main ftpush: - git push --tags -fu codeberg && git push --tags -fu origin + git push --tags -fu codeberg + git push --tags -fu origin -name := "-n tek" -bpm := "-b 174" -clock: - {{debug}} {{name}} {{bpm}} clock -clock-release: - {{release}} {{name}} {{bpm}} clock +transport: + reset + cargo run --bin tek_transport +transport-release: + reset + cargo run --release --bin tek_transport -midi-in := "-i 'Midi-Bridge:.*nanoKEY.*:.*capture.*'" -midi-out := "-o 'Midi-Bridge:.*playback.*'" -audio-in := "-l 'Komplete Audio 6 Pro:capture_AUX1' -r 'Komplete Audio 6 Pro:capture_AUX1'" -audio-out := "-L 'Komplete Audio 6 Pro:playback_AUX1' -R 'Komplete Audio 6 Pro:playback_AUX1'" -firefox-in := "-l 'Firefox:output_FL*' -r 'Firefox:output_FR*'" arranger: - {{debug}} {{name}} {{bpm}} arranger + reset + cargo run --bin tek_arranger arranger-ext: - {{debug}} {{name}} {{bpm}} {{midi-in}} {{midi-out}} arranger + reset + cargo run --bin tek_arranger -- -n tek \ + -i "1=Midi-Bridge:nanoKEY Studio 2:(capture_0) nanoKEY Studio nanoKEY Studio _" \ + -o "1=Midi-Bridge:Komplete Audio 6 1:(playback_0) Komplete Audio 6 MIDI 1" \ + -i "2=Midi-Bridge:nanoKEY Studio 2:(capture_0) nanoKEY Studio nanoKEY Studio _" \ + -o "2=Midi-Bridge:Komplete Audio 6 1:(playback_0) Komplete Audio 6 MIDI 1" \ + -i "3=Midi-Bridge:nanoKEY Studio 2:(capture_0) nanoKEY Studio nanoKEY Studio _" \ + -o "3=Midi-Bridge:Komplete Audio 6 1:(playback_0) Komplete Audio 6 MIDI 1" \ + -i "4=Midi-Bridge:nanoKEY Studio 2:(capture_0) nanoKEY Studio nanoKEY Studio _" \ + -o "4=Midi-Bridge:Komplete Audio 6 1:(playback_0) Komplete Audio 6 MIDI 1" arranger-release: - {{release}} {{name}} {{bpm}} arranger + reset + cargo run --release --bin tek_arranger arranger-release-ext: - {{release}} {{name}} {{bpm}} {{midi-in}} {{firefox-in}} {{midi-out}} arranger + reset + cargo run --release --bin tek_arranger -- -n tek \ + -i "1=Midi-Bridge:nanoKEY Studio 2:(capture_0) nanoKEY Studio nanoKEY Studio _" \ + -o "1=Midi-Bridge:Komplete Audio 6 1:(playback_0) Komplete Audio 6 MIDI 1" \ + -i "2=Midi-Bridge:nanoKEY Studio 2:(capture_0) nanoKEY Studio nanoKEY Studio _" \ + -o "2=Midi-Bridge:Komplete Audio 6 1:(playback_0) Komplete Audio 6 MIDI 1" \ + -i "3=Midi-Bridge:nanoKEY Studio 2:(capture_0) nanoKEY Studio nanoKEY Studio _" \ + -o "3=Midi-Bridge:Komplete Audio 6 1:(playback_0) Komplete Audio 6 MIDI 1" \ + -i "4=Midi-Bridge:nanoKEY Studio 2:(capture_0) nanoKEY Studio nanoKEY Studio _" \ + -o "4=Midi-Bridge:Komplete Audio 6 1:(playback_0) Komplete Audio 6 MIDI 1" groovebox: - {{debug}} {{name}} {{bpm}} groovebox + reset + cargo run --bin tek_groovebox -- -b 174 groovebox-ext: reset - {{debug}} {{name}} {{bpm}} {{midi-in}} {{midi-out}} {{audio-in}} {{audio-out}} groovebox -groovebox-browser: - {{debug}} {{name}} {{bpm}} {{audio-in}} groovebox + cargo run --bin tek_groovebox -- -n tek \ + -i "Midi-Bridge:nanoKEY Studio 1:(capture_0) nanoKEY Studio nanoKEY Studio _" \ + -l "Komplete Audio 6 Pro:capture_AUX1" \ + -r "Komplete Audio 6 Pro:capture_AUX1" \ + -L "Komplete Audio 6 Pro:playback_AUX1" \ + -R "Komplete Audio 6 Pro:playback_AUX1" groovebox-release: - {{release}} {{name}} {{bpm}} groovebox + reset + cargo run --release --bin tek_groovebox groovebox-release-ext: - {{release}} {{name}} {{bpm}} {{midi-in}} {{midi-out}} {{audio-in}} {{audio-out}} groovebox + reset + cargo run --release --bin tek_groovebox -- -n tek \ + -i "Midi-Bridge:nanoKEY Studio 1:(capture_0) nanoKEY Studio nanoKEY Studio _" \ + -l "Komplete Audio 6 Pro:capture_AUX1" \ + -r "Komplete Audio 6 Pro:capture_AUX1" \ + -L "Komplete Audio 6 Pro:playback_AUX1" \ + -R "Komplete Audio 6 Pro:playback_AUX2" groovebox-release-ext-browser: - {{release}} {{name}} {{bpm}} {{midi-in}} {{firefox-in}} {{audio-out}} groovebox + reset + cargo run --release --bin tek_groovebox -- -n tek \ + -b 112 \ + -i "Midi-Bridge:nanoKEY Studio 1:(capture_0) nanoKEY Studio nanoKEY Studio _" \ + -l "Firefox:output_FL" \ + -r "Firefox:output_FR" \ + -L "Komplete Audio 6 Pro:playback_AUX1" \ + -R "Komplete Audio 6 Pro:playback_AUX2" sequencer: - {{debug}} {{name}} {{bpm}} sequencer + reset + cargo run --bin tek_sequencer sequencer-ext: - {{debug}} {{name}} {{bpm}} {{midi-in}} {{midi-out}} sequencer + reset + cargo run --bin tek_sequencer -- \ + -i "Midi-Bridge:nanoKEY Studio 1:(capture_0) nanoKEY Studio nanoKEY Studio _" \ + -o "Midi-Bridge:Komplete Audio 6 0:(playback_0) Komplete Audio 6 MIDI 1" sequencer-release: - {{release}} {{name}} {{bpm}} sequencer + reset + cargo run --release --bin tek_sequencer sequencer-release-ext: - {{release}} {{name}} {{bpm}} {{midi-in}} {{midi-out}} sequencer + reset + cargo run --release --bin tek_sequencer -- \ + -i "Midi-Bridge:nanoKEY Studio 1:(capture_0) nanoKEY Studio nanoKEY Studio _" \ + -o "Midi-Bridge:Komplete Audio 6 0:(playback_0) Komplete Audio 6 MIDI 1" mixer: - {{debug}} mixer + reset + cargo run --bin tek_mixer track: - {{debug}} track + reset + cargo run --bin tek_track sampler: - {{debug}} sampler + reset + cargo run --bin tek_sampler plugin: - {{debug}} plugin + reset + cargo run --bin tek_plugin diff --git a/LICENSE b/LICENSE index be3f7b28..4b61f9fc 100644 --- a/LICENSE +++ b/LICENSE @@ -1,661 +1,8 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU Affero General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. +0. The attached collection of letters, numbers, punctuation and other characters will be + collectively referred to as "the work". +1. The work exists as-is. It is composed as an extended meditation on the futility of computing. + No implication is made that the work compiles, executes, or that it is good for anything + whatsoever. +2. You may not copy, modify, or distribute the work for any purpose. +3. You may not affirm to third parties that the work exists, that you are its "author", + or that the "author" of the work exists. diff --git a/README.md b/README.md index 64655591..710719a0 100644 --- a/README.md +++ b/README.md @@ -10,81 +10,81 @@ for [jack](https://jackaudio.org/) and [pipewire](https://www.pipewire.org/). [statically linked binaries](https://codeberg.org/unspeaker/tek/releases), and on the [aur](https://codeberg.org/unspeaker/tek#arch-linux). -author is reachable via [**mastodon** `@unspeaker@mastodon.social`](https://mastodon.social/@unspeaker) +hmu on [**mastodon** `@unspeaker@mastodon.social`](https://mastodon.social/@unspeaker) or [**matrix** `@unspeaker:matrix.org`](https://matrix.to/#/@unspeaker:matrix.org) -| | | -|-|-| -|![Screenshot of Arranger Mode](https://codeberg.org/unspeaker/tek/attachments/5014ff4d-9ece-4862-90de-3bc6573eacf6)|![Screenshot of Groovebox Mode](https://codeberg.org/unspeaker/tek/attachments/bbc12eda-1d5e-4e0f-9474-f585128255a5)
![Screenshot of Help in Groovebox Mode](https://codeberg.org/unspeaker/tek/attachments/d8963b84-8183-4c05-b77b-349a4c4c6161)| +![Screenshot](https://codeberg.org/unspeaker/tek/attachments/549efab7-f46b-438b-9508-cd499d044b41) -## usage +this codebase produces the following binaries: + +* **`tek_sequencer`** is a single-track, multi-pattern MIDI sequencer with properly tempo-synced pattern switch +* **`tek_groovebox`** connects the sequencer to a sampler, so that you can sample while you sequence +* **`tek_arranger`** is a multi-track sequencer based on a familiar clip launching UI +* **`tek_transport`** is a JACK transport controller +* **`tek_sampler`** is a MIDI-controlled sampler +* **`tek_plugin`** is an audio plugin host. +* **`tek_channel`** is a standalone channel strip +* **`tek_mixer`** is an audio mixer. + +some of these are currently work in progress. + +the project roadmap is at https://codeberg.org/unspeaker/tek/milestones + +## getting started * **requirements:** linux; jack or pipewire; 24-bit terminal (i use `kitty`) * **recommended:** midi controller; samples in wav format; lv2 plugins. -## keymaps +### arch linux -* Arranger: - * [x] arrows: navigate - * [x] tab: enter editor - * [x] `q`: enqueue clip - * [x] space: play/pause -* Editor: - * [x] arrows: navigate - * [x] `,` / `.`: change note length - * [x] enter: write note - * [x] `-` / `=`: zoom midi editor - * [ ] `z`: zoom lock/unlock - * [ ] del: delete -* Global: - * [x] esc: options menu - * [x] f1: help/command list - * [ ] f2: rename - * [ ] f6: save - * [ ] f9: load - -## installation - -### binary download - -you can download [tek 0.2.0 "almost static"](https://codeberg.org/unspeaker/tek/releases/tag/0.2.0) -from codeberg releases. this standalone binary release, should work on any glibc-based system. - -### from distro repositories - -[![Packaging status](https://repology.org/badge/vertical-allrepos/tek.svg)](https://repology.org/project/tek/versions) - -#### arch linux - -[tek 0.2.0-rc7](https://aur.archlinux.org/packages/tek) is available as a package in the AUR. -you can install it using your preferred AUR helper (e.g. `paru`): +[tek](https://codeberg.org/unspeaker/tek) is available as a package in the AUR. +you can install it using an AUR helper (e.g. `paru`): ```sh paru -S tek ``` +### downloads + +see the [releases page](https://codeberg.org/unspeaker/tek/releases). + ### building from source -requires docker. +you'll need a Rust toolchain and various system libraries. -``` -git clone --recursive -b 0.2 https://codeberg.org/unspeaker/tek -cd tek # enter directory -cat bin/release-glibc.sh # preview build script -sudo bin/release-glibc.sh # run build script -sudo cp bin/tek /usr/local/bin/tek # install +you can obtain the former using `rustup` and the latter using `nix-shell`. +there's a `shell.nix` provided with the project. + +from there, use the commands in the `Justfile`, e.g.: + +```sh +just arranger ``` ## design goals -* inspired by trackers and hardware sequencers, - but with the critical feature that 90s samplers lack: - able to **resample, i.e. record while playing!** +### lightweight -* **pop-up scratchpad for musical ideas.** - low resource consumption, can stay open in background. - but flexible enough to allow expanding on compositions +* pop-up scratchpad for musical ideas +* low resource consumption, can stay open in background +* advanced toolset allows quickly expanding on compositions -* **human- and machine- readable project format** - simple representation for project data - enable scripting and remapping. +### flexible + +* inspired by trackers and hardware sequencers +* able to record while playing! + +### programmable + +* human-readable project format +* command architecture allows for scripting and remapping + +--- + +> [!NOTE] +> Your moral support means a lot to me. +> Feel free to [contact me on Mastodon](https://mastodon.social/@unspeaker)![^0] +> +> Love, +> 🤫 +> (a rogue knowledge worker in a cyberpunk dystopia) diff --git a/app/Cargo.toml b/app/Cargo.toml deleted file mode 100644 index d6c3afa9..00000000 --- a/app/Cargo.toml +++ /dev/null @@ -1,58 +0,0 @@ -[package] -name = "tek" -edition = { workspace = true } -version = { workspace = true } - -[lib] -path = "tek.rs" - -[[bin]] -name = "tek" -path = "tek_cli.rs" - -[target.'cfg(target_os = "linux")'] -rustflags = ["-C", "link-arg=-fuse-ld=mold"] - -[dependencies] -tek_device = { path = "../device" } - -atomic_float = { workspace = true } -backtrace = { workspace = true } -clap = { workspace = true, optional = true } -jack = { workspace = true } -konst = { workspace = true } -livi = { workspace = true, optional = true } -midly = { workspace = true } -palette = { workspace = true } -rand = { workspace = true } -symphonia = { workspace = true, optional = true } -tengri = { workspace = true } -toml = { workspace = true } -uuid = { workspace = true, optional = true } -wavers = { workspace = true, optional = true } -winit = { workspace = true, optional = true } -xdg = { workspace = true } - -[dev-dependencies] -proptest = { workspace = true } -proptest-derive = { workspace = true } - -[features] -arranger = ["port", "editor", "sequencer", "editor"] -browse = [] -clap = [] -cli = ["dep:clap"] -clock = [] -default = ["cli", "arranger", "sampler", "lv2"] -editor = [] -host = ["lv2"] -lv2 = ["port", "livi", "winit"] -meter = [] -mixer = [] -pool = [] -port = [] -sampler = ["port", "meter", "mixer", "browse", "symphonia", "wavers"] -sequencer = ["port", "clock", "uuid", "pool"] -sf2 = [] -vst2 = [] -vst3 = [] diff --git a/app/tek.edn b/app/tek.edn deleted file mode 100644 index 17b02a6c..00000000 --- a/app/tek.edn +++ /dev/null @@ -1,224 +0,0 @@ -(keys :axis/x - (@left x/dec) - (@right x/inc)) -(keys :axis/x2 - (@shift/left x2/dec) - (@shift/right x2/inc)) -(keys :axis/y - (@up y/dec) - (@down y/inc)) -(keys :axis/y2 - (@shift/up y2/dec) - (@shift/down y2/inc)) -(keys :axis/z - (@minus z/dec) - (@equal z/inc)) -(keys :axis/z2 - (@underscore z2/dec) - (@plus z2/inc)) -(keys :axis/i - (@comma i/dec) - (@period z/inc)) -(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 (keys :axis/y :confirm) :menu) - -(keys :confirm - (@enter confirm)) - -(view :menu (bg (g 0) (bsp/s - :ports/out - (bsp/n :ports/in (bg (g 30) (bsp/s (fixed/y 7 :logo) (fill :dialog/menu))))))) - -(view :menu (bsp/s - (push/y 4 (fixed/xy 20 2 (bg (g 0) :debug))) - (fixed 20 2 (bg (g 20) (push/x 2 :debug))))) - -(view :menu (bsp/s (fixed/y 4 :debug) :debug)) - -(view :ports/out (fill/x (fixed/y 3 (bsp/a - (fill/x (align/w (text L-AUDIO-OUT))) - (bsp/a (text MIDI-OUT) (fill/x (align/e (text AUDIO-OUT-R)))))))) - -(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)))))))) - -(view :browse (bsp/s - (padding 3 1 :browse-title) - (enclose (fg (g 96)) browser))) - -(keys :help - (@f1 dialog :help)) - -(keys :back - (@escape back)) - -(keys :page - (@pgup page/up) - (@pgdn page/down)) - -(keys :delete - (@delete delete) - (@backspace delete/back)) - -(keys :input (see :axis/x :delete) - (:char input)) - -(keys :list (see :axis/y :confirm)) - -(keys :length (see :axis/x :axis/y :confirm)) - -(keys :browse (see :list :input :focus)) - -(keys :history - (@u undo 1) - (@r redo 1)) - -(keys :clock - (@space clock/toggle 0) - (@shift/space clock/toggle 0)) - -(keys :color - (@c color)) - -(keys :launch - (@q launch)) - -(keys :saveload - (@f6 dialog :save) - (@f9 dialog :load)) - -(keys :global (see :history :saveload) - (@f8 dialog :options) - (@f10 dialog :quit)) - -(keys :focus) - -(mode :transport (name Transport) (info A JACK transport controller.) (keys :clock :global) - (view :transport)) - -(mode :arranger (name Arranger) (info A grid of launchable clips arranged by track and scene.) - (mode :editor (keys :editor)) (mode :dialog (keys :dialog)) (mode :message (keys :message)) - (mode :add-device (keys :add-device)) (mode :browse (keys :browse)) (mode :rename (keys :input)) - (mode :length (keys :rename)) (mode :clip (keys :clip)) (mode :track (keys :track)) - (mode :scene (keys :scene)) (mode :mix (keys :mix)) - (keys :clock :arranger :global) :arranger) - -(view :arranger (bsp/n - :status - (bsp/w :meters/output (bsp/e :meters/input :arrangement)))) - -(view :arrangement (bsp/n - :tracks/inputs - (bsp/s :tracks/outputs (bsp/s :tracks/names (bsp/s :tracks/devices - (fill (either :mode/editor (bsp/e :scenes/names :editor) :scenes))))))) - -(keys :arranger (see :color :launch :scenes :tracks) - (@tab project/edit) (@enter project/edit) - (@shift/I project/input/add) (@shift/O project/output/add) - (@shift/S project/scene/add) (@shift/T project/track/add) - (@shift/D dialog/show :dialog/device)) - -(keys :tracks - (@t select :select/track) - (@left select :select/track/dec) - (@right select :select/track/inc)) - -(keys :scenes - (@s select :select/scene) - (@up select :select/scene/dec) - (@down select :select/scene/inc)) - -(keys :track (see :color :launch :axis/z :axis/z2 :delete) - (@r toggle :rec) - (@m toggle :mon) - (@p toggle :play) - (@P toggle :solo)) -(keys :scene (see :color :launch :axis/z :axis/z2 :delete)) - -(keys :clip (see :color :launch :axis/z :axis/z2 :delete) - (@l toggle :loop)) - -(mode :groovebox (name Groovebox) (info A sequencer with built-in sampler.) - (mode browse (keys :browse)) - (mode rename (keys :pool-rename)) - (mode length (keys :pool-length)) - (keys :clock :editor :sampler :global) (view :groovebox)) - -(view :groovebox (bsp/w - :meters/output - (bsp/e :meters/input (bsp/w :groove/meta :groove/editor)))) - -(view :groove/meta (fill/y (align/n (stack/s - :midi-ins/status :midi-outs/status :audio-ins/status :audio-outs/status :pool)))) - -(view :groove/editor (bsp/n - :groove/sample - :groove/sequence)) - -(view :groove/sample (fixed/y :h-sample-detail (bsp/e - (fill/y (fixed/x 20 (align/nw :sample-status))) - :sample-viewer))) - -(view :groove/sequence (bsp/e - (fill/y (align/n (bsp/s :status/v :editor-status))) - (bsp/e :samples/keys :editor))) - -(mode :sampler (name Sampler) (info A sampling soundboard.) - (keys :sampler :global) (view :sampler)) - -(view :sampler (bsp/s - (fixed/y 1 :transport) - (bsp/n (fixed/y 1 :status) (fill :samples/grid)))) - -(keys :sampler (see :sampler/directions :sampler/record :sampler/play)) - -(keys :sampler/record - (@r sampler/record/toggle :sample/selected) (@shift/R sampler/record/back)) - -(keys :sampler/play - (@p sampler/play/sample :sample/selected) (@P sampler/stop/sample :sample/selected)) - -(keys :sampler/import-export - (@shift/f6 dialog :dialog/export/sample) (@shift/f9 dialog :dialog/import/sample)) - -(keys :sampler/directions - (@up sampler/select :sample/above) - (@down sampler/select :sample/below) - (@left sampler/select :sample/to/left) - (@right sampler/select :sample/to/right)) - -(mode :sequencer (name Sequencer) (info A MIDI sequencer.) - (mode browse (keys :browse)) (mode rename (keys :pool/rename)) (mode length (keys :pool/length)) - (keys :editor :clock :global) (view :sequencer)) - -(view :sequencer (bsp/s - (fixed/y 1 :transport) - (bsp/n (fixed/y 1 :status) (fill (bsp/a (fill/xy (align/e :pool)) :editor))))) - -(keys :editor (see :editor/view :editor/note)) - -(keys :editor/view (see :axis/x :axis/x2 :axis/z :axis/z2) - (@z toggle :lock)) - -(keys :editor/note (see :axis/i :axis/i2 :axis/y :page) - (@a editor/append :true) - (@enter editor/append :false) - (@del editor/delete/note) - (@shift/del editor/delete/note)) - -(keys :pool (see :axis-y :axis-w :axis/z2 :color :delete) - (@n rename/begin) (@t length/begin) (@m import/begin) (@x export/begin) - (@shift/A clip/add :after :new/clip) (@shift/D clip/add :after :cloned/clip)) - -(keys :sequencer (see :color :launch) - (@shift/I input/add) (@shift/O output/add)) diff --git a/app/tek.rs b/app/tek.rs deleted file mode 100644 index 1ce0c252..00000000 --- a/app/tek.rs +++ /dev/null @@ -1,468 +0,0 @@ -#![feature( - adt_const_params, - associated_type_defaults, - closure_lifetime_binder, - if_let_guard, - impl_trait_in_assoc_type, - trait_alias, - type_alias_impl_trait, - type_changing_struct_update, -)] - -#![allow( - clippy::unit_arg -)] - -#[cfg(test)] mod tek_test; - -mod tek_bind; pub use self::tek_bind::*; -mod tek_cfg; pub use self::tek_cfg::*; -mod tek_deps; pub use self::tek_deps::*; -mod tek_mode; pub use self::tek_mode::*; -mod tek_view; pub use self::tek_view::*; - -/// Total state -#[derive(Default, Debug)] -pub struct App { - /// Base color. - pub color: ItemTheme, - /// Must not be dropped for the duration of the process - pub jack: Jack<'static>, - /// Display size - pub size: Measure, - /// Performance counter - pub perf: PerfModel, - /// Available view modes and input bindings - pub config: Config, - /// Currently selected mode - pub mode: Arc>>, - /// Undo history - pub history: Vec<(AppCommand, Option)>, - /// Dialog overlay - pub dialog: Dialog, - /// Contains all recently created clips. - pub pool: Pool, - /// Contains the currently edited musical arrangement - pub project: Arrangement, -} - -audio!( - |self: App, client, scope|{ - let t0 = self.perf.get_t0(); - self.clock().update_from_scope(scope).unwrap(); - let midi_in = self.project.midi_input_collect(scope); - if let Some(editor) = &self.editor() { - let mut pitch: Option = None; - for port in midi_in.iter() { - for event in port.iter() { - if let (_, Ok(LiveEvent::Midi {message: MidiMessage::NoteOn {key, ..}, ..})) - = event - { - pitch = Some(key.clone()); - } - } - } - if let Some(pitch) = pitch { - editor.set_note_pos(pitch.as_int() as usize); - } - } - let result = self.project.process_tracks(client, scope); - self.perf.update_from_jack_scope(t0, scope); - result - }; - |self, event|{ - use JackEvent::*; - match event { - SampleRate(sr) => { self.clock().timebase.sr.set(sr as f64); }, - PortRegistration(_id, true) => { - //let port = self.jack().port_by_id(id); - //println!("\rport add: {id} {port:?}"); - //println!("\rport add: {id}"); - }, - PortRegistration(_id, false) => { - /*println!("\rport del: {id}")*/ - }, - PortsConnected(_a, _b, true) => { /*println!("\rport conn: {a} {b}")*/ }, - PortsConnected(_a, _b, false) => { /*println!("\rport disc: {a} {b}")*/ }, - ClientRegistration(_id, true) => {}, - ClientRegistration(_id, false) => {}, - ThreadInit => {}, - XRun => {}, - GraphReorder => {}, - _ => { panic!("{event:?}"); } - } - } -); - -// Allow source to be read as Literal string -dsl_ns!(App: Arc { - literal = |dsl|Ok(dsl.src()?.map(|x|x.into())); -}); - -// Provide boolean values. -dsl_ns!(App: bool { - // TODO literal = ... - word = |app| { - ":mode/editor" => app.project.editor.is_some(), - ":focused/dialog" => !matches!(app.dialog, Dialog::None), - ":focused/message" => matches!(app.dialog, Dialog::Message(..)), - ":focused/add_device" => matches!(app.dialog, Dialog::Device(..)), - ":focused/browser" => app.dialog.browser().is_some(), - ":focused/pool/import" => matches!(app.pool.mode, Some(PoolMode::Import(..))), - ":focused/pool/export" => matches!(app.pool.mode, Some(PoolMode::Export(..))), - ":focused/pool/rename" => matches!(app.pool.mode, Some(PoolMode::Rename(..))), - ":focused/pool/length" => matches!(app.pool.mode, Some(PoolMode::Length(..))), - ":focused/clip" => !app.editor_focused() && matches!(app.selection(), - Selection::TrackClip{..}), - ":focused/track" => !app.editor_focused() && matches!(app.selection(), - Selection::Track(..)), - ":focused/scene" => !app.editor_focused() && matches!(app.selection(), - Selection::Scene(..)), - ":focused/mix" => !app.editor_focused() && matches!(app.selection(), - Selection::Mix), - }; -}); - -// TODO: provide colors here -dsl_ns!(App: ItemTheme {}); - -dsl_ns!(App: Dialog { - word = |app| { - ":dialog/none" => Dialog::None, - ":dialog/options" => Dialog::Options, - ":dialog/device" => Dialog::Device(0), - ":dialog/device/prev" => Dialog::Device(0), - ":dialog/device/next" => Dialog::Device(0), - ":dialog/help" => Dialog::Help(0), - ":dialog/save" => Dialog::Browse(BrowseTarget::SaveProject, - Browse::new(None).unwrap().into()), - ":dialog/load" => Dialog::Browse(BrowseTarget::LoadProject, - Browse::new(None).unwrap().into()), - ":dialog/import/clip" => Dialog::Browse(BrowseTarget::ImportClip(Default::default()), - Browse::new(None).unwrap().into()), - ":dialog/export/clip" => Dialog::Browse(BrowseTarget::ExportClip(Default::default()), - Browse::new(None).unwrap().into()), - ":dialog/import/sample" => Dialog::Browse(BrowseTarget::ImportSample(Default::default()), - Browse::new(None).unwrap().into()), - ":dialog/export/sample" => Dialog::Browse(BrowseTarget::ExportSample(Default::default()), - Browse::new(None).unwrap().into()), - }; -}); - -dsl_ns!(App: Selection { - word = |app| { - ":select/scene" => app.selection().select_scene(app.tracks().len()), - ":select/scene/next" => app.selection().select_scene_next(app.scenes().len()), - ":select/scene/prev" => app.selection().select_scene_prev(), - ":select/track" => app.selection().select_track(app.tracks().len()), - ":select/track/next" => app.selection().select_track_next(app.tracks().len()), - ":select/track/prev" => app.selection().select_track_prev(), - }; -}); - -dsl_ns!(App: Color { - word = |app| { - ":color/bg" => Color::Rgb(28, 32, 36), - }; - expr = |app| { - "g" (n: u8) => Color::Rgb(n, n, n), - "rgb" (r: u8, g: u8, b: u8) => Color::Rgb(r, g, b), - }; -}); - -dsl_ns!(App: Option { - word = |app| { - ":editor/pitch" => Some( - (app.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into() - ) - }; -}); - -dsl_ns!(App: Option { - word = |app| { - ":selected/scene" => app.selection().scene(), - ":selected/track" => app.selection().track(), - }; -}); - -dsl_ns!(App: Option>> { - word = |app| { - ":selected/clip" => if let Selection::TrackClip { track, scene } = app.selection() { - app.scenes()[*scene].clips[*track].clone() - } else { - None - } - }; -}); - -dsl_ns!(App: u8 { - literal = |dsl|Ok(if let Some(src) = dsl.src()? { - Some(to_number(src)? as u8) - } else { - None - }); -}); - -dsl_ns!(App: u16 { - literal = |dsl|Ok(if let Some(src) = dsl.src()? { - Some(to_number(src)? as u16) - } else { - None - }); - word = |app| { - ":w/sidebar" => app.project.w_sidebar(app.editor().is_some()), - ":h/sample-detail" => 6.max(app.height() as u16 * 3 / 9), - }; -}); - -dsl_ns!(App: usize { - literal = |dsl|Ok(if let Some(src) = dsl.src()? { - Some(to_number(src)? as usize) - } else { - None - }); - word = |app| { - ":scene-count" => app.scenes().len(), - ":track-count" => app.tracks().len(), - ":device-kind" => app.dialog.device_kind().unwrap_or(0), - ":device-kind/next" => app.dialog.device_kind_next().unwrap_or(0), - ":device-kind/prev" => app.dialog.device_kind_prev().unwrap_or(0), - }; -}); - -dsl_ns!(App: isize { - literal = |dsl|Ok(if let Some(src) = dsl.src()? { - Some(to_number(src)? as isize) - } else { - None - }); -}); - -has!(Jack<'static>: |self: App|self.jack); -has!(Pool: |self: App|self.pool); -has!(Dialog: |self: App|self.dialog); -has!(Clock: |self: App|self.project.clock); -has!(Option: |self: App|self.project.editor); -has!(Selection: |self: App|self.project.selection); -has!(Vec: |self: App|self.project.midi_ins); -has!(Vec: |self: App|self.project.midi_outs); -has!(Vec: |self: App|self.project.scenes); -has!(Vec: |self: App|self.project.tracks); -has!(Measure: |self: App|self.size); -has_clips!( |self: App|self.pool.clips); -maybe_has!(Track: |self: App| { MaybeHas::::get(&self.project) }; - { MaybeHas::::get_mut(&mut self.project) }); -maybe_has!(Scene: |self: App| { MaybeHas::::get(&self.project) }; - { MaybeHas::::get_mut(&mut self.project) }); - -impl HasClipsSize for App { - fn clips_size (&self) -> &Measure { &self.project.size_inner } -} -impl HasTrackScroll for App { - fn track_scroll (&self) -> usize { self.project.track_scroll() } -} -impl HasSceneScroll for App { - fn scene_scroll (&self) -> usize { self.project.scene_scroll() } -} -impl HasJack<'static> for App { - fn jack (&self) -> &Jack<'static> { &self.jack } -} -impl ScenesView for App { - fn w_side (&self) -> u16 { 20 } - fn w_mid (&self) -> u16 { (self.width() as u16).saturating_sub(self.w_side()) } - fn h_scenes (&self) -> u16 { (self.height() as u16).saturating_sub(20) } -} - - -impl App { - - pub fn editor_focused (&self) -> bool { - false - } - - pub fn toggle_dialog (&mut self, mut dialog: Dialog) -> Dialog { - std::mem::swap(&mut self.dialog, &mut dialog); - dialog - } - - pub fn toggle_editor (&mut self, value: Option) { - //FIXME: self.editing.store(value.unwrap_or_else(||!self.is_editing()), Relaxed); - let value = value.unwrap_or_else(||!self.editor().is_some()); - if value { - // Create new clip in pool when entering empty cell - if let Selection::TrackClip { track, scene } = *self.selection() - && let Some(scene) = self.project.scenes.get_mut(scene) - && let Some(slot) = scene.clips.get_mut(track) - && slot.is_none() - && let Some(track) = self.project.tracks.get_mut(track) - { - let (index, mut clip) = self.pool.add_new_clip(); - // autocolor: new clip colors from scene and track color - let color = track.color.base.mix(scene.color.base, 0.5); - clip.write().unwrap().color = ItemColor::random_near(color, 0.2).into(); - if let Some(editor) = &mut self.project.editor { - editor.set_clip(Some(&clip)); - } - *slot = Some(clip.clone()); - //Some(clip) - } else { - //None - } - } else if let Selection::TrackClip { track, scene } = *self.selection() - && let Some(scene) = self.project.scenes.get_mut(scene) - && let Some(slot) = scene.clips.get_mut(track) - && let Some(clip) = slot.as_mut() - { - // Remove clip from arrangement when exiting empty clip editor - let mut swapped = None; - if clip.read().unwrap().count_midi_messages() == 0 { - std::mem::swap(&mut swapped, slot); - } - if let Some(clip) = swapped { - self.pool.delete_clip(&clip.read().unwrap()); - } - } - } - - pub fn browser (&self) -> Option<&Browse> { - if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None } - } - - pub fn device_pick (&mut self, index: usize) { - self.dialog = Dialog::Device(index); - } - - pub fn add_device (&mut self, index: usize) -> Usually<()> { - match index { - 0 => { - let name = self.jack.with_client(|c|c.name().to_string()); - let midi = self.project.track().expect("no active track").sequencer.midi_outs[0].port_name(); - let track = self.track().expect("no active track"); - let port = format!("{}/Sampler", &track.name); - let connect = Connect::exact(format!("{name}:{midi}")); - let sampler = if let Ok(sampler) = Sampler::new( - &self.jack, &port, &[connect], &[&[], &[]], &[&[], &[]] - ) { - self.dialog = Dialog::None; - Device::Sampler(sampler) - } else { - self.dialog = Dialog::Message("Failed to add device.".into()); - return Err("failed to add device".into()) - }; - let track = self.track_mut().expect("no active track"); - track.devices.push(sampler); - Ok(()) - }, - 1 => { - todo!(); - Ok(()) - }, - _ => unreachable!(), - } - } - - pub fn update_clock (&self) { - ViewCache::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80) - } -} - -/// Various possible dialog modes. -#[derive(Debug, Clone, Default, PartialEq)] -pub enum Dialog { - #[default] None, - Help(usize), - Menu(usize, MenuItems), - Device(usize), - Message(Arc), - Browse(BrowseTarget, Arc), - Options, -} - -#[derive(Debug, Clone, Default, PartialEq)] -pub struct MenuItems(pub Arc<[MenuItem]>); - -impl AsRef> for MenuItems { - fn as_ref (&self) -> &Arc<[MenuItem]> { - &self.0 - } -} - -#[derive(Clone)] -pub struct MenuItem( - /// Label - pub Arc, - /// Callback - pub ArcUsually<()> + Send + Sync>> -); - -impl Default for MenuItem { - fn default () -> Self { Self("".into(), Arc::new(Box::new(|_|Ok(())))) } -} - -impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) }); - -impl PartialEq for MenuItem { - fn eq (&self, other: &Self) -> bool { - self.0 == other.0 - } -} - -impl Dialog { - pub fn welcome () -> Self { - Self::Menu(1, MenuItems([ - MenuItem("Resume session".into(), Arc::new(Box::new(|_|Ok(())))), - MenuItem("Create new session".into(), Arc::new(Box::new(|app|Ok({ - app.dialog = Dialog::None; - app.mode = app.config.modes.clone().read().unwrap().get(":arranger").cloned().unwrap(); - })))), - MenuItem("Load old session".into(), Arc::new(Box::new(|_|Ok(())))), - ].into())) - } - pub fn menu_next (&self) -> Self { - match self { - Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.0.len()), items.clone()), - _ => Self::None - } - } - pub fn menu_prev (&self) -> Self { - match self { - Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.0.len()), items.clone()), - _ => Self::None - } - } - pub fn menu_selected (&self) -> Option { - if let Self::Menu(selected, _) = self { Some(*selected) } else { None } - } - pub fn device_kind (&self) -> Option { - if let Self::Device(index) = self { Some(*index) } else { None } - } - pub fn device_kind_next (&self) -> Option { - self.device_kind().map(|index|(index + 1) % device_kinds().len()) - } - pub fn device_kind_prev (&self) -> Option { - self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1))) - } - pub fn message (&self) -> Option<&str> { - todo!() - } - pub fn browser (&self) -> Option<&Arc> { - todo!() - } - pub fn browser_target (&self) -> Option<&BrowseTarget> { - todo!() - } -} - -/////////////////////////////////////////////////////////////////////////////////////////////////// -//has_editor!(|self: App|{ - //editor = self.editor; - //editor_w = { - //let size = self.size.w(); - //let editor = self.editor.as_ref().expect("missing editor"); - //let time_len = editor.time_len().get(); - //let time_zoom = editor.time_zoom().get().max(1); - //(5 + (time_len / time_zoom)).min(size.saturating_sub(20)).max(16) - //}; - //editor_h = 15; - //is_editing = self.editor.is_some(); -//}); diff --git a/app/tek_bind.rs b/app/tek_bind.rs deleted file mode 100644 index 84763b70..00000000 --- a/app/tek_bind.rs +++ /dev/null @@ -1,325 +0,0 @@ -use crate::*; - -pub type Binds = Arc, EventMap>>>>; - -/// A collection of input bindings. -#[derive(Debug)] -pub struct EventMap( - /// Map of each event (e.g. key combination) to - /// all command expressions bound to it by - /// all loaded input layers. - pub BTreeMap>> -); - -/// An input binding. -#[derive(Debug, Clone)] -pub struct Binding { - pub commands: Arc<[C]>, - pub condition: Option, - pub description: Option>, - pub source: Option>, -} - -/// Input bindings are only returned if this evaluates to true -#[derive(Clone)] -pub struct Condition(Arcbool + Send + Sync>>); - -/// Default is always empty map regardless if `E` and `C` implement [Default]. -impl Default for EventMap { - fn default () -> Self { Self(Default::default()) } -} - -impl EventMap { - /// Create a new event map - pub fn new () -> Self { - Default::default() - } - /// Add a binding to an owned event map. - pub fn def (mut self, event: E, binding: Binding) -> Self { - self.add(event, binding); - self - } - /// Add a binding to an event map. - pub fn add (&mut self, event: E, binding: Binding) -> &mut Self { - if !self.0.contains_key(&event) { - self.0.insert(event.clone(), Default::default()); - } - self.0.get_mut(&event).unwrap().push(binding); - self - } - /// Return the binding(s) that correspond to an event. - pub fn query (&self, event: &E) -> Option<&[Binding]> { - self.0.get(event).map(|x|x.as_slice()) - } - /// Return the first binding that corresponds to an event, considering conditions. - pub fn dispatch (&self, event: &E) -> Option<&Binding> { - self.query(event) - .map(|bb|bb.iter().filter(|b|b.condition.as_ref().map(|c|(c.0)()).unwrap_or(true)).next()) - .flatten() - } -} - -impl EventMap> { - pub fn load_into (binds: &Binds, name: &impl AsRef, body: &impl Dsl) -> Usually<()> { - println!("EventMap::load_into: {}: {body:?}", name.as_ref()); - let mut map = Self::new(); - body.each(|item|if item.expr().head() == Ok(Some("see")) { - // TODO - Ok(()) - } else if let Ok(Some(_word)) = item.expr().head().word() { - if let Some(key) = TuiEvent::from_dsl(item.expr()?.head()?)? { - map.add(key, Binding { - commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(), - condition: None, - description: None, - source: None - }); - Ok(()) - } else if Some(":char") == item.expr()?.head()? { - // TODO - return Ok(()) - } else { - return Err(format!("Config::load_bind: invalid key: {:?}", item.expr()?.head()?).into()) - } - } else { - return Err(format!("Config::load_bind: unexpected: {item:?}").into()) - })?; - binds.write().unwrap().insert(name.as_ref().into(), map); - Ok(()) - } -} - -impl Binding { - pub fn from_dsl (dsl: impl Dsl) -> Usually { - let command: Option = None; - let condition: Option = None; - let description: Option> = None; - let source: Option> = None; - if let Some(command) = command { - Ok(Self { commands: [command].into(), condition, description, source }) - } else { - Err(format!("no command in {dsl:?}").into()) - } - } -} - -impl_debug!(Condition |self, w| { write!(w, "*") }); - -handle!(TuiIn:|self: App, input|{ - let mut commands = vec![]; - for id in self.mode.keys.iter() { - if let Some(event_map) = self.config.binds.clone().read().unwrap().get(id.as_ref()) { - if let Some(bindings) = event_map.query(input.event()) { - for binding in bindings { - for command in binding.commands.iter() { - if let Some(command) = self.from(command)? as Option { - commands.push(command) - } - } - } - } - } - } - for command in commands.into_iter() { - let result = command.execute(self); - match result { - Ok(undo) => { - self.history.push((command, undo)); - }, - Err(e) => { - self.history.push((command, None)); - return Err(e) - } - } - } - Ok(None) -}); - -#[derive(Debug, Copy, Clone)] -pub enum Axis { X, Y, Z, I } - -impl<'a> DslNs<'a, AppCommand> for App {} -impl<'a> DslNsExprs<'a, AppCommand> for App {} -impl<'a> DslNsWords<'a, AppCommand> for App { - dsl_words!('a |app| -> AppCommand { - "x/inc" => AppCommand::Inc { axis: Axis::X }, - "x/dec" => AppCommand::Dec { axis: Axis::X }, - "y/inc" => AppCommand::Inc { axis: Axis::Y }, - "y/dec" => AppCommand::Dec { axis: Axis::Y }, - "confirm" => AppCommand::Confirm, - "cancel" => AppCommand::Cancel, - }); -} - -impl Default for AppCommand { fn default () -> Self { Self::Nop } } - -def_command!(AppCommand: |app: App| { - Nop => Ok(None), - Confirm => Ok(match &app.dialog { - Dialog::Menu(index, items) => { - let callback = items.0[*index].1.clone(); - callback(app)?; - None - }, - _ => todo!(), - }), - Cancel => todo!(), // TODO delegate: - Inc { axis: Axis } => Ok(match (&app.dialog, axis) { - (Dialog::None, _) => todo!(), - (Dialog::Menu(_, _), Axis::Y) => AppCommand::SetDialog { dialog: app.dialog.menu_next() } - .execute(app)?, - _ => todo!() - }), - Dec { axis: Axis } => Ok(match (&app.dialog, axis) { - (Dialog::None, _) => None, - (Dialog::Menu(_, _), Axis::Y) => AppCommand::SetDialog { dialog: app.dialog.menu_prev() } - .execute(app)?, - _ => todo!() - }), - SetDialog { dialog: Dialog } => { - swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog }) - }, -}); - - //AppCommand => { - //("x/inc" / - //("stop-all") => todo!(),//app.project.stop_all(), - //("enqueue", clip: Option>>) => todo!(), - //("history", delta: isize) => todo!(), - //("zoom", zoom: usize) => todo!(), - //("select", selection: Selection) => todo!(), - //("dialog" / command: DialogCommand) => todo!(), - //("project" / command: ArrangementCommand) => todo!(), - //("clock" / command: ClockCommand) => todo!(), - //("sampler" / command: SamplerCommand) => todo!(), - //("pool" / command: PoolCommand) => todo!(), - //("edit" / editor: MidiEditCommand) => todo!(), - //}; - - //DialogCommand; - - //ArrangementCommand; - - //ClockCommand; - - //SamplerCommand; - - //PoolCommand; - - //MidiEditCommand; - - -//take!(DialogCommand |state: App, iter|Take::take(&state.dialog, iter)); -//#[derive(Clone, Debug)] -//pub enum DialogCommand { - //Open { dialog: Dialog }, - //Close -//} - -//impl Command> for DialogCommand { - //fn execute (self, state: &mut Option) -> Perhaps { - //match self { - //Self::Open { dialog } => { - //*state = Some(dialog); - //}, - //Self::Close => { - //*state = None; - //} - //}; - //Ok(None) - //} -//} - -//dsl!(DialogCommand: |self: Dialog, iter|todo!()); -//Dsl::take(&mut self.dialog, iter)); - -//#[tengri_proc::command(Option)]//Nope. -//impl DialogCommand { - //fn open (dialog: &mut Option, new: Dialog) -> Perhaps { - //*dialog = Some(new); - //Ok(None) - //} - //fn close (dialog: &mut Option) -> Perhaps { - //*dialog = None; - //Ok(None) - //} -//} -// -//dsl_bind!(AppCommand: App { - //enqueue = |app, clip: Option>>| { todo!() }; - //history = |app, delta: isize| { todo!() }; - //zoom = |app, zoom: usize| { todo!() }; - //stop_all = |app| { app.tracks_stop_all(); Ok(None) }; - ////dialog = |app, command: DialogCommand| - ////Ok(command.delegate(&mut app.dialog, |c|Self::Dialog{command: c})?); - //project = |app, command: ArrangementCommand| - //Ok(command.delegate(&mut app.project, |c|Self::Project{command: c})?); - //clock = |app, command: ClockCommand| - //Ok(command.execute(app.clock_mut())?.map(|c|Self::Clock{command: c})); - //sampler = |app, command: SamplerCommand| - //Ok(app.project.sampler_mut().map(|s|command.delegate(s, |command|Self::Sampler{command})) - //.transpose()?.flatten()); - //pool = |app, command: PoolCommand| { - //let undo = command.clone().delegate(&mut app.pool, |command|AppCommand::Pool{command})?; - //// update linked editor after pool action - //match command { - //// autoselect: automatically load selected clip in editor - //PoolCommand::Select { .. } | - //// autocolor: update color in all places simultaneously - //PoolCommand::Clip { command: PoolClipCommand::SetColor { .. } } => { - //let clip = app.pool.clip().clone(); - //app.editor_mut().map(|editor|editor.set_clip(clip.as_ref())) - //}, - //_ => None - //}; - //Ok(undo) - //}; - //select = |app, selection: Selection| { - //*app.project.selection_mut() = selection; - ////todo! - ////if let Some(ref mut editor) = app.editor_mut() { - ////editor.set_clip(match selection { - ////Selection::TrackClip { track, scene } if let Some(Some(Some(clip))) = app - ////.project - ////.scenes.get(scene) - ////.map(|s|s.clips.get(track)) - ////=> - ////Some(clip), - ////_ => - ////None - ////}); - ////} - //Ok(None) - ////("select" [t: usize, s: usize] Some(match (t.expect("no track"), s.expect("no scene")) { - ////(0, 0) => Self::Select(Selection::Mix), - ////(t, 0) => Self::Select(Selection::Track(t)), - ////(0, s) => Self::Select(Selection::Scene(s)), - ////(t, s) => Self::Select(Selection::TrackClip { track: t, scene: s }) }))) - //// autoedit: load focused clip in editor. - //}; - ////fn color (app: &mut App, theme: ItemTheme) -> Perhaps { - ////Ok(app.set_color(Some(theme)).map(|theme|Self::Color{theme})) - ////} - ////fn launch (app: &mut App) -> Perhaps { - ////app.project.launch(); - ////Ok(None) - ////} - //toggle_editor = |app, value: bool|{ app.toggle_editor(Some(value)); Ok(None) }; - //editor = |app, command: MidiEditCommand| Ok(if let Some(editor) = app.editor_mut() { - //let undo = command.clone().delegate(editor, |command|AppCommand::Editor{command})?; - //// update linked sampler after editor action - //app.project.sampler_mut().map(|sampler|match command { - //// autoselect: automatically select sample in sampler - //MidiEditCommand::SetNotePos { pos } => { sampler.set_note_pos(pos); }, - //_ => {} - //}); - //undo - //} else { - //None - //}); -//}); -//take!(ClockCommand |state: App, iter|Take::take(state.clock(), iter)); -//take!(MidiEditCommand |state: App, iter|Ok(state.editor().map(|x|Take::take(x, iter)).transpose()?.flatten())); -//take!(PoolCommand |state: App, iter|Take::take(&state.pool, iter)); -//take!(SamplerCommand |state: App, iter|Ok(state.project.sampler().map(|x|Take::take(x, iter)).transpose()?.flatten())); -//take!(ArrangementCommand |state: App, iter|Take::take(&state.project, iter)); diff --git a/app/tek_cfg.rs b/app/tek_cfg.rs deleted file mode 100644 index ee323bb6..00000000 --- a/app/tek_cfg.rs +++ /dev/null @@ -1,65 +0,0 @@ -use crate::*; - -/// Configuration. -/// -/// Contains mode, view, and bind definitions. -#[derive(Default, Debug)] -pub struct Config { - pub dirs: BaseDirectories, - pub modes: Modes, - pub views: Views, - pub binds: Binds, -} - -impl Config { - const CONFIG: &'static str = "tek.edn"; - const DEFAULTS: &'static str = include_str!("./tek.edn"); - - pub fn new (dirs: Option) -> Self { - Self { - dirs: dirs.unwrap_or_else(||BaseDirectories::with_profile("tek", "v0")), - ..Default::default() - } - } - pub fn init (&mut self) -> Usually<()> { - self.init_file(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|cfgs.load(&dsl))?; - Ok(()) - } - pub fn init_file ( - &mut self, path: &str, defaults: &str, mut each: impl FnMut(&mut Self, &str)->Usually<()> - ) -> Usually<()> { - if self.dirs.find_config_file(path).is_none() { - println!("Creating {path:?}"); - std::fs::write(self.dirs.place_config_file(path)?, defaults)?; - } - Ok(if let Some(path) = self.dirs.find_config_file(path) { - println!("Loading {path:?}"); - let src = std::fs::read_to_string(&path)?; - src.as_str().each(move|item|each(self, item))?; - } else { - return Err(format!("{path}: not found").into()) - }) - } - pub fn load (&mut self, dsl: impl Dsl) -> Usually<()> { - dsl.each(|item|if let Some(expr) = item.expr()? { - let head = expr.head()?; - let tail = expr.tail()?; - let name = tail.head()?; - let body = tail.tail()?; - println!("Config::load: {} {} {}", head.unwrap_or_default(), name.unwrap_or_default(), body.unwrap_or_default()); - match head { - Some("mode") if let Some(name) = name => - Mode::>::load_into(&self.modes, &name, &body)?, - Some("keys") if let Some(name) = name => - EventMap::>::load_into(&self.binds, &name, &body)?, - Some("view") if let Some(name) = name => { - self.views.write().unwrap().insert(name.into(), body.src()?.unwrap_or_default().into()); - }, - _ => return Err(format!("Config::load: expected view/keys/mode, got: {item:?}").into()) - } - Ok(()) - } else { - return Err(format!("Config::load: expected expr, got: {item:?}").into()) - }) - } -} diff --git a/app/tek_cli.rs b/app/tek_cli.rs deleted file mode 100644 index 3562ecab..00000000 --- a/app/tek_cli.rs +++ /dev/null @@ -1,132 +0,0 @@ -pub(crate) use tek::*; -pub(crate) use clap::{self, Parser, Subcommand}; - -/// Application entrypoint. -pub fn main () -> Usually<()> { - Cli::parse().run() -} - -#[derive(Debug, Parser)] -#[command(name = "tek", version, about = Some(HEADER), long_about = Some(HEADER))] -pub struct Cli { - /// Pre-defined configuration modes. - /// - /// TODO: Replace these with scripted configurations. - #[command(subcommand)] mode: Option, - /// Name of JACK client - #[arg(short='n', long)] name: Option, - /// Whether to attempt to become transport master - #[arg(short='S', long, default_value_t = false)] sync_lead: bool, - /// Whether to sync to external transport master - #[arg(short='s', 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='t', 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, -} - -/// Application modes -#[derive(Debug, Clone, Subcommand)] -pub enum LaunchMode { - /// Create a new session instead of loading the previous one. - New, -} - -impl Cli { - pub fn run (&self) -> Usually<()> { - let name = self.name.as_ref().map_or("tek", |x|x.as_str()); - let tracks = vec![]; - let scenes = vec![]; - let empty = &[] as &[&str]; - let left_froms = Connect::collect(&self.left_from, empty, empty); - let left_tos = Connect::collect(&self.left_to, empty, empty); - let right_froms = Connect::collect(&self.right_from, empty, empty); - let right_tos = Connect::collect(&self.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()]; - let mut config = Config::new(None); - config.init()?; - Tui::new()?.run(&Jack::new_run(&name, move|jack|{ - let app = App { - jack: jack.clone(), - color: ItemTheme::random(), - dialog: Dialog::welcome(), - mode: config.modes.clone().read().unwrap().get(":menu").cloned().unwrap(), - config, - project: Arrangement { - name: Default::default(), - color: ItemTheme::random(), - jack: jack.clone(), - clock: Clock::new(&jack, self.bpm)?, - tracks, - scenes, - selection: Selection::TrackClip { track: 0, scene: 0 }, - midi_ins: { - let mut midi_ins = vec![]; - for (index, connect) in self.midi_froms().iter().enumerate() { - midi_ins.push(jack.midi_in(&format!("M/{index}"), &[connect.clone()])?); - } - midi_ins - }, - midi_outs: { - let mut midi_outs = vec![]; - for (index, connect) in self.midi_tos().iter().enumerate() { - midi_outs.push(jack.midi_out(&format!("{index}/M"), &[connect.clone()])?); - }; - midi_outs - }, - ..Default::default() - }, - ..Default::default() - }; - jack.sync_lead(self.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(self.sync_follow)?; - Ok(app) - })?) - } - fn midi_froms (&self) -> Vec { - Connect::collect(&self.midi_from, &[] as &[&str], &self.midi_from_re) - } - fn midi_tos (&self) -> Vec { - Connect::collect(&self.midi_to, &[] as &[&str], &self.midi_to_re) - } -} - -/// CLI header -const HEADER: &'static str = r#" -~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~ ~~~~~~ ~ ~~~ - ~~ ║ ~ ╟─╌ ~╟─< ~ v0.3.0, 2025 sum(m)er @ the nose of the cat. ~ -~~~ ╨ ~ ╙──╜ ╨ ╜ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ ~~ ~~ ~ ~~ - On first run, Tek will create configuration and state dirs: - * [x] ~/.config/tek - config - * [ ] ~/.local/share/tek - projects - * [ ] ~/.local/lib/tek - plugins - * [ ] ~/.cache/tek - cache -~"#; - -#[cfg(test)] #[test] fn test_cli () { - use clap::CommandFactory; - Cli::command().debug_assert(); - //let jack = Jack::default(); -} diff --git a/app/tek_deps.rs b/app/tek_deps.rs deleted file mode 100644 index b4af1e3f..00000000 --- a/app/tek_deps.rs +++ /dev/null @@ -1,38 +0,0 @@ -pub(crate) use ::{ - tek_device::{*, tek_engine::*}, - tengri::{ - Usually, Perhaps, Has, MaybeHas, has, maybe_has, impl_debug, from, - wrap_inc, wrap_dec, - dsl::*, - input::*, - output::*, - tui::{ - *, - ratatui::{ - self, - prelude::{Rect, Style, Stylize, Buffer, Modifier, buffer::Cell, Color::{self, *}}, - widgets::{Widget, canvas::{Canvas, Line}}, - }, - crossterm::{ - self, - event::{Event, KeyCode::{self, *}}, - }, - } - }, - std::{ - path::{Path, PathBuf}, - sync::{Arc, RwLock}, - sync::atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}, - error::Error, - collections::BTreeMap, - fmt::Write, - cmp::Ord, - ffi::OsString, - fmt::{Debug, Formatter}, - fs::File, - ops::{Add, Sub, Mul, Div, Rem}, - thread::JoinHandle - }, - xdg::BaseDirectories, - atomic_float::* -}; diff --git a/app/tek_mode.rs b/app/tek_mode.rs deleted file mode 100644 index f304b517..00000000 --- a/app/tek_mode.rs +++ /dev/null @@ -1,58 +0,0 @@ -use super::*; - -pub type Modes = Arc, Arc>>>>>; - -/// A set of currently active view and keys definitions, -/// with optional name and description. -#[derive(Default, Debug)] -pub struct Mode { - pub path: PathBuf, - pub name: Vec, - pub info: Vec, - pub view: Vec, - pub keys: Vec, - pub modes: Modes, -} - -impl Draw for Mode { - fn draw (&self, to: &mut TuiOut) { - self.content().draw(to) - } -} - -impl Mode> { - - pub fn load_into (modes: &Modes, name: &impl AsRef, body: &impl Dsl) -> Usually<()> { - let mut mode = Self::default(); - println!("Mode::load_into: {}: {body:?}", name.as_ref()); - body.each(|item|mode.load_one(item))?; - modes.write().unwrap().insert(name.as_ref().into(), Arc::new(mode)); - Ok(()) - } - - fn load_one (&mut self, dsl: impl Dsl) -> Usually<()> { - Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() { - println!("Mode::load_one: {head} {:?}", expr.tail()); - let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); - match head { - "name" => self.name.push(tail.into()), - "info" => self.info.push(tail.into()), - "view" => self.view.push(tail.into()), - "keys" => tail.each(|expr|{self.keys.push(expr.trim().into()); Ok(())})?, - "mode" => if let Some(id) = tail.head()? { - Self::load_into(&self.modes, &id, &tail.tail())?; - } else { - return Err(format!("Mode::load_one: self: incomplete: {expr:?}").into()); - }, - _ => { - return Err(format!("Mode::load_one: unexpected expr: {head:?} {tail:?}").into()) - }, - }; - } else if let Ok(Some(word)) = dsl.word() { - self.view.push(word.into()); - } else { - return Err(format!("Mode::load_one: unexpected: {dsl:?}").into()); - }) - } - -} diff --git a/app/tek_test.rs b/app/tek_test.rs deleted file mode 100644 index 71f604be..00000000 --- a/app/tek_test.rs +++ /dev/null @@ -1,103 +0,0 @@ -use crate::*; - -#[cfg(test)] #[test] fn test_app () -> Usually<()> { - let mut app = App::default(); - let _ = app.scene_add(None, None)?; - let _ = app.update_clock(); - Ok(()) -} - -#[cfg(test)] #[test] fn test_track () -> Usually<()> { - let track = Track::default(); - Ok(()) -} - -#[cfg(test)] #[test] fn test_scene () -> Usually<()> { - let scene = Scene::default(); - let _ = scene.pulses(); - let _ = scene.is_playing(&[]); - Ok(()) -} - -#[cfg(test)] #[test] fn test_view_layout () { - let _ = button_play_pause(true); - let _ = button_2("", "", true); - let _ = button_2("", "", false); - let _ = button_3("", "", "", true); - let _ = button_3("", "", "", false); - //let _ = heading("", "", 0, "", true); - //let _ = heading("", "", 0, "", false); - let _ = wrap(Reset, Reset, ""); -} - -#[cfg(test)] mod test_view_meter { - use super::*; - use proptest::prelude::*; - #[test] fn test_view_meter () { - let _ = view_meter("", 0.0); - let _ = view_meters(&[0.0, 0.0]); - } - proptest! { - #[test] fn proptest_view_meter ( - label in "\\PC*", value in f32::MIN..f32::MAX - ) { - let _ = view_meter(&label, value); - } - #[test] fn proptest_view_meters ( - value1 in f32::MIN..f32::MAX, - value2 in f32::MIN..f32::MAX - ) { - let _ = view_meters(&[value1, value2]); - } - } -} - -#[cfg(test)] #[test] fn test_view_iter () { - let mut app = App::default(); - app.project.editor = Some(Default::default()); - //let _: Vec<_> = app.project.inputs_with_sizes().collect(); - //let _: Vec<_> = app.project.outputs_with_sizes().collect(); - let _: Vec<_> = app.project.tracks_with_sizes().collect(); - //let _: Vec<_> = app.project.scenes_with_sizes(true, 10, 10).collect(); - //let _: Vec<_> = app.scenes_with_colors(true, 10).collect(); - //let _: Vec<_> = app.scenes_with_track_colors(true, 10, 10).collect(); -} - -#[cfg(test)] #[test] fn test_view_sizes () { - let app = App::default(); - let _ = app.project.w(); - //let _ = app.project.w_sidebar(); - //let _ = app.project.w_tracks_area(); - let _ = app.project.h(); - //let _ = app.project.h_tracks_area(); - //let _ = app.project.h_inputs(); - //let _ = app.project.h_outputs(); - let _ = app.project.h_scenes(); -} - -#[cfg(test)] #[test] fn test_midi_edit () { - let _editor = MidiEditor::default(); - let mut editor = MidiEditor { - mode: PianoHorizontal::new(Some(&Arc::new(RwLock::new(MidiClip::stop_all())))), - size: Default::default(), - //keys: Default::default(), - }; - let _ = editor.put_note(true); - let _ = editor.put_note(false); - let _ = editor.clip_status(); - let _ = editor.edit_status(); - struct TestEditorHost(Option); - has!(Option: |self: TestEditorHost|self.0); - //has_editor!(|self: TestEditorHost|{ - //editor = self.0; - //editor_w = 0; - //editor_h = 0; - //is_editing = false; - //}); - let mut host = TestEditorHost(Some(editor)); - let _ = host.editor(); - let _ = host.editor_mut(); - let _ = host.is_editing(); - let _ = host.editor_w(); - let _ = host.editor_h(); -} diff --git a/app/tek_view.rs b/app/tek_view.rs deleted file mode 100644 index 9198ca0c..00000000 --- a/app/tek_view.rs +++ /dev/null @@ -1,353 +0,0 @@ -use crate::*; - -pub type Views = Arc, Arc>>>; - -impl Draw for App { - fn draw (&self, to: &mut TuiOut) { - for (index, dsl) in self.mode.view.iter().enumerate() { - if let Err(e) = self.view(to, dsl) { - panic!("render #{index} failed ({e}): {dsl}"); - } - } - } -} - -impl View for App { - fn view_expr <'a> (&'a self, to: &mut TuiOut, expr: &'a impl DslExpr) -> Usually<()> { - if evaluate_output_expression(self, to, expr)? - || evaluate_output_expression_tui(self, to, expr)? { - Ok(()) - } else { - Err(format!("App::view_expr: unexpected: {expr:?}").into()) - } - } - fn view_word <'a> (&'a self, to: &mut TuiOut, dsl: &'a impl DslExpr) -> Usually<()> { - let mut frags = dsl.src()?.unwrap().split("/"); - match frags.next() { - Some(":logo") => to.place(&view_logo()), - Some(":status") => to.place(&Fixed::Y(1, "TODO: Status Bar")), - Some(":meters") => match frags.next() { - Some("input") => to.place(&Tui::bg(Rgb(30, 30, 30), Fill::Y(Align::s("Input Meters")))), - Some("output") => to.place(&Tui::bg(Rgb(30, 30, 30), Fill::Y(Align::s("Output Meters")))), - _ => panic!() - }, - Some(":tracks") => match frags.next() { - None => to.place(&"TODO tracks"), - Some("names") => to.place(&self.project.view_track_names(self.color.clone())),//Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Names")))), - Some("inputs") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Inputs")))), - Some("devices") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Devices")))), - Some("outputs") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Outputs")))), - _ => panic!() - }, - Some(":scenes") => match frags.next() { - None => to.place(&"TODO scenes"), - Some(":scenes/names") => to.place(&"TODO Scene Names"), - _ => panic!() - }, - Some(":editor") => to.place(&"TODO Editor"), - Some(":dialog") => match frags.next() { - Some("menu") => to.place(&if let Dialog::Menu(selected, items) = &self.dialog { - let items = items.clone(); - let selected = selected; - Some(Fill::XY(Thunk::new(move|to: &mut TuiOut|{ - for (index, MenuItem(item, _)) in items.0.iter().enumerate() { - to.place(&Push::Y((2 * index) as u16, - Tui::fg_bg( - if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) }, - if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) }, - Fixed::Y(2, Align::n(Fill::X(item))) - ))); - } - }))) - } else { - None - }), - _ => unimplemented!("App::view_word: {dsl:?} ({frags:?})"), - }, - Some(":templates") => to.place(&{ - let modes = self.config.modes.clone(); - let height = (modes.read().unwrap().len() * 2) as u16; - Fixed::Y(height, Min::X(30, Thunk::new(move |to: &mut TuiOut|{ - for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() { - let bg = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) }; - let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or(""); - let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or(""); - let fg1 = Rgb(224, 192, 128); - let fg2 = Rgb(224, 128, 32); - let field_name = Fill::X(Align::w(Tui::fg(fg1, name))); - let field_id = Fill::X(Align::e(Tui::fg(fg2, id))); - let field_info = Fill::X(Align::w(info)); - to.place(&Push::Y((2 * index) as u16, - Fixed::Y(2, Fill::X(Tui::bg(bg, Bsp::s( - Bsp::a(field_name, field_id), field_info)))))); - } - }))) - }), - Some(":sessions") => to.place(&Fixed::Y(6, Min::X(30, Thunk::new(|to: &mut TuiOut|{ - let fg = Rgb(224, 192, 128); - for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { - let bg = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; - to.place(&Push::Y((2 * index) as u16, - &Fixed::Y(2, Fill::X(Tui::bg(bg, Align::w(Tui::fg(fg, name))))))); - } - })))), - Some(":browse/title") => to.place(&Fill::X(Align::w(FieldV(ItemColor::default(), - match self.dialog.browser_target().unwrap() { - BrowseTarget::SaveProject => "Save project:", - BrowseTarget::LoadProject => "Load project:", - BrowseTarget::ImportSample(_) => "Import sample:", - BrowseTarget::ExportSample(_) => "Export sample:", - BrowseTarget::ImportClip(_) => "Import clip:", - BrowseTarget::ExportClip(_) => "Export clip:", - }, Shrink::X(3, Fixed::Y(1, Tui::fg(Tui::g(96), RepeatH("🭻")))))))), - Some(":device") => { - let selected = self.dialog.device_kind().unwrap(); - to.place(&Bsp::s(Tui::bold(true, "Add device"), Map::south(1, - move||device_kinds().iter(), - move|_label: &&'static str, i|{ - let bg = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }; - let lb = if i == selected { "[ " } else { " " }; - let rb = if i == selected { " ]" } else { " " }; - Fill::X(Tui::bg(bg, Bsp::e(lb, Bsp::w(rb, "FIXME device name")))) }))) - }, - Some(":debug") => to.place(&Fixed::Y(1, format!("[{:?}]", to.area()))), - Some(_) => { - let views = self.config.views.read().unwrap(); - if let Some(dsl) = views.get(dsl.src()?.unwrap()) { - let dsl = dsl.clone(); - std::mem::drop(views); - self.view(to, &dsl)? - } else { - unimplemented!("{dsl:?}"); - } - }, - _ => unreachable!() - } - Ok(()) - } -} - -fn view_logo () -> impl Content { - Fixed::XY(32, 7, Tui::bold(true, Tui::fg(Rgb(240,200,180), col!{ - Fixed::Y(1, ""), - Fixed::Y(1, ""), - Fixed::Y(1, "~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~"), - Fixed::Y(1, Bsp::e("~~~~ ║ ~ ╟─╌ ~╟─< ~~ ", Bsp::e(Tui::fg(Rgb(230,100,40), "v0.3.0"), " ~~"))), - Fixed::Y(1, "~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~"), - }))) -} - -//pub fn view_nil (_: &App) -> TuiCb { - //|to|to.place(&Fill::XY("·")) -//} - - //Bsp::s("", - //Map::south(1, - //move||app.config.binds.layers.iter() - //.filter_map(|a|(a.0)(app).then_some(a.1)) - //.flat_map(|a|a) - //.filter_map(|x|if let Value::Exp(_, iter)=x.value{ Some(iter) } else { None }) - //.skip(offset) - //.take(20), - //|mut b,i|Fixed::X(60, Align::w(Bsp::e("(", Bsp::e( - //b.next().map(|t|Fixed::X(16, Align::w(Tui::fg(Rgb(64,224,0), format!("{}", t.value))))), - //Bsp::e(" ", Align::w(format!("{}", b.0.0.trim()))))))))))), - - //Dialog::Browse(BrowseTarget::Load, browser) => { - //"bobcat".boxed() - ////Bsp::s( - ////Fill::X(Align::w(Margin::XY(1, 1, Bsp::e( - ////Tui::bold(true, " Load project: "), - ////Shrink::X(3, Fixed::Y(1, RepeatH("🭻"))))))), - ////Outer(true, Style::default().fg(Tui::g(96))) - ////.enclose(Fill::XY(browser))) - //}, - //Dialog::Browse(BrowseTarget::Export, browser) => { - //"bobcat".boxed() - ////Bsp::s( - ////Fill::X(Align::w(Margin::XY(1, 1, Bsp::e( - ////Tui::bold(true, " Export: "), - ////Shrink::X(3, Fixed::Y(1, RepeatH("🭻"))))))), - ////Outer(true, Style::default().fg(Tui::g(96))) - ////.enclose(Fill::XY(browser))) - //}, - //Dialog::Browse(BrowseTarget::Import, browser) => { - //"bobcat".boxed() - ////Bsp::s( - ////Fill::X(Align::w(Margin::XY(1, 1, Bsp::e( - ////Tui::bold(true, " Import: "), - ////Shrink::X(3, Fixed::Y(1, RepeatH("🭻"))))))), - ////Outer(true, Style::default().fg(Tui::g(96))) - ////.enclose(Fill::XY(browser))) - //}, -// - //pub fn view_history (&self) -> impl Content { - //Fixed::Y(1, Fill::X(Align::w(FieldH(self.color, - //format!("History ({})", self.history.len()), - //self.history.last().map(|last|Fill::X(Align::w(format!("{:?}", last.0)))))))) - //} - //pub fn view_status_h2 (&self) -> impl Content { - //self.update_clock(); - //let theme = self.color; - //let clock = self.clock(); - //let playing = clock.is_rolling(); - //let cache = clock.view_cache.clone(); - ////let selection = self.selection().describe(self.tracks(), self.scenes()); - //let hist_len = self.history.len(); - //let hist_last = self.history.last(); - //Fixed::Y(2, Stack::east(move|add: &mut dyn FnMut(&dyn Draw)|{ - //add(&Fixed::X(5, Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, - //Either::new(false, // TODO - //Thunk::new(move||Fixed::X(9, Either::new(playing, - //Tui::fg(Rgb(0, 255, 0), " PLAYING "), - //Tui::fg(Rgb(255, 128, 0), " STOPPED "))) - //), - //Thunk::new(move||Fixed::X(5, Either::new(playing, - //Tui::fg(Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), - //Tui::fg(Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",)))) - //) - //) - //))); - //add(&" "); - //{ - //let cache = cache.read().unwrap(); - //add(&Fixed::X(15, Align::w(Bsp::s( - //FieldH(theme, "Beat", cache.beat.view.clone()), - //FieldH(theme, "Time", cache.time.view.clone()), - //)))); - //add(&Fixed::X(13, Align::w(Bsp::s( - //Fill::X(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))), - //Fill::X(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))), - //)))); - //add(&Fixed::X(12, Align::w(Bsp::s( - //Fill::X(Align::w(FieldH(theme, "Buf", cache.buf.view.clone()))), - //Fill::X(Align::w(FieldH(theme, "Lat", cache.lat.view.clone()))), - //)))); - ////add(&Bsp::s( - //////Fill::X(Align::w(FieldH(theme, "Selected", Align::w(selection)))), - ////Fill::X(Align::w(FieldH(theme, format!("History ({})", hist_len), - ////hist_last.map(|last|Fill::X(Align::w(format!("{:?}", last.0))))))), - ////"" - ////)); - //////if let Some(last) = self.history.last() { - //////add(&FieldV(theme, format!("History ({})", self.history.len()), - //////Fill::X(Align::w(format!("{:?}", last.0))))); - //////} - //} - //})) - //} - //pub fn view_status_v (&self) -> impl Content + use<'_> { - //self.update_clock(); - //let cache = self.project.clock.view_cache.read().unwrap(); - //let theme = self.color; - //let playing = self.clock().is_rolling(); - //Tui::bg(theme.darker.rgb, Fixed::XY(20, 5, Outer(true, Style::default().fg(Tui::g(96))).enclose( - //col!( - //Fill::X(Align::w(Bsp::e( - //Align::w(Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, - //Either::new(false, // TODO - //Thunk::new(move||Fixed::X(9, Either::new(playing, - //Tui::fg(Rgb(0, 255, 0), " PLAYING "), - //Tui::fg(Rgb(255, 128, 0), " STOPPED "))) - //), - //Thunk::new(move||Fixed::X(5, Either::new(playing, - //Tui::fg(Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), - //Tui::fg(Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",)))) - //) - //) - //)), - //Bsp::s( - //FieldH(theme, "Beat", cache.beat.view.clone()), - //FieldH(theme, "Time", cache.time.view.clone()), - //), - //))), - //Fill::X(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))), - //Fill::X(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))), - //Fill::X(Align::w(FieldH(theme, "Buf", Bsp::e(cache.buf.view.clone(), Bsp::e(" = ", cache.lat.view.clone()))))), - //)))) - //} - //pub fn view_status (&self) -> impl Content + use<'_> { - //self.update_clock(); - //let cache = self.project.clock.view_cache.read().unwrap(); - //view_status(Some(self.project.selection.describe(self.tracks(), self.scenes())), - //cache.sr.view.clone(), cache.buf.view.clone(), cache.lat.view.clone()) - //} - //pub fn view_transport (&self) -> impl Content + use<'_> { - //self.update_clock(); - //let cache = self.project.clock.view_cache.read().unwrap(); - //view_transport(self.project.clock.is_rolling(), - //cache.bpm.view.clone(), cache.beat.view.clone(), cache.time.view.clone()) - //} - //pub fn view_editor (&self) -> impl Content + use<'_> { - //let bg = self.editor() - //.and_then(|editor|editor.clip().clone()) - //.map(|clip|clip.read().unwrap().color.darker) - //.unwrap_or(self.color.darker); - //Fill::XY(Tui::bg(bg.rgb, self.editor())) - //} - //pub fn view_editor_status (&self) -> impl Content + use<'_> { - //self.editor().map(|e|Fixed::X(20, Outer(true, Style::default().fg(Tui::g(96))).enclose( - //Fill::Y(Align::n(Bsp::s(e.clip_status(), e.edit_status())))))) - //} - //pub fn view_midi_ins_status (&self) -> impl Content + use<'_> { - //self.project.view_midi_ins_status(self.color) - //} - //pub fn view_midi_outs_status (&self) -> impl Content + use<'_> { - //self.project.view_midi_outs_status(self.color) - //} - //pub fn view_audio_ins_status (&self) -> impl Content + use<'_> { - //self.project.view_audio_ins_status(self.color) - //} - //pub fn view_audio_outs_status (&self) -> impl Content + use<'_> { - //self.project.view_audio_outs_status(self.color) - //} - //pub fn view_scenes (&self) -> impl Content + use<'_> { - //Bsp::e( - //Fixed::X(20, Align::nw(self.project.view_scenes_names())), - //self.project.view_scenes_clips(), - //) - //} - //pub fn view_scenes_names (&self) -> impl Content + use<'_> { - //self.project.view_scenes_names() - //} - //pub fn view_scenes_clips (&self) -> impl Content + use<'_> { - //self.project.view_scenes_clips() - //} - //pub fn view_tracks_inputs <'a> (&'a self) -> impl Content + use<'a> { - //Fixed::Y(1 + self.project.midi_ins.len() as u16, - //self.project.view_inputs(self.color)) - //} - //pub fn view_tracks_outputs <'a> (&'a self) -> impl Content + use<'a> { - //self.project.view_outputs(self.color) - //} - //pub fn view_tracks_devices <'a> (&'a self) -> impl Content + use<'a> { - //Fixed::Y(4, self.project.view_track_devices(self.color)) - //} - //pub fn view_tracks_names <'a> (&'a self) -> impl Content + use<'a> { - //Fixed::Y(2, self.project.view_track_names(self.color)) - //} - //pub fn view_pool (&self) -> impl Content + use<'_> { - //Fixed::X(20, Bsp::s( - //Fill::X(Align::w(FieldH(self.color, "Clip pool:", ""))), - //Fill::Y(Align::n(Tui::bg(Rgb(0, 0, 0), Outer(true, Style::default().fg(Tui::g(96))) - //.enclose(PoolView(&self.pool))))))) - //} - //pub fn view_samples_keys (&self) -> impl Content + use<'_> { - //self.project.sampler().map(|s|s.view_list(true, self.editor().unwrap())) - //} - //pub fn view_samples_grid (&self) -> impl Content + use<'_> { - //self.project.sampler().map(|s|s.view_grid()) - //} - //pub fn view_sample_viewer (&self) -> impl Content + use<'_> { - //self.project.sampler().map(|s|s.view_sample(self.editor().unwrap().get_note_pos())) - //} - //pub fn view_sample_info (&self) -> impl Content + use<'_> { - //self.project.sampler().map(|s|s.view_sample_info(self.editor().unwrap().get_note_pos())) - //} - //pub fn view_sample_status (&self) -> impl Content + use<'_> { - //self.project.sampler().map(|s|Outer(true, Style::default().fg(Tui::g(96))).enclose( - //Fill::Y(Align::n(s.view_sample_status(self.editor().unwrap().get_note_pos()))))) - //} - ////let options = ||["Projects", "Settings", "Help", "Quit"].iter(); - ////let option = |a,i|Tui::fg(Rgb(255,255,255), format!("{}", a)); - ////Bsp::s(Tui::bold(true, "tek!"), Bsp::s("", Map::south(1, options, option))) diff --git a/bacon.toml b/bacon.toml deleted file mode 100644 index 4af91cd0..00000000 --- a/bacon.toml +++ /dev/null @@ -1,64 +0,0 @@ -# https://dystroy.org/bacon/config/ -default_job = "test" -env.CARGO_TERM_COLOR = "always" -[keybindings] -c = "job:check" -t = "job:test" -n = "job:nextest" -l = "job:clippy" -[jobs] -[jobs.check] -command = ["cargo", "check"] -need_stdout = false -watch = ["deps", "engine", "device", "app"] -[jobs.check-all] -command = ["cargo", "check", "--all-targets"] -need_stdout = false -watch = ["deps", "engine", "device", "app"] -[jobs.clippy] -command = ["cargo", "clippy"] -need_stdout = false -watch = ["deps", "engine", "device", "app"] -[jobs.clippy-all] -command = ["cargo", "clippy", "--all-targets"] -need_stdout = false -watch = ["deps", "engine", "device", "app"] -[jobs.test] -command = ["cargo", "test", "--workspace", "--exclude", "jack"] -need_stdout = true -watch = ["deps", "engine", "device", "app"] -[jobs.nextest] -watch = ["deps", "engine", "device", "app"] -command = [ - "cargo", "nextest", "run", - "--hide-progress-bar", "--failure-output", "final" -] -need_stdout = true -analyzer = "nextest" -[jobs.doc] -command = ["cargo", "doc", "--no-deps"] -need_stdout = false -[jobs.doc-open] -command = ["cargo", "doc", "--no-deps", "--open"] -need_stdout = false -on_success = "back" # so that we don't open the browser at each change -[jobs.run] -command = [ - "cargo", "run", - # put launch parameters for your program behind a `--` separator -] -need_stdout = true -allow_warnings = true -background = true -[jobs.run-long] -watch = ["deps", "engine", "device", "app"] -command = [ "cargo", "run", ] -need_stdout = true -allow_warnings = true -background = false -on_change_strategy = "kill_then_restart" -[jobs.ex] -watch = ["deps", "engine", "device", "app"] -command = ["cargo", "run", "--example"] -need_stdout = true -allow_warnings = true diff --git a/bin/cli_arranger.rs b/bin/cli_arranger.rs new file mode 100644 index 00000000..a2b9d77a --- /dev/null +++ b/bin/cli_arranger.rs @@ -0,0 +1,126 @@ +include!("./lib.rs"); +use tek::ArrangerTui; +pub fn main () -> Usually<()> { ArrangerCli::parse().run() } + +/// Launches an interactive MIDI arranger. +#[derive(Debug, Parser)] +#[command(version, about, long_about = None)] +pub struct ArrangerCli { + /// Name of JACK client + #[arg(short, long)] + name: Option, + /// Whether to include a transport toolbar (default: true) + #[arg(short, long, default_value_t = true)] + transport: bool, + /// Number of tracks + #[arg(short = 'x', long, default_value_t = 4)] + tracks: usize, + /// Number of scenes + #[arg(short, long, default_value_t = 8)] + scenes: usize, + /// MIDI outs to connect each track to. + #[arg(short='i', long)] + midi_from: Vec, + /// MIDI ins to connect each track to. + #[arg(short='o', long)] + midi_to: Vec, +} + +impl ArrangerCli { + /// Run the arranger TUI from CLI arguments. + fn run (&self) -> Usually<()> { + let name = self.name.as_deref().unwrap_or("tek_arranger"); + let engine = Tui::new()?; + let state = JackConnection::new(name)?.activate_with(|jack|{ + let mut app = ArrangerTui::try_from(jack)?; + let jack = jack.read().unwrap(); + app.color = ItemPalette::random(); + add_tracks(&jack, &mut app, self)?; + add_scenes(&mut app, self.scenes)?; + Ok(app) + })?; + engine.run(&state) + } +} + +fn add_tracks (jack: &JackConnection, app: &mut ArrangerTui, cli: &ArrangerCli) -> Usually<()> { + let n = cli.tracks; + let track_color_1 = ItemColor::random(); + let track_color_2 = ItemColor::random(); + for i in 0..n { + let track = app.track_add(None, Some( + track_color_1.mix(track_color_2, i as f32 / n as f32).into() + ))?; + track.width = 8; + let name = track.name.read().unwrap(); + track.player.midi_ins.push( + jack.register_port(&format!("{}I", &name), MidiIn::default())? + ); + track.player.midi_outs.push( + jack.register_port(&format!("{}O", &name), MidiOut::default())? + ); + } + for connection in cli.midi_from.iter() { + let mut split = connection.split("="); + let number = split.next().unwrap().trim(); + if let Ok(track) = number.parse::() { + if track < 1 { + panic!("Tracks are zero-indexed") + } + if track > n { + panic!("Tried to connect track {track} or {n}. Pass -t {track} to increase number of tracks.") + } + if let Some(port) = split.next() { + if let Some(port) = jack.port_by_name(port).as_ref() { + jack.client().connect_ports(port, &app.tracks[track-1].player.midi_ins[0])?; + } else { + panic!("Missing MIDI output: {port}. Use jack_lsp to list all port names."); + } + } else { + panic!("No port specified for track {track}. Format is TRACK_NUMBER=PORT_NAME") + } + } else { + panic!("Failed to parse track number: {number}") + } + } + for connection in cli.midi_to.iter() { + let mut split = connection.split("="); + let number = split.next().unwrap().trim(); + if let Ok(track) = number.parse::() { + if track < 1 { + panic!("Tracks are zero-indexed") + } + if track > n { + panic!("Tried to connect track {track} or {n}. Pass -t {track} to increase number of tracks.") + } + if let Some(port) = split.next() { + if let Some(port) = jack.port_by_name(port).as_ref() { + jack.client().connect_ports(&app.tracks[track-1].player.midi_outs[0], port)?; + } else { + panic!("Missing MIDI input: {port}. Use jack_lsp to list all port names."); + } + } else { + panic!("No port specified for track {track}. Format is TRACK_NUMBER=PORT_NAME") + } + } else { + panic!("Failed to parse track number: {number}") + } + } + Ok(()) +} + +fn add_scenes (app: &mut ArrangerTui, n: usize) -> Usually<()> { + let scene_color_1 = ItemColor::random(); + let scene_color_2 = ItemColor::random(); + for i in 0..n { + let _scene = app.scene_add(None, Some( + scene_color_1.mix(scene_color_2, i as f32 / n as f32).into() + ))?; + } + Ok(()) +} + +#[test] fn verify_arranger_cli () { + use clap::CommandFactory; + ArrangerCli::command().debug_assert(); +} diff --git a/bin/cli_groovebox.rs b/bin/cli_groovebox.rs new file mode 100644 index 00000000..472916e3 --- /dev/null +++ b/bin/cli_groovebox.rs @@ -0,0 +1,76 @@ +include!("./lib.rs"); +pub fn main () -> Usually<()> { GrooveboxCli::parse().run() } +#[derive(Debug, Parser)] +#[command(version, about, long_about = None)] +pub struct GrooveboxCli { + /// Name of JACK client + #[arg(short, long)] + name: Option, + /// Whether to include a transport toolbar (default: true) + #[arg(short, long, default_value_t = true)] + transport: bool, + /// Whether to attempt to become transport master + #[arg(short='S', long, default_value_t = false)] + sync_lead: bool, + /// Whether to attempt to become transport master + #[arg(short='s', long, default_value_t = true)] + sync_follow: bool, + /// Default BPM + #[arg(short='b', long, default_value = None)] + bpm: Option, + /// MIDI outs to connect to MIDI input + #[arg(short='i', long)] + midi_from: Vec, + /// MIDI ins to connect from MIDI output + #[arg(short='o', long)] + midi_to: Vec, + /// Audio outs to connect to left input + #[arg(short='l', long)] + l_from: Vec, + /// Audio outs to connect to right input + #[arg(short='r', long)] + r_from: Vec, + /// Audio ins to connect from left output + #[arg(short='L', long)] + l_to: Vec, + /// Audio ins to connect from right output + #[arg(short='R', long)] + r_to: Vec, +} +impl GrooveboxCli { + fn run (&self) -> Usually<()> { + let name = self.name.as_deref().unwrap_or("tek_groovebox"); + let engine = Tui::new()?; + let state = JackConnection::new(name)?.activate_with(|jack|{ + let app = tek::Groovebox::new( + jack, + &self.midi_from.as_slice(), + &self.midi_to.as_slice(), + &[&self.l_from.as_slice(), &self.r_from.as_slice()], + &[&self.l_to.as_slice(), &self.r_to.as_slice()], + )?; + if let Some(bpm) = self.bpm { + app.clock().timebase.bpm.set(bpm); + } + if self.sync_lead { + jack.read().unwrap().client().register_timebase_callback(false, |mut state|{ + app.clock().playhead.update_from_sample(state.position.frame() as f64); + state.position.bbt = Some(app.clock().bbt()); + state.position + })? + } else if self.sync_follow { + jack.read().unwrap().client().register_timebase_callback(false, |state|{ + app.clock().playhead.update_from_sample(state.position.frame() as f64); + state.position + })? + } + Ok(app) + })?; + engine.run(&state) + } +} + +#[test] fn verify_groovebox_cli () { + use clap::CommandFactory; + GrooveboxCli::command().debug_assert(); +} diff --git a/bin/cli_sampler.rs b/bin/cli_sampler.rs new file mode 100644 index 00000000..d28bcf52 --- /dev/null +++ b/bin/cli_sampler.rs @@ -0,0 +1,48 @@ +include!("./lib.rs"); +pub fn main () -> Usually<()> { SamplerCli::parse().run() } +#[derive(Debug, Parser)] #[command(version, about, long_about = None)] pub struct SamplerCli { + /// Name of JACK client + #[arg(short, long)] name: Option, + /// Path to plugin + #[arg(short, long)] path: Option, + /// MIDI outs to connect to MIDI input + #[arg(short='i', long)] + midi_from: Vec, + /// Audio outs to connect to left input + #[arg(short='l', long)] + l_from: Vec, + /// Audio outs to connect to right input + #[arg(short='r', long)] + r_from: Vec, + /// Audio ins to connect from left output + #[arg(short='L', long)] + l_to: Vec, + /// Audio ins to connect from right output + #[arg(short='R', long)] + r_to: Vec, +} +impl SamplerCli { + fn run (&self) -> Usually<()> { + let name = self.name.as_deref().unwrap_or("tek_sampler"); + let engine = Tui::new()?; + let state = JackConnection::new(name)?.activate_with(|jack|{ + Ok(tek::SamplerTui { + cursor: (0, 0), + editing: None, + mode: None, + size: Measure::new(), + note_lo: 36.into(), + note_pt: 36.into(), + color: ItemPalette::from(Color::Rgb(64, 128, 32)), + state: Sampler::new( + jack, + &"sampler", + &self.midi_from.as_slice(), + &[&self.l_from.as_slice(), &self.r_from.as_slice()], + &[&self.l_to.as_slice(), &self.r_to.as_slice()], + )?, + }) + })?; + engine.run(&state) + } +} diff --git a/bin/cli_sequencer.rs b/bin/cli_sequencer.rs new file mode 100644 index 00000000..0e644967 --- /dev/null +++ b/bin/cli_sequencer.rs @@ -0,0 +1,47 @@ +include!("./lib.rs"); + +pub fn main () -> Usually<()> { + SequencerCli::parse().run() +} + +/// Launches a single interactive MIDI sequencer. +#[derive(Debug, Parser)] +#[command(version, about, long_about = None)] +pub struct SequencerCli { + /// Name of JACK client + #[arg(short, long)] + name: Option, + /// Whether to include a transport toolbar (default: true) + #[arg(short, long, default_value_t = true)] + transport: bool, + /// MIDI outs to connect to (multiple instances accepted) + #[arg(short='i', long)] + midi_from: Vec, + /// MIDI ins to connect to (multiple instances accepted) + #[arg(short='o', long)] + midi_to: Vec, +} + +impl SequencerCli { + fn run (&self) -> Usually<()> { + let name = self.name.as_deref().unwrap_or("tek_sequencer"); + let engine = Tui::new()?; + let state = JackConnection::new(name)?.activate_with(|jack|{ + let mut app = SequencerTui::try_from(jack)?; + let jack = jack.read().unwrap(); + let midi_in = jack.register_port("i", MidiIn::default())?; + let midi_out = jack.register_port("o", MidiOut::default())?; + connect_from(&jack, &midi_in, &self.midi_from)?; + connect_to(&jack, &midi_out, &self.midi_to)?; + app.player.midi_ins.push(midi_in); + app.player.midi_outs.push(midi_out); + Ok(app) + })?; + engine.run(&state) + } +} + +#[test] fn verify_sequencer_cli () { + use clap::CommandFactory; + SequencerCli::command().debug_assert(); +} diff --git a/bin/cli_transport.rs b/bin/cli_transport.rs new file mode 100644 index 00000000..2bee935c --- /dev/null +++ b/bin/cli_transport.rs @@ -0,0 +1,8 @@ +include!("./lib.rs"); + +/// Application entrypoint. +pub fn main () -> Usually<()> { + let name = "tek_transport"; + Tui::new()?.run(&JackConnection::new(name)? + .activate_with(|jack|TransportTui::new(jack))?) +} diff --git a/bin/lib.rs b/bin/lib.rs new file mode 100644 index 00000000..0819abb8 --- /dev/null +++ b/bin/lib.rs @@ -0,0 +1,56 @@ +#[allow(unused_imports)] use std::sync::Arc; +#[allow(unused_imports)] use clap::{self, Parser}; +#[allow(unused_imports)] use tek::{ + *, + jack::*, + tek_layout::Measure, + tek_engine::{Usually, tui::{Tui, TuiRun, ratatui::prelude::Color}} +}; + +#[allow(unused)] +fn connect_from (jack: &JackConnection, input: &Port, ports: &[String]) -> Usually<()> { + for port in ports.iter() { + if let Some(port) = jack.port_by_name(port).as_ref() { + jack.client().connect_ports(port, input)?; + } else { + panic!("Missing MIDI output: {port}. Use jack_lsp to list all port names."); + } + } + Ok(()) +} + +#[allow(unused)] +fn connect_to (jack: &JackConnection, output: &Port, ports: &[String]) -> Usually<()> { + for port in ports.iter() { + if let Some(port) = jack.port_by_name(port).as_ref() { + jack.client().connect_ports(output, port)?; + } else { + panic!("Missing MIDI input: {port}. Use jack_lsp to list all port names."); + } + } + Ok(()) +} + +#[allow(unused)] +fn connect_audio_from (jack: &JackConnection, input: &Port, ports: &[String]) -> Usually<()> { + for port in ports.iter() { + if let Some(port) = jack.port_by_name(port).as_ref() { + jack.client().connect_ports(port, input)?; + } else { + panic!("Missing MIDI output: {port}. Use jack_lsp to list all port names."); + } + } + Ok(()) +} + +#[allow(unused)] +fn connect_audio_to (jack: &JackConnection, output: &Port, ports: &[String]) -> Usually<()> { + for port in ports.iter() { + if let Some(port) = jack.port_by_name(port).as_ref() { + jack.client().connect_ports(output, port)?; + } else { + panic!("Missing MIDI input: {port}. Use jack_lsp to list all port names."); + } + } + Ok(()) +} diff --git a/.old/todo_cli_mixer.rs b/bin/todo_cli_mixer.rs similarity index 100% rename from .old/todo_cli_mixer.rs rename to bin/todo_cli_mixer.rs diff --git a/.old/todo_cli_plugin.rs b/bin/todo_cli_plugin.rs similarity index 100% rename from .old/todo_cli_plugin.rs rename to bin/todo_cli_plugin.rs diff --git a/.old/todo_cli_sampler.rs b/bin/todo_cli_sampler.rs similarity index 100% rename from .old/todo_cli_sampler.rs rename to bin/todo_cli_sampler.rs diff --git a/build/Dockerfile.glibc b/build/Dockerfile.glibc deleted file mode 100644 index ec33045e..00000000 --- a/build/Dockerfile.glibc +++ /dev/null @@ -1,14 +0,0 @@ -FROM docker.io/library/debian:bookworm -RUN apt update \ - && apt install -y build-essential bash tree git wget \ - pkg-config libjack-dev liblilv-dev libserd-dev libsord-dev -RUN adduser --quiet --uid 1000 --disabled-password build -RUN wget https://static.rust-lang.org/rustup/dist/x86_64-unknown-linux-gnu/rustup-init \ - && chmod +x ./rustup-init \ - && mv rustup-init /usr/bin/rustup-init -USER build -WORKDIR /home/build -RUN rustup-init -yv --profile minimal --default-toolchain nightly \ - && rm -rvf "$HOME/.rustup/roolchains/*/share" -RUN ls -alh "$HOME" && bash -c '. "$HOME/.cargo/env" \ - && cargo version -vv' diff --git a/build/Dockerfile.musl b/build/Dockerfile.musl deleted file mode 100644 index ed350cdc..00000000 --- a/build/Dockerfile.musl +++ /dev/null @@ -1,13 +0,0 @@ -FROM docker.io/library/alpine:edge - -RUN apk add --no-cache build-base bash tree rustup git just cloc clang20-dev pipewire-jack-dev - -RUN adduser -Du1000 build - -USER 1000 - -RUN rustup-init -y --profile minimal --default-toolchain nightly \ - && rm -rvf "$HOME/.rustup/roolchains/*/share" - -RUN source "$HOME/.cargo/env" \ - && cargo version -vv diff --git a/build/README.md b/build/README.md deleted file mode 100644 index 2f70feaa..00000000 --- a/build/README.md +++ /dev/null @@ -1,11 +0,0 @@ -This directory contains Dockerfiles and shell scripts -for building Tek in a container. For now, only the -GLIBC build works, as the Musl static build is unable -to `dlopen` the system's `libjack.so`. - -Invoke from repo root, like this: `build/release-glibc.sh`. -This will first build a Docker image, `tek:glibc`, which -will contain all build-time dependencies; then, it -will invoke a `cargo build --release` in a container -spawned from that image, ultimately placing the -release build in this directory, as `build/tek`. diff --git a/build/release-glibc-shell.sh b/build/release-glibc-shell.sh deleted file mode 100755 index 3a22285a..00000000 --- a/build/release-glibc-shell.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env sh -set -exo pipefail -docker inspect tek:glibc || time docker build --cache-from=internal \ - -f build/Dockerfile.glibc -t tek:glibc . -time docker run \ - --rm -itu0 \ - -v .:/build -w /build \ - -vtek-build-cargo:/home/build/.cargo \ - -vtek-build-target:/build/target \ - -eRUST_JACK_DLOPEN=true \ - tek:glibc $@ diff --git a/build/release-glibc.sh b/build/release-glibc.sh deleted file mode 100755 index b7c01fc1..00000000 --- a/build/release-glibc.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env sh -set -exo pipefail -docker inspect tek:glibc || time docker build --cache-from=internal \ - -f build/Dockerfile.glibc -t tek:glibc . -time docker run \ - --rm -itu0 \ - -v .:/build -w /build \ - -vtek-build-cargo:/home/build/.cargo \ - -vtek-build-target:/build/target \ - -eRUST_JACK_DLOPEN=true \ - tek:glibc sh -c "chown -R 1000:1000 /build/target \ - && su build -c '. ~/.cargo/env \ - && time cargo build -j4 --release \ - && cp target/release/tek build/'" diff --git a/build/release-musl-shell.sh b/build/release-musl-shell.sh deleted file mode 100755 index 8e5c047c..00000000 --- a/build/release-musl-shell.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env sh -set -exo pipefail -docker inspect tek:musl || time docker build --cache-from=internal \ - -f build/Dockerfile.musl -t tek:musl . -time docker run \ - --rm -itu0 \ - -v .:/build -w /build \ - -vtek-build-cargo:/home/build/.cargo \ - -vtek-build-target:/build/target \ - -eRUST_JACK_DLOPEN=true \ - tek:musl $@ diff --git a/build/release-musl.sh b/build/release-musl.sh deleted file mode 100755 index b8526fb5..00000000 --- a/build/release-musl.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env sh -set -exo pipefail -docker inspect tek:musl || time docker build --cache-from=internal \ - -f build/Dockerfile.musl -t tek:musl . -time docker run \ - --rm -itu0 \ - -v .:/build -w /build \ - -vtek-build-cargo:/home/build/.cargo \ - -vtek-build-target:/build/target \ - -eRUST_JACK_DLOPEN=true \ - tek:musl sh -c "chown -R 1000:1000 /build/target \ - && su build -c 'source ~/.cargo/env \ - && just build-release \ - && cp target/release/tek build/'" diff --git a/deps/rust-jack b/deps/rust-jack deleted file mode 160000 index 764a38a8..00000000 --- a/deps/rust-jack +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 764a38a880ab4749ea60aa7e53cd814b858e606c diff --git a/deps/tengri b/deps/tengri deleted file mode 160000 index 8c54510f..00000000 --- a/deps/tengri +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8c54510f630e8a81b7d7bdca0a51a69cdb9dffcc diff --git a/deps/vst/.github/workflows/deploy.yml b/deps/vst/.github/workflows/deploy.yml deleted file mode 100644 index 4636f530..00000000 --- a/deps/vst/.github/workflows/deploy.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Deploy - -on: - release: - types: [published] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - # Installs the latest stable rust, and all components needed for the rest of the CI pipeline. - - name: Set up CI environment - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true - - # Sanity check: make sure the release builds - - name: Build - run: cargo build --verbose - - # Sanity check: make sure all tests in the release pass - - name: Test - run: cargo test --verbose - - # Deploy to crates.io - # Only works on github releases (tagged commits) - - name: Deploy to crates.io - env: - CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} - run: cargo publish --token $CRATES_IO_TOKEN --manifest-path Cargo.toml \ No newline at end of file diff --git a/deps/vst/.github/workflows/docs.yml b/deps/vst/.github/workflows/docs.yml deleted file mode 100644 index 6cb04feb..00000000 --- a/deps/vst/.github/workflows/docs.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: Docs - -on: - push: - branches: - - master - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - persist-credentials: false - fetch-depth: 0 - - # Installs the latest stable rust, and all components needed for the rest of the CI pipeline. - - name: Set up CI environment - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true - - # Sanity check: make sure the release builds - - name: Build - run: cargo build --verbose - - # Sanity check: make sure all tests in the release pass - - name: Test - run: cargo test --verbose - - # Generate docs - # TODO: what does the last line here do? - - name: Generate docs - env: - GH_ENCRYPTED_TOKEN: ${{ secrets.GH_ENCRYPTED_TOKEN }} - run: | - cargo doc --all --no-deps - echo '' > target/doc/index.html - - # Push docs to github pages (branch `gh-pages`) - - name: Deploy - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: target/doc diff --git a/deps/vst/.github/workflows/rust.yml b/deps/vst/.github/workflows/rust.yml deleted file mode 100644 index a453c48b..00000000 --- a/deps/vst/.github/workflows/rust.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Rust - -on: [push, pull_request] - -jobs: - build: - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, windows-latest, macOS-latest] - - steps: - - uses: actions/checkout@v2 - - # Installs the latest stable rust, and all components needed for the rest of the CI pipeline. - - name: Set up CI environment - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true - components: rustfmt, clippy - - # Makes sure the code builds successfully. - - name: Build - run: cargo build --verbose - - # Makes sure all of the tests pass. - - name: Test - run: cargo test --verbose - - # Runs Clippy on the codebase, and makes sure there are no lint warnings. - # Disabled for now. Re-enable if you find it useful enough to deal with it constantly breaking. - # - name: Clippy - # run: cargo clippy --all-targets --all-features -- -D warnings -A clippy::unreadable_literal -A clippy::needless_range_loop -A clippy::float_cmp -A clippy::comparison-chain -A clippy::needless-doctest-main -A clippy::missing-safety-doc - - # Makes sure the codebase is up to `cargo fmt` standards - - name: Format check - run: cargo fmt --all -- --check \ No newline at end of file diff --git a/deps/vst/.gitignore b/deps/vst/.gitignore deleted file mode 100644 index 06b76755..00000000 --- a/deps/vst/.gitignore +++ /dev/null @@ -1,21 +0,0 @@ -# Compiled files -*.o -*.so -*.rlib -*.dll - -# Executables -*.exe - -# Generated by Cargo -/target/ -/examples/*/target/ -Cargo.lock - -# Vim -[._]*.s[a-w][a-z] -[._]s[a-w][a-z] -*.un~ -Session.vim -.netrwhist -*~ diff --git a/deps/vst/CHANGELOG.md b/deps/vst/CHANGELOG.md deleted file mode 100644 index df61cc20..00000000 --- a/deps/vst/CHANGELOG.md +++ /dev/null @@ -1,86 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## 0.4.0 - -### Changed - -- Added deprecation notice. - -## 0.3.0 - -### Fixed - -- `SysExEvent` no longer contains invalid data on 64-bit systems ([#170](https://github.com/RustAudio/vst-rs/pull/171)] -- Function pointers in `AEffect` marked as `extern` ([#141](https://github.com/RustAudio/vst-rs/pull/141)) -- Key character fixes ([#152](https://github.com/RustAudio/vst-rs/pull/152)) -- Doc and deploy actions fixes ([9eb1bef](https://github.com/RustAudio/vst-rs/commit/9eb1bef1826db1581b4162081de05c1090935afb)) -- Various doc fixes ([#177](https://github.com/RustAudio/vst-rs/pull/177)) - -### Added - -- `begin_edit` and `end_edit` now in `Host` trait ([#151](https://github.com/RustAudio/vst-rs/pull/151)) -- Added a `prelude` for commonly used items when constructing a `Plugin` ([#161](https://github.com/RustAudio/vst-rs/pull/161)) -- Various useful implementations for `AtomicFloat` ([#150](https://github.com/RustAudio/vst-rs/pull/150)) - -### Changed - -- **Major breaking change:** New `Plugin` `Send` requirement ([#140](https://github.com/RustAudio/vst-rs/pull/140)) -- No longer require `Plugin` to implement `Default` ([#154](https://github.com/RustAudio/vst-rs/pull/154)) -- `impl_clicke` replaced with `num_enum` ([#168](https://github.com/RustAudio/vst-rs/pull/168)) -- Reworked `SendEventBuffer` to make it useable in `Plugin::process_events` ([#160](https://github.com/RustAudio/vst-rs/pull/160)) -- Updated dependencies and removed development dependency on `time` ([#179](https://github.com/RustAudio/vst-rs/pull/179)) - -## 0.2.1 - -### Fixed - -- Introduced zero-valued `EventType` variant to enable zero-initialization of `Event`, fixing a panic on Rust 1.48 and newer ([#138](https://github.com/RustAudio/vst-rs/pull/138)) -- `EditorGetRect` opcode returns `1` on success, ensuring that the provided dimensions are applied by the host ([#115](https://github.com/RustAudio/vst-rs/pull/115)) - -### Added - -- Added `update_display()` method to `Host`, telling the host to update its display (after a parameter change) via the `UpdateDisplay` opcode ([#126](https://github.com/RustAudio/vst-rs/pull/126)) -- Allow plug-in to return a custom value in `can_do()` via the `Supported::Custom` enum variant ([#130](https://github.com/RustAudio/vst-rs/pull/130)) -- Added `PartialEq` and `Eq` for `Supported` ([#135](https://github.com/RustAudio/vst-rs/pull/135)) -- Implemented `get_editor()` and `Editor` interface for `PluginInstance` to enable editor support on the host side ([#136](https://github.com/RustAudio/vst-rs/pull/136)) -- Default value (`0.0`) for `AtomicFloat` ([#139](https://github.com/RustAudio/vst-rs/pull/139)) - -## 0.2.0 - -### Changed - -- **Major breaking change:** Restructured `Plugin` API to make it thread safe ([#65](https://github.com/RustAudio/vst-rs/pull/65)) -- Fixed a number of unsoundness issues in the `Outputs` API ([#67](https://github.com/RustAudio/vst-rs/pull/67), [#108](https://github.com/RustAudio/vst-rs/pull/108)) -- Set parameters to be automatable by default ([#99](https://github.com/RustAudio/vst-rs/pull/99)) -- Moved repository to the [RustAudio](https://github.com/RustAudio) organization and renamed it to `vst-rs` ([#90](https://github.com/RustAudio/vst-rs/pull/90), [#94](https://github.com/RustAudio/vst-rs/pull/94)) - -### Fixed - -- Fixed a use-after-move bug in the event iterator ([#93](https://github.com/RustAudio/vst-rs/pull/93), [#111](https://github.com/RustAudio/vst-rs/pull/111)) - -### Added - -- Handle `Opcode::GetEffectName` to resolve name display issues on some hosts ([#89](https://github.com/RustAudio/vst-rs/pull/89)) -- More examples ([#65](https://github.com/RustAudio/vst-rs/pull/65), [#92](https://github.com/RustAudio/vst-rs/pull/92)) - -## 0.1.0 - -### Added - -- Added initial changelog -- Initial project files - -### Removed - -- The `#[derive(Copy, Clone)]` attribute from `Outputs`. - -### Changed -- The signature of the `Outputs::split_at_mut` now takes an `self` parameter instead of `&mut self`. -So calling `split_at_mut` will now move instead of "borrow". -- Now `&mut Outputs` (instead of `Outputs`) implements the `IntoIterator` trait. -- The return type of the `AudioBuffer::zip()` method (but it still implements the Iterator trait). diff --git a/deps/vst/Cargo.toml b/deps/vst/Cargo.toml deleted file mode 100644 index ae189138..00000000 --- a/deps/vst/Cargo.toml +++ /dev/null @@ -1,75 +0,0 @@ -[package] -name = "vst" -version = "0.4.0" -edition = "2021" -authors = [ - "Marko Mijalkovic ", - "Boscop", - "Alex Zywicki ", - "doomy ", - "Ms2ger", - "Rob Saunders", - "David Lu", - "Aske Simon Christensen", - "kykc", - "Jordan Earls", - "xnor104", - "Nathaniel Theis", - "Colin Wallace", - "Henrik Nordvik", - "Charles Saracco", - "Frederik Halkjær" ] - -description = "VST 2.4 API implementation in rust. Create plugins or hosts." - -readme = "README.md" -repository = "https://github.com/rustaudio/vst-rs" - -license = "MIT" -keywords = ["vst", "vst2", "plugin"] - -autoexamples = false - -[features] -default = [] -disable_deprecation_warning = [] - -[dependencies] -log = "0.4" -num-traits = "0.2" -libc = "0.2" -bitflags = "1" -libloading = "0.7" -num_enum = "0.5.2" - -[dev-dependencies] -rand = "0.8" - -[[example]] -name = "dimension_expander" -crate-type = ["cdylib"] - -[[example]] -name = "simple_host" -crate-type = ["bin"] - -[[example]] -name = "sine_synth" -crate-type = ["cdylib"] - -[[example]] -name = "fwd_midi" -crate-type = ["cdylib"] - -[[example]] -name = "gain_effect" -crate-type = ["cdylib"] - -[[example]] -name = "transfer_and_smooth" -crate-type = ["cdylib"] - -[[example]] -name = "ladder_filter" -crate-type = ["cdylib"] - diff --git a/deps/vst/LICENSE b/deps/vst/LICENSE deleted file mode 100644 index 29e06b29..00000000 --- a/deps/vst/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2015 Marko Mijalkovic - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/deps/vst/README.md b/deps/vst/README.md deleted file mode 100644 index a4b28af3..00000000 --- a/deps/vst/README.md +++ /dev/null @@ -1,112 +0,0 @@ -# vst-rs -[![crates.io][crates-img]][crates-url] -[![dependency status](https://deps.rs/repo/github/rustaudio/vst-rs/status.svg)](https://deps.rs/repo/github/rustaudio/vst-rs) -[![Discord Chat][discord-img]][discord-url] -[![Discourse topics][dc-img]][dc-url] - -> **Notice**: `vst-rs` is deprecated. -> -> This crate is no longer actively developed or maintained. VST 2 has been [officially discontinued](http://web.archive.org/web/20210727141622/https://www.steinberg.net/en/newsandevents/news/newsdetail/article/vst-2-coming-to-an-end-4727.html) and it is [no longer possible](https://forum.juce.com/t/steinberg-closing-down-vst2-for-good/27722/25) to acquire a license to distribute VST 2 products. It is highly recommended that you make use of other libraries for developing audio plugins and plugin hosts in Rust. -> -> If you're looking for a high-level, multi-format framework for developing plugins in Rust, consider using [NIH-plug](https://github.com/robbert-vdh/nih-plug/) or [`baseplug`](https://github.com/wrl/baseplug/). If you're looking for bindings to specific plugin APIs, consider using [`vst3-sys`](https://github.com/RustAudio/vst3-sys/), [`clap-sys`](https://github.com/glowcoil/clap-sys), [`lv2(-sys)`](https://github.com/RustAudio/rust-lv2), or [`auv2-sys`](https://github.com/glowcoil/auv2-sys). If, despite the above warnings, you still have a need to use the VST 2 API from Rust, consider using [`vst2-sys`](https://github.com/RustAudio/vst2-sys) or generating bindings from the original VST 2 SDK using [`bindgen`](https://github.com/rust-lang/rust-bindgen). - -`vst-rs` is a library for creating VST2 plugins in the Rust programming language. - -This library is a work in progress, and as such it does not yet implement all -functionality. It can create basic VST plugins without an editor interface. - -**Note:** If you are upgrading from a version prior to 0.2.0, you will need to update -your plugin code to be compatible with the new, thread-safe plugin API. See the -[`transfer_and_smooth`](examples/transfer_and_smooth.rs) example for a guide on how -to port your plugin. - -## Library Documentation - -Documentation for **released** versions can be found [here](https://docs.rs/vst/). - -Development documentation (current `master` branch) can be found [here](https://rustaudio.github.io/vst-rs/vst/). - -## Crate -This crate is available on [crates.io](https://crates.io/crates/vst). If you prefer the bleeding-edge, you can also -include the crate directly from the official [Github repository](https://github.com/rustaudio/vst-rs). - -```toml -# get from crates.io. -vst = "0.3" -``` -```toml -# get directly from Github. This might be unstable! -vst = { git = "https://github.com/rustaudio/vst-rs" } -``` - -## Usage -To create a plugin, simply create a type which implements the `Plugin` trait. Then call the `plugin_main` macro, which will export the necessary functions and handle dealing with the rest of the API. - -## Example Plugin -A simple plugin that bears no functionality. The provided `Cargo.toml` has a -`crate-type` directive which builds a dynamic library, usable by any VST host. - -`src/lib.rs` - -```rust -#[macro_use] -extern crate vst; - -use vst::prelude::*; - -struct BasicPlugin; - -impl Plugin for BasicPlugin { - fn new(_host: HostCallback) -> Self { - BasicPlugin - } - - fn get_info(&self) -> Info { - Info { - name: "Basic Plugin".to_string(), - unique_id: 1357, // Used by hosts to differentiate between plugins. - ..Default::default() - } - } -} - -plugin_main!(BasicPlugin); // Important! -``` - -`Cargo.toml` - -```toml -[package] -name = "basic_vst" -version = "0.0.1" -authors = ["Author "] - -[dependencies] -vst = { git = "https://github.com/rustaudio/vst-rs" } - -[lib] -name = "basicvst" -crate-type = ["cdylib"] -``` - -[crates-img]: https://img.shields.io/crates/v/vst.svg -[crates-url]: https://crates.io/crates/vst -[discord-img]: https://img.shields.io/discord/590254806208217089.svg?label=Discord&logo=discord&color=blue -[discord-url]: https://discord.gg/QPdhk2u -[dc-img]: https://img.shields.io/discourse/https/rust-audio.discourse.group/topics.svg?logo=discourse&color=blue -[dc-url]: https://rust-audio.discourse.group - -#### Packaging on OS X - -On OS X VST plugins are packaged inside loadable bundles. -To package your VST as a loadable bundle you may use the `osx_vst_bundler.sh` script this library provides.  - -Example:  - -``` -./osx_vst_bundler.sh Plugin target/release/plugin.dylib -Creates a Plugin.vst bundle -``` - -## Special Thanks -[Marko Mijalkovic](https://github.com/overdrivenpotato) for [initiating this project](https://github.com/overdrivenpotato/rust-vst2) diff --git a/deps/vst/examples/dimension_expander.rs b/deps/vst/examples/dimension_expander.rs deleted file mode 100644 index 0fbe008a..00000000 --- a/deps/vst/examples/dimension_expander.rs +++ /dev/null @@ -1,222 +0,0 @@ -// author: Marko Mijalkovic - -#[macro_use] -extern crate vst; - -use std::collections::VecDeque; -use std::f64::consts::PI; -use std::sync::Arc; -use std::time::{SystemTime, UNIX_EPOCH}; -use vst::prelude::*; - -/// Calculate the length in samples for a delay. Size ranges from 0.0 to 1.0. -fn delay(index: usize, mut size: f32) -> isize { - const SIZE_OFFSET: f32 = 0.06; - const SIZE_MULT: f32 = 1_000.0; - - size += SIZE_OFFSET; - - // Spread ratio between delays - const SPREAD: f32 = 0.3; - - let base = size * SIZE_MULT; - let mult = (index as f32 * SPREAD) + 1.0; - let offset = if index > 2 { base * SPREAD / 2.0 } else { 0.0 }; - - (base * mult + offset) as isize -} - -/// A left channel and right channel sample. -type SamplePair = (f32, f32); - -/// The Dimension Expander. -struct DimensionExpander { - buffers: Vec>, - params: Arc, - old_size: f32, -} - -struct DimensionExpanderParameters { - dry_wet: AtomicFloat, - size: AtomicFloat, -} - -impl DimensionExpander { - fn new(size: f32, dry_wet: f32) -> DimensionExpander { - const NUM_DELAYS: usize = 4; - - let mut buffers = Vec::new(); - - // Generate delay buffers - for i in 0..NUM_DELAYS { - let samples = delay(i, size); - let mut buffer = VecDeque::with_capacity(samples as usize); - - // Fill in the delay buffers with empty samples - for _ in 0..samples { - buffer.push_back((0.0, 0.0)); - } - - buffers.push(buffer); - } - - DimensionExpander { - buffers, - params: Arc::new(DimensionExpanderParameters { - dry_wet: AtomicFloat::new(dry_wet), - size: AtomicFloat::new(size), - }), - old_size: size, - } - } - - /// Update the delay buffers with a new size value. - fn resize(&mut self, n: f32) { - let old_size = self.old_size; - - for (i, buffer) in self.buffers.iter_mut().enumerate() { - // Calculate the size difference between delays - let old_delay = delay(i, old_size); - let new_delay = delay(i, n); - - let diff = new_delay - old_delay; - - // Add empty samples if the delay was increased, remove if decreased - if diff > 0 { - for _ in 0..diff { - buffer.push_back((0.0, 0.0)); - } - } else if diff < 0 { - for _ in 0..-diff { - let _ = buffer.pop_front(); - } - } - } - - self.old_size = n; - } -} - -impl Plugin for DimensionExpander { - fn new(_host: HostCallback) -> Self { - DimensionExpander::new(0.12, 0.66) - } - - fn get_info(&self) -> Info { - Info { - name: "Dimension Expander".to_string(), - vendor: "overdrivenpotato".to_string(), - unique_id: 243723071, - version: 1, - inputs: 2, - outputs: 2, - parameters: 2, - category: Category::Effect, - - ..Default::default() - } - } - - fn process(&mut self, buffer: &mut AudioBuffer) { - let (inputs, outputs) = buffer.split(); - - // Assume 2 channels - if inputs.len() < 2 || outputs.len() < 2 { - return; - } - - // Resize if size changed - let size = self.params.size.get(); - if size != self.old_size { - self.resize(size); - } - - // Iterate over inputs as (&f32, &f32) - let (l, r) = inputs.split_at(1); - let stereo_in = l[0].iter().zip(r[0].iter()); - - // Iterate over outputs as (&mut f32, &mut f32) - let (mut l, mut r) = outputs.split_at_mut(1); - let stereo_out = l[0].iter_mut().zip(r[0].iter_mut()); - - // Zip and process - for ((left_in, right_in), (left_out, right_out)) in stereo_in.zip(stereo_out) { - // Push the new samples into the delay buffers. - for buffer in &mut self.buffers { - buffer.push_back((*left_in, *right_in)); - } - - let mut left_processed = 0.0; - let mut right_processed = 0.0; - - // Recalculate time per sample - let time_s = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs_f64(); - - // Use buffer index to offset volume LFO - for (n, buffer) in self.buffers.iter_mut().enumerate() { - if let Some((left_old, right_old)) = buffer.pop_front() { - const LFO_FREQ: f64 = 0.5; - const WET_MULT: f32 = 0.66; - - let offset = 0.25 * (n % 4) as f64; - - // Sine wave volume LFO - let lfo = ((time_s * LFO_FREQ + offset) * PI * 2.0).sin() as f32; - - let wet = self.params.dry_wet.get() * WET_MULT; - let mono = (left_old + right_old) / 2.0; - - // Flip right channel and keep left mono so that the result is - // entirely stereo - left_processed += mono * wet * lfo; - right_processed += -mono * wet * lfo; - } - } - - // By only adding to the input, the output value always remains the same in mono - *left_out = *left_in + left_processed; - *right_out = *right_in + right_processed; - } - } - - fn get_parameter_object(&mut self) -> Arc { - Arc::clone(&self.params) as Arc - } -} - -impl PluginParameters for DimensionExpanderParameters { - fn get_parameter(&self, index: i32) -> f32 { - match index { - 0 => self.size.get(), - 1 => self.dry_wet.get(), - _ => 0.0, - } - } - - fn get_parameter_text(&self, index: i32) -> String { - match index { - 0 => format!("{}", (self.size.get() * 1000.0) as isize), - 1 => format!("{:.1}%", self.dry_wet.get() * 100.0), - _ => "".to_string(), - } - } - - fn get_parameter_name(&self, index: i32) -> String { - match index { - 0 => "Size", - 1 => "Dry/Wet", - _ => "", - } - .to_string() - } - - fn set_parameter(&self, index: i32, val: f32) { - match index { - 0 => self.size.set(val), - 1 => self.dry_wet.set(val), - _ => (), - } - } -} - -plugin_main!(DimensionExpander); diff --git a/deps/vst/examples/fwd_midi.rs b/deps/vst/examples/fwd_midi.rs deleted file mode 100644 index c5818fbc..00000000 --- a/deps/vst/examples/fwd_midi.rs +++ /dev/null @@ -1,71 +0,0 @@ -#[macro_use] -extern crate vst; - -use vst::api; -use vst::prelude::*; - -plugin_main!(MyPlugin); // Important! - -#[derive(Default)] -struct MyPlugin { - host: HostCallback, - recv_buffer: SendEventBuffer, - send_buffer: SendEventBuffer, -} - -impl MyPlugin { - fn send_midi(&mut self) { - self.send_buffer - .send_events(self.recv_buffer.events().events(), &mut self.host); - self.recv_buffer.clear(); - } -} - -impl Plugin for MyPlugin { - fn new(host: HostCallback) -> Self { - MyPlugin { - host, - ..Default::default() - } - } - - fn get_info(&self) -> Info { - Info { - name: "fwd_midi".to_string(), - unique_id: 7357001, // Used by hosts to differentiate between plugins. - ..Default::default() - } - } - - fn process_events(&mut self, events: &api::Events) { - self.recv_buffer.store_events(events.events()); - } - - fn process(&mut self, buffer: &mut AudioBuffer) { - for (input, output) in buffer.zip() { - for (in_sample, out_sample) in input.iter().zip(output) { - *out_sample = *in_sample; - } - } - self.send_midi(); - } - - fn process_f64(&mut self, buffer: &mut AudioBuffer) { - for (input, output) in buffer.zip() { - for (in_sample, out_sample) in input.iter().zip(output) { - *out_sample = *in_sample; - } - } - self.send_midi(); - } - - fn can_do(&self, can_do: CanDo) -> vst::api::Supported { - use vst::api::Supported::*; - use vst::plugin::CanDo::*; - - match can_do { - SendEvents | SendMidiEvent | ReceiveEvents | ReceiveMidiEvent => Yes, - _ => No, - } - } -} diff --git a/deps/vst/examples/gain_effect.rs b/deps/vst/examples/gain_effect.rs deleted file mode 100644 index cbe06bd7..00000000 --- a/deps/vst/examples/gain_effect.rs +++ /dev/null @@ -1,129 +0,0 @@ -// author: doomy - -#[macro_use] -extern crate vst; - -use std::sync::Arc; - -use vst::prelude::*; - -/// Simple Gain Effect. -/// Note that this does not use a proper scale for sound and shouldn't be used in -/// a production amplification effect! This is purely for demonstration purposes, -/// as well as to keep things simple as this is meant to be a starting point for -/// any effect. -struct GainEffect { - // Store a handle to the plugin's parameter object. - params: Arc, -} - -/// The plugin's parameter object contains the values of parameters that can be -/// adjusted from the host. If we were creating an effect that didn't allow the -/// user to modify it at runtime or have any controls, we could omit this part. -/// -/// The parameters object is shared between the processing and GUI threads. -/// For this reason, all mutable state in the object has to be represented -/// through thread-safe interior mutability. The easiest way to achieve this -/// is to store the parameters in atomic containers. -struct GainEffectParameters { - // The plugin's state consists of a single parameter: amplitude. - amplitude: AtomicFloat, -} - -impl Default for GainEffectParameters { - fn default() -> GainEffectParameters { - GainEffectParameters { - amplitude: AtomicFloat::new(0.5), - } - } -} - -// All plugins using `vst` also need to implement the `Plugin` trait. Here, we -// define functions that give necessary info to our host. -impl Plugin for GainEffect { - fn new(_host: HostCallback) -> Self { - // Note that controls will always return a value from 0 - 1. - // Setting a default to 0.5 means it's halfway up. - GainEffect { - params: Arc::new(GainEffectParameters::default()), - } - } - - fn get_info(&self) -> Info { - Info { - name: "Gain Effect in Rust".to_string(), - vendor: "Rust DSP".to_string(), - unique_id: 243723072, - version: 1, - inputs: 2, - outputs: 2, - // This `parameters` bit is important; without it, none of our - // parameters will be shown! - parameters: 1, - category: Category::Effect, - ..Default::default() - } - } - - // Here is where the bulk of our audio processing code goes. - fn process(&mut self, buffer: &mut AudioBuffer) { - // Read the amplitude from the parameter object - let amplitude = self.params.amplitude.get(); - // First, we destructure our audio buffer into an arbitrary number of - // input and output buffers. Usually, we'll be dealing with stereo (2 of each) - // but that might change. - for (input_buffer, output_buffer) in buffer.zip() { - // Next, we'll loop through each individual sample so we can apply the amplitude - // value to it. - for (input_sample, output_sample) in input_buffer.iter().zip(output_buffer) { - *output_sample = *input_sample * amplitude; - } - } - } - - // Return the parameter object. This method can be omitted if the - // plugin has no parameters. - fn get_parameter_object(&mut self) -> Arc { - Arc::clone(&self.params) as Arc - } -} - -impl PluginParameters for GainEffectParameters { - // the `get_parameter` function reads the value of a parameter. - fn get_parameter(&self, index: i32) -> f32 { - match index { - 0 => self.amplitude.get(), - _ => 0.0, - } - } - - // the `set_parameter` function sets the value of a parameter. - fn set_parameter(&self, index: i32, val: f32) { - #[allow(clippy::single_match)] - match index { - 0 => self.amplitude.set(val), - _ => (), - } - } - - // This is what will display underneath our control. We can - // format it into a string that makes the most since. - fn get_parameter_text(&self, index: i32) -> String { - match index { - 0 => format!("{:.2}", (self.amplitude.get() - 0.5) * 2f32), - _ => "".to_string(), - } - } - - // This shows the control's name. - fn get_parameter_name(&self, index: i32) -> String { - match index { - 0 => "Amplitude", - _ => "", - } - .to_string() - } -} - -// This part is important! Without it, our plugin won't work. -plugin_main!(GainEffect); diff --git a/deps/vst/examples/ladder_filter.rs b/deps/vst/examples/ladder_filter.rs deleted file mode 100644 index 0c6dac81..00000000 --- a/deps/vst/examples/ladder_filter.rs +++ /dev/null @@ -1,248 +0,0 @@ -//! This zero-delay feedback filter is based on a 4-stage transistor ladder filter. -//! It follows the following equations: -//! x = input - tanh(self.res * self.vout[3]) -//! vout[0] = self.params.g.get() * (tanh(x) - tanh(self.vout[0])) + self.s[0] -//! vout[1] = self.params.g.get() * (tanh(self.vout[0]) - tanh(self.vout[1])) + self.s[1] -//! vout[0] = self.params.g.get() * (tanh(self.vout[1]) - tanh(self.vout[2])) + self.s[2] -//! vout[0] = self.params.g.get() * (tanh(self.vout[2]) - tanh(self.vout[3])) + self.s[3] -//! since we can't easily solve a nonlinear equation, -//! Mystran's fixed-pivot method is used to approximate the tanh() parts. -//! Quality can be improved a lot by oversampling a bit. -//! Feedback is clipped independently of the input, so it doesn't disappear at high gains. - -#[macro_use] -extern crate vst; -use std::f32::consts::PI; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::Arc; - -use vst::prelude::*; - -// this is a 4-pole filter with resonance, which is why there's 4 states and vouts -#[derive(Clone)] -struct LadderFilter { - // Store a handle to the plugin's parameter object. - params: Arc, - // the output of the different filter stages - vout: [f32; 4], - // s is the "state" parameter. In an IIR it would be the last value from the filter - // In this we find it by trapezoidal integration to avoid the unit delay - s: [f32; 4], -} - -struct LadderParameters { - // the "cutoff" parameter. Determines how heavy filtering is - cutoff: AtomicFloat, - g: AtomicFloat, - // needed to calculate cutoff. - sample_rate: AtomicFloat, - // makes a peak at cutoff - res: AtomicFloat, - // used to choose where we want our output to be - poles: AtomicUsize, - // pole_value is just to be able to use get_parameter on poles - pole_value: AtomicFloat, - // a drive parameter. Just used to increase the volume, which results in heavier distortion - drive: AtomicFloat, -} - -impl Default for LadderParameters { - fn default() -> LadderParameters { - LadderParameters { - cutoff: AtomicFloat::new(1000.), - res: AtomicFloat::new(2.), - poles: AtomicUsize::new(3), - pole_value: AtomicFloat::new(1.), - drive: AtomicFloat::new(0.), - sample_rate: AtomicFloat::new(44100.), - g: AtomicFloat::new(0.07135868), - } - } -} - -// member methods for the struct -impl LadderFilter { - // the state needs to be updated after each process. Found by trapezoidal integration - fn update_state(&mut self) { - self.s[0] = 2. * self.vout[0] - self.s[0]; - self.s[1] = 2. * self.vout[1] - self.s[1]; - self.s[2] = 2. * self.vout[2] - self.s[2]; - self.s[3] = 2. * self.vout[3] - self.s[3]; - } - - // performs a complete filter process (mystran's method) - fn tick_pivotal(&mut self, input: f32) { - if self.params.drive.get() > 0. { - self.run_ladder_nonlinear(input * (self.params.drive.get() + 0.7)); - } else { - // - self.run_ladder_linear(input); - } - self.update_state(); - } - - // nonlinear ladder filter function with distortion. - fn run_ladder_nonlinear(&mut self, input: f32) { - let mut a = [1f32; 5]; - let base = [input, self.s[0], self.s[1], self.s[2], self.s[3]]; - // a[n] is the fixed-pivot approximation for tanh() - for n in 0..base.len() { - if base[n] != 0. { - a[n] = base[n].tanh() / base[n]; - } else { - a[n] = 1.; - } - } - // denominators of solutions of individual stages. Simplifies the math a bit - let g0 = 1. / (1. + self.params.g.get() * a[1]); - let g1 = 1. / (1. + self.params.g.get() * a[2]); - let g2 = 1. / (1. + self.params.g.get() * a[3]); - let g3 = 1. / (1. + self.params.g.get() * a[4]); - // these are just factored out of the feedback solution. Makes the math way easier to read - let f3 = self.params.g.get() * a[3] * g3; - let f2 = self.params.g.get() * a[2] * g2 * f3; - let f1 = self.params.g.get() * a[1] * g1 * f2; - let f0 = self.params.g.get() * g0 * f1; - // outputs a 24db filter - self.vout[3] = - (f0 * input * a[0] + f1 * g0 * self.s[0] + f2 * g1 * self.s[1] + f3 * g2 * self.s[2] + g3 * self.s[3]) - / (f0 * self.params.res.get() * a[3] + 1.); - // since we know the feedback, we can solve the remaining outputs: - self.vout[0] = g0 - * (self.params.g.get() * a[1] * (input * a[0] - self.params.res.get() * a[3] * self.vout[3]) + self.s[0]); - self.vout[1] = g1 * (self.params.g.get() * a[2] * self.vout[0] + self.s[1]); - self.vout[2] = g2 * (self.params.g.get() * a[3] * self.vout[1] + self.s[2]); - } - - // linear version without distortion - pub fn run_ladder_linear(&mut self, input: f32) { - // denominators of solutions of individual stages. Simplifies the math a bit - let g0 = 1. / (1. + self.params.g.get()); - let g1 = self.params.g.get() * g0 * g0; - let g2 = self.params.g.get() * g1 * g0; - let g3 = self.params.g.get() * g2 * g0; - // outputs a 24db filter - self.vout[3] = - (g3 * self.params.g.get() * input + g0 * self.s[3] + g1 * self.s[2] + g2 * self.s[1] + g3 * self.s[0]) - / (g3 * self.params.g.get() * self.params.res.get() + 1.); - // since we know the feedback, we can solve the remaining outputs: - self.vout[0] = g0 * (self.params.g.get() * (input - self.params.res.get() * self.vout[3]) + self.s[0]); - self.vout[1] = g0 * (self.params.g.get() * self.vout[0] + self.s[1]); - self.vout[2] = g0 * (self.params.g.get() * self.vout[1] + self.s[2]); - } -} - -impl LadderParameters { - pub fn set_cutoff(&self, value: f32) { - // cutoff formula gives us a natural feeling cutoff knob that spends more time in the low frequencies - self.cutoff.set(20000. * (1.8f32.powf(10. * value - 10.))); - // bilinear transformation for g gives us a very accurate cutoff - self.g.set((PI * self.cutoff.get() / (self.sample_rate.get())).tan()); - } - - // returns the value used to set cutoff. for get_parameter function - pub fn get_cutoff(&self) -> f32 { - 1. + 0.17012975 * (0.00005 * self.cutoff.get()).ln() - } - - pub fn set_poles(&self, value: f32) { - self.pole_value.set(value); - self.poles.store(((value * 3.).round()) as usize, Ordering::Relaxed); - } -} - -impl PluginParameters for LadderParameters { - // get_parameter has to return the value used in set_parameter - fn get_parameter(&self, index: i32) -> f32 { - match index { - 0 => self.get_cutoff(), - 1 => self.res.get() / 4., - 2 => self.pole_value.get(), - 3 => self.drive.get() / 5., - _ => 0.0, - } - } - - fn set_parameter(&self, index: i32, value: f32) { - match index { - 0 => self.set_cutoff(value), - 1 => self.res.set(value * 4.), - 2 => self.set_poles(value), - 3 => self.drive.set(value * 5.), - _ => (), - } - } - - fn get_parameter_name(&self, index: i32) -> String { - match index { - 0 => "cutoff".to_string(), - 1 => "resonance".to_string(), - 2 => "filter order".to_string(), - 3 => "drive".to_string(), - _ => "".to_string(), - } - } - - fn get_parameter_label(&self, index: i32) -> String { - match index { - 0 => "Hz".to_string(), - 1 => "%".to_string(), - 2 => "poles".to_string(), - 3 => "%".to_string(), - _ => "".to_string(), - } - } - // This is what will display underneath our control. We can - // format it into a string that makes the most sense. - fn get_parameter_text(&self, index: i32) -> String { - match index { - 0 => format!("{:.0}", self.cutoff.get()), - 1 => format!("{:.3}", self.res.get()), - 2 => format!("{}", self.poles.load(Ordering::Relaxed) + 1), - 3 => format!("{:.3}", self.drive.get()), - _ => format!(""), - } - } -} - -impl Plugin for LadderFilter { - fn new(_host: HostCallback) -> Self { - LadderFilter { - vout: [0f32; 4], - s: [0f32; 4], - params: Arc::new(LadderParameters::default()), - } - } - - fn set_sample_rate(&mut self, rate: f32) { - self.params.sample_rate.set(rate); - } - - fn get_info(&self) -> Info { - Info { - name: "LadderFilter".to_string(), - unique_id: 9263, - inputs: 1, - outputs: 1, - category: Category::Effect, - parameters: 4, - ..Default::default() - } - } - - fn process(&mut self, buffer: &mut AudioBuffer) { - for (input_buffer, output_buffer) in buffer.zip() { - for (input_sample, output_sample) in input_buffer.iter().zip(output_buffer) { - self.tick_pivotal(*input_sample); - // the poles parameter chooses which filter stage we take our output from. - *output_sample = self.vout[self.params.poles.load(Ordering::Relaxed)]; - } - } - } - - fn get_parameter_object(&mut self) -> Arc { - Arc::clone(&self.params) as Arc - } -} - -plugin_main!(LadderFilter); diff --git a/deps/vst/examples/simple_host.rs b/deps/vst/examples/simple_host.rs deleted file mode 100644 index d8bafbdc..00000000 --- a/deps/vst/examples/simple_host.rs +++ /dev/null @@ -1,63 +0,0 @@ -extern crate vst; - -use std::env; -use std::path::Path; -use std::process; -use std::sync::{Arc, Mutex}; - -use vst::host::{Host, PluginLoader}; -use vst::plugin::Plugin; - -#[allow(dead_code)] -struct SampleHost; - -impl Host for SampleHost { - fn automate(&self, index: i32, value: f32) { - println!("Parameter {} had its value changed to {}", index, value); - } -} - -fn main() { - let args: Vec = env::args().collect(); - if args.len() < 2 { - println!("usage: simple_host path/to/vst"); - process::exit(1); - } - - let path = Path::new(&args[1]); - - // Create the host - let host = Arc::new(Mutex::new(SampleHost)); - - println!("Loading {}...", path.to_str().unwrap()); - - // Load the plugin - let mut loader = - PluginLoader::load(path, Arc::clone(&host)).unwrap_or_else(|e| panic!("Failed to load plugin: {}", e)); - - // Create an instance of the plugin - let mut instance = loader.instance().unwrap(); - - // Get the plugin information - let info = instance.get_info(); - - println!( - "Loaded '{}':\n\t\ - Vendor: {}\n\t\ - Presets: {}\n\t\ - Parameters: {}\n\t\ - VST ID: {}\n\t\ - Version: {}\n\t\ - Initial Delay: {} samples", - info.name, info.vendor, info.presets, info.parameters, info.unique_id, info.version, info.initial_delay - ); - - // Initialize the instance - instance.init(); - println!("Initialized instance!"); - - println!("Closing instance..."); - // Close the instance. This is not necessary as the instance is shut down when - // it is dropped as it goes out of scope. - // drop(instance); -} diff --git a/deps/vst/examples/sine_synth.rs b/deps/vst/examples/sine_synth.rs deleted file mode 100644 index 81a0475a..00000000 --- a/deps/vst/examples/sine_synth.rs +++ /dev/null @@ -1,160 +0,0 @@ -// author: Rob Saunders - -#[macro_use] -extern crate vst; - -use vst::prelude::*; - -use std::f64::consts::PI; - -/// Convert the midi note's pitch into the equivalent frequency. -/// -/// This function assumes A4 is 440hz. -fn midi_pitch_to_freq(pitch: u8) -> f64 { - const A4_PITCH: i8 = 69; - const A4_FREQ: f64 = 440.0; - - // Midi notes can be 0-127 - ((f64::from(pitch as i8 - A4_PITCH)) / 12.).exp2() * A4_FREQ -} - -struct SineSynth { - sample_rate: f64, - time: f64, - note_duration: f64, - note: Option, -} - -impl SineSynth { - fn time_per_sample(&self) -> f64 { - 1.0 / self.sample_rate - } - - /// Process an incoming midi event. - /// - /// The midi data is split up like so: - /// - /// `data[0]`: Contains the status and the channel. Source: [source] - /// `data[1]`: Contains the supplemental data for the message - so, if this was a NoteOn then - /// this would contain the note. - /// `data[2]`: Further supplemental data. Would be velocity in the case of a NoteOn message. - /// - /// [source]: http://www.midimountain.com/midi/midi_status.htm - fn process_midi_event(&mut self, data: [u8; 3]) { - match data[0] { - 128 => self.note_off(data[1]), - 144 => self.note_on(data[1]), - _ => (), - } - } - - fn note_on(&mut self, note: u8) { - self.note_duration = 0.0; - self.note = Some(note) - } - - fn note_off(&mut self, note: u8) { - if self.note == Some(note) { - self.note = None - } - } -} - -pub const TAU: f64 = PI * 2.0; - -impl Plugin for SineSynth { - fn new(_host: HostCallback) -> Self { - SineSynth { - sample_rate: 44100.0, - note_duration: 0.0, - time: 0.0, - note: None, - } - } - - fn get_info(&self) -> Info { - Info { - name: "SineSynth".to_string(), - vendor: "DeathDisco".to_string(), - unique_id: 6667, - category: Category::Synth, - inputs: 2, - outputs: 2, - parameters: 0, - initial_delay: 0, - ..Info::default() - } - } - - #[allow(unused_variables)] - #[allow(clippy::single_match)] - fn process_events(&mut self, events: &Events) { - for event in events.events() { - match event { - Event::Midi(ev) => self.process_midi_event(ev.data), - // More events can be handled here. - _ => (), - } - } - } - - fn set_sample_rate(&mut self, rate: f32) { - self.sample_rate = f64::from(rate); - } - - fn process(&mut self, buffer: &mut AudioBuffer) { - let samples = buffer.samples(); - let (_, mut outputs) = buffer.split(); - let output_count = outputs.len(); - let per_sample = self.time_per_sample(); - let mut output_sample; - for sample_idx in 0..samples { - let time = self.time; - let note_duration = self.note_duration; - if let Some(current_note) = self.note { - let signal = (time * midi_pitch_to_freq(current_note) * TAU).sin(); - - // Apply a quick envelope to the attack of the signal to avoid popping. - let attack = 0.5; - let alpha = if note_duration < attack { - note_duration / attack - } else { - 1.0 - }; - - output_sample = (signal * alpha) as f32; - - self.time += per_sample; - self.note_duration += per_sample; - } else { - output_sample = 0.0; - } - for buf_idx in 0..output_count { - let buff = outputs.get_mut(buf_idx); - buff[sample_idx] = output_sample; - } - } - } - - fn can_do(&self, can_do: CanDo) -> Supported { - match can_do { - CanDo::ReceiveMidiEvent => Supported::Yes, - _ => Supported::Maybe, - } - } -} - -plugin_main!(SineSynth); - -#[cfg(test)] -mod tests { - use midi_pitch_to_freq; - - #[test] - fn test_midi_pitch_to_freq() { - for i in 0..127 { - // expect no panics - midi_pitch_to_freq(i); - } - } -} diff --git a/deps/vst/examples/transfer_and_smooth.rs b/deps/vst/examples/transfer_and_smooth.rs deleted file mode 100644 index ba50b121..00000000 --- a/deps/vst/examples/transfer_and_smooth.rs +++ /dev/null @@ -1,136 +0,0 @@ -// This example illustrates how an existing plugin can be ported to the new, -// thread-safe API with the help of the ParameterTransfer struct. -// It shows how the parameter iteration feature of ParameterTransfer can be -// used to react explicitly to parameter changes in an efficient way (here, -// to implement smoothing of parameters). - -#[macro_use] -extern crate vst; - -use std::f32; -use std::sync::Arc; - -use vst::prelude::*; - -const PARAMETER_COUNT: usize = 100; -const BASE_FREQUENCY: f32 = 5.0; -const FILTER_FACTOR: f32 = 0.01; // Set this to 1.0 to disable smoothing. -const TWO_PI: f32 = 2.0 * f32::consts::PI; - -// 1. Define a struct to hold parameters. Put a ParameterTransfer inside it, -// plus optionally a HostCallback. -struct MyPluginParameters { - #[allow(dead_code)] - host: HostCallback, - transfer: ParameterTransfer, -} - -// 2. Put an Arc reference to your parameter struct in your main Plugin struct. -struct MyPlugin { - params: Arc, - states: Vec, - sample_rate: f32, - phase: f32, -} - -// 3. Implement PluginParameters for your parameter struct. -// The set_parameter and get_parameter just access the ParameterTransfer. -// The other methods can be implemented on top of this as well. -impl PluginParameters for MyPluginParameters { - fn set_parameter(&self, index: i32, value: f32) { - self.transfer.set_parameter(index as usize, value); - } - - fn get_parameter(&self, index: i32) -> f32 { - self.transfer.get_parameter(index as usize) - } -} - -impl Plugin for MyPlugin { - fn new(host: HostCallback) -> Self { - MyPlugin { - // 4. Initialize your main Plugin struct with a parameter struct - // wrapped in an Arc, and put the HostCallback inside it. - params: Arc::new(MyPluginParameters { - host, - transfer: ParameterTransfer::new(PARAMETER_COUNT), - }), - states: vec![Smoothed::default(); PARAMETER_COUNT], - sample_rate: 44100.0, - phase: 0.0, - } - } - - fn get_info(&self) -> Info { - Info { - parameters: PARAMETER_COUNT as i32, - inputs: 0, - outputs: 2, - category: Category::Synth, - f64_precision: false, - - name: "transfer_and_smooth".to_string(), - vendor: "Loonies".to_string(), - unique_id: 0x500007, - version: 100, - - ..Info::default() - } - } - - // 5. Return a reference to the parameter struct from get_parameter_object. - fn get_parameter_object(&mut self) -> Arc { - Arc::clone(&self.params) as Arc - } - - fn set_sample_rate(&mut self, sample_rate: f32) { - self.sample_rate = sample_rate; - } - - fn process(&mut self, buffer: &mut AudioBuffer) { - // 6. In the process method, iterate over changed parameters and do - // for each what you would previously do in set_parameter. Since this - // runs in the processing thread, it has mutable access to the Plugin. - for (p, value) in self.params.transfer.iterate(true) { - // Example: Update filter state of changed parameter. - self.states[p].set(value); - } - - // Example: Dummy synth adding together a bunch of sines. - let samples = buffer.samples(); - let mut outputs = buffer.split().1; - for i in 0..samples { - let mut sum = 0.0; - for p in 0..PARAMETER_COUNT { - let amp = self.states[p].get(); - if amp != 0.0 { - sum += (self.phase * p as f32 * TWO_PI).sin() * amp; - } - } - outputs[0][i] = sum; - outputs[1][i] = sum; - self.phase = (self.phase + BASE_FREQUENCY / self.sample_rate).fract(); - } - } -} - -// Example: Parameter smoothing as an example of non-trivial parameter handling -// that has to happen when a parameter changes. -#[derive(Clone, Default)] -struct Smoothed { - state: f32, - target: f32, -} - -impl Smoothed { - fn set(&mut self, value: f32) { - self.target = value; - } - - fn get(&mut self) -> f32 { - self.state += (self.target - self.state) * FILTER_FACTOR; - self.state - } -} - -plugin_main!(MyPlugin); diff --git a/deps/vst/osx_vst_bundler.sh b/deps/vst/osx_vst_bundler.sh deleted file mode 100755 index b28d7da4..00000000 --- a/deps/vst/osx_vst_bundler.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/bin/bash - -# Make sure we have the arguments we need -if [[ -z $1 || -z $2 ]]; then - echo "Generates a macOS bundle from a compiled dylib file" - echo "Example:" - echo -e "\t$0 Plugin target/release/plugin.dylib" - echo -e "\tCreates a Plugin.vst bundle" -else - # Make the bundle folder - mkdir -p "$1.vst/Contents/MacOS" - - # Create the PkgInfo - echo "BNDL????" > "$1.vst/Contents/PkgInfo" - - #build the Info.Plist - echo " - - - - CFBundleDevelopmentRegion - English - - CFBundleExecutable - $1 - - CFBundleGetInfoString - vst - - CFBundleIconFile - - - CFBundleIdentifier - com.rust-vst.$1 - - CFBundleInfoDictionaryVersion - 6.0 - - CFBundleName - $1 - - CFBundlePackageType - BNDL - - CFBundleVersion - 1.0 - - CFBundleSignature - $((RANDOM % 9999)) - - CSResourcesFileMapped - - - -" > "$1.vst/Contents/Info.plist" - - # move the provided library to the correct location - cp "$2" "$1.vst/Contents/MacOS/$1" - - echo "Created bundle $1.vst" -fi diff --git a/deps/vst/rustfmt.toml b/deps/vst/rustfmt.toml deleted file mode 100644 index 866c7561..00000000 --- a/deps/vst/rustfmt.toml +++ /dev/null @@ -1 +0,0 @@ -max_width = 120 \ No newline at end of file diff --git a/deps/vst/src/api.rs b/deps/vst/src/api.rs deleted file mode 100644 index 44a1a78a..00000000 --- a/deps/vst/src/api.rs +++ /dev/null @@ -1,927 +0,0 @@ -//! Structures and types for interfacing with the VST 2.4 API. - -use std::os::raw::c_void; -use std::sync::Arc; - -use self::consts::*; -use crate::{ - editor::Editor, - plugin::{Info, Plugin, PluginParameters}, -}; - -/// Constant values -#[allow(missing_docs)] // For obvious constants -pub mod consts { - - pub const MAX_PRESET_NAME_LEN: usize = 24; - pub const MAX_PARAM_STR_LEN: usize = 32; - pub const MAX_LABEL: usize = 64; - pub const MAX_SHORT_LABEL: usize = 8; - pub const MAX_PRODUCT_STR_LEN: usize = 64; - pub const MAX_VENDOR_STR_LEN: usize = 64; - - /// VST plugins are identified by a magic number. This corresponds to 0x56737450. - pub const VST_MAGIC: i32 = ('V' as i32) << 24 | ('s' as i32) << 16 | ('t' as i32) << 8 | ('P' as i32); -} - -/// `VSTPluginMain` function signature. -pub type PluginMain = fn(callback: HostCallbackProc) -> *mut AEffect; - -/// Host callback function passed to plugin. -/// Can be used to query host information from plugin side. -pub type HostCallbackProc = - extern "C" fn(effect: *mut AEffect, opcode: i32, index: i32, value: isize, ptr: *mut c_void, opt: f32) -> isize; - -/// Dispatcher function used to process opcodes. Called by host. -pub type DispatcherProc = - extern "C" fn(effect: *mut AEffect, opcode: i32, index: i32, value: isize, ptr: *mut c_void, opt: f32) -> isize; - -/// Process function used to process 32 bit floating point samples. Called by host. -pub type ProcessProc = - extern "C" fn(effect: *mut AEffect, inputs: *const *const f32, outputs: *mut *mut f32, sample_frames: i32); - -/// Process function used to process 64 bit floating point samples. Called by host. -pub type ProcessProcF64 = - extern "C" fn(effect: *mut AEffect, inputs: *const *const f64, outputs: *mut *mut f64, sample_frames: i32); - -/// Callback function used to set parameter values. Called by host. -pub type SetParameterProc = extern "C" fn(effect: *mut AEffect, index: i32, parameter: f32); - -/// Callback function used to get parameter values. Called by host. -pub type GetParameterProc = extern "C" fn(effect: *mut AEffect, index: i32) -> f32; - -/// Used with the VST API to pass around plugin information. -#[allow(non_snake_case)] -#[repr(C)] -pub struct AEffect { - /// Magic number. Must be `['V', 'S', 'T', 'P']`. - pub magic: i32, - - /// Host to plug-in dispatcher. - pub dispatcher: DispatcherProc, - - /// Accumulating process mode is deprecated in VST 2.4! Use `processReplacing` instead! - pub _process: ProcessProc, - - /// Set value of automatable parameter. - pub setParameter: SetParameterProc, - - /// Get value of automatable parameter. - pub getParameter: GetParameterProc, - - /// Number of programs (Presets). - pub numPrograms: i32, - - /// Number of parameters. All programs are assumed to have this many parameters. - pub numParams: i32, - - /// Number of audio inputs. - pub numInputs: i32, - - /// Number of audio outputs. - pub numOutputs: i32, - - /// Bitmask made of values from `api::PluginFlags`. - /// - /// ```no_run - /// use vst::api::PluginFlags; - /// let flags = PluginFlags::CAN_REPLACING | PluginFlags::CAN_DOUBLE_REPLACING; - /// // ... - /// ``` - pub flags: i32, - - /// Reserved for host, must be 0. - pub reserved1: isize, - - /// Reserved for host, must be 0. - pub reserved2: isize, - - /// For algorithms which need input in the first place (Group delay or latency in samples). - /// - /// This value should be initially in a resume state. - pub initialDelay: i32, - - /// Deprecated unused member. - pub _realQualities: i32, - - /// Deprecated unused member. - pub _offQualities: i32, - - /// Deprecated unused member. - pub _ioRatio: f32, - - /// Void pointer usable by api to store object data. - pub object: *mut c_void, - - /// User defined pointer. - pub user: *mut c_void, - - /// Registered unique identifier (register it at Steinberg 3rd party support Web). - /// This is used to identify a plug-in during save+load of preset and project. - pub uniqueId: i32, - - /// Plug-in version (e.g. 1100 for v1.1.0.0). - pub version: i32, - - /// Process audio samples in replacing mode. - pub processReplacing: ProcessProc, - - /// Process double-precision audio samples in replacing mode. - pub processReplacingF64: ProcessProcF64, - - /// Reserved for future use (please zero). - pub future: [u8; 56], -} - -impl AEffect { - /// Return handle to Plugin object. Only works for plugins created using this library. - /// Caller is responsible for not calling this function concurrently. - // Suppresses warning about returning a reference to a box - #[allow(clippy::borrowed_box)] - pub unsafe fn get_plugin(&self) -> &mut Box { - //FIXME: find a way to do this without resorting to transmuting via a box - &mut *(self.object as *mut Box) - } - - /// Return handle to Info object. Only works for plugins created using this library. - pub unsafe fn get_info(&self) -> &Info { - &(*(self.user as *mut super::PluginCache)).info - } - - /// Return handle to PluginParameters object. Only works for plugins created using this library. - pub unsafe fn get_params(&self) -> &Arc { - &(*(self.user as *mut super::PluginCache)).params - } - - /// Return handle to Editor object. Only works for plugins created using this library. - /// Caller is responsible for not calling this function concurrently. - pub unsafe fn get_editor(&self) -> &mut Option> { - &mut (*(self.user as *mut super::PluginCache)).editor - } - - /// Drop the Plugin object. Only works for plugins created using this library. - pub unsafe fn drop_plugin(&mut self) { - drop(Box::from_raw(self.object as *mut Box)); - drop(Box::from_raw(self.user as *mut super::PluginCache)); - } -} - -/// Information about a channel. Only some hosts use this information. -#[repr(C)] -pub struct ChannelProperties { - /// Channel name. - pub name: [u8; MAX_LABEL as usize], - - /// Flags found in `ChannelFlags`. - pub flags: i32, - - /// Type of speaker arrangement this channel is a part of. - pub arrangement_type: SpeakerArrangementType, - - /// Name of channel (recommended: 6 characters + delimiter). - pub short_name: [u8; MAX_SHORT_LABEL as usize], - - /// Reserved for future use. - pub future: [u8; 48], -} - -/// Tells the host how the channels are intended to be used in the plugin. Only useful for some -/// hosts. -#[repr(i32)] -#[derive(Clone, Copy)] -pub enum SpeakerArrangementType { - /// User defined arrangement. - Custom = -2, - /// Empty arrangement. - Empty = -1, - - /// Mono. - Mono = 0, - - /// L R - Stereo, - /// Ls Rs - StereoSurround, - /// Lc Rc - StereoCenter, - /// Sl Sr - StereoSide, - /// C Lfe - StereoCLfe, - - /// L R C - Cinema30, - /// L R S - Music30, - - /// L R C Lfe - Cinema31, - /// L R Lfe S - Music31, - - /// L R C S (LCRS) - Cinema40, - /// L R Ls Rs (Quadro) - Music40, - - /// L R C Lfe S (LCRS + Lfe) - Cinema41, - /// L R Lfe Ls Rs (Quadro + Lfe) - Music41, - - /// L R C Ls Rs - Surround50, - /// L R C Lfe Ls Rs - Surround51, - - /// L R C Ls Rs Cs - Cinema60, - /// L R Ls Rs Sl Sr - Music60, - - /// L R C Lfe Ls Rs Cs - Cinema61, - /// L R Lfe Ls Rs Sl Sr - Music61, - - /// L R C Ls Rs Lc Rc - Cinema70, - /// L R C Ls Rs Sl Sr - Music70, - - /// L R C Lfe Ls Rs Lc Rc - Cinema71, - /// L R C Lfe Ls Rs Sl Sr - Music71, - - /// L R C Ls Rs Lc Rc Cs - Cinema80, - /// L R C Ls Rs Cs Sl Sr - Music80, - - /// L R C Lfe Ls Rs Lc Rc Cs - Cinema81, - /// L R C Lfe Ls Rs Cs Sl Sr - Music81, - - /// L R C Lfe Ls Rs Tfl Tfc Tfr Trl Trr Lfe2 - Surround102, -} - -/// Used to specify whether functionality is supported. -#[allow(missing_docs)] -#[derive(PartialEq, Eq)] -pub enum Supported { - Yes, - Maybe, - No, - Custom(isize), -} - -impl Supported { - /// Create a `Supported` value from an integer if possible. - pub fn from(val: isize) -> Option { - use self::Supported::*; - - match val { - 1 => Some(Yes), - 0 => Some(Maybe), - -1 => Some(No), - _ => None, - } - } -} - -impl Into for Supported { - /// Convert to integer ordinal for interop with VST api. - fn into(self) -> isize { - use self::Supported::*; - - match self { - Yes => 1, - Maybe => 0, - No => -1, - Custom(i) => i, - } - } -} - -/// Denotes in which thread the host is in. -#[repr(i32)] -pub enum ProcessLevel { - /// Unsupported by host. - Unknown = 0, - - /// GUI thread. - User, - /// Audio process thread. - Realtime, - /// Sequence thread (MIDI, etc). - Prefetch, - /// Offline processing thread (therefore GUI/user thread). - Offline, -} - -/// Language that the host is using. -#[repr(i32)] -#[allow(missing_docs)] -pub enum HostLanguage { - English = 1, - German, - French, - Italian, - Spanish, - Japanese, -} - -/// The file operation to perform. -#[repr(i32)] -pub enum FileSelectCommand { - /// Load a file. - Load = 0, - /// Save a file. - Save, - /// Load multiple files simultaneously. - LoadMultipleFiles, - /// Choose a directory. - SelectDirectory, -} - -// TODO: investigate removing this. -/// Format to select files. -pub enum FileSelectType { - /// Regular file selector. - Regular, -} - -/// File type descriptor. -#[repr(C)] -pub struct FileType { - /// Display name of file type. - pub name: [u8; 128], - - /// OS X file type. - pub osx_type: [u8; 8], - /// Windows file type. - pub win_type: [u8; 8], - /// Unix file type. - pub nix_type: [u8; 8], - - /// MIME type. - pub mime_type_1: [u8; 128], - /// Additional MIME type. - pub mime_type_2: [u8; 128], -} - -/// File selector descriptor used in `host::OpCode::OpenFileSelector`. -#[repr(C)] -pub struct FileSelect { - /// The type of file selection to perform. - pub command: FileSelectCommand, - /// The file selector to open. - pub select_type: FileSelectType, - /// Unknown. 0 = no creator. - pub mac_creator: i32, - /// Number of file types. - pub num_types: i32, - /// List of file types to show. - pub file_types: *mut FileType, - - /// File selector's title. - pub title: [u8; 1024], - /// Initial path. - pub initial_path: *mut u8, - /// Used when operation returns a single path. - pub return_path: *mut u8, - /// Size of the path buffer in bytes. - pub size_return_path: i32, - - /// Used when operation returns multiple paths. - pub return_multiple_paths: *mut *mut u8, - /// Number of paths returned. - pub num_paths: i32, - - /// Reserved by host. - pub reserved: isize, - /// Reserved for future use. - pub future: [u8; 116], -} - -/// A struct which contains events. -#[repr(C)] -pub struct Events { - /// Number of events. - pub num_events: i32, - - /// Reserved for future use. Should be 0. - pub _reserved: isize, - - /// Variable-length array of pointers to `api::Event` objects. - /// - /// The VST standard specifies a variable length array of initial size 2. If there are more - /// than 2 elements a larger array must be stored in this structure. - pub events: [*mut Event; 2], -} - -impl Events { - #[inline] - pub(crate) fn events_raw(&self) -> &[*const Event] { - use std::slice; - unsafe { - slice::from_raw_parts( - &self.events[0] as *const *mut _ as *const *const _, - self.num_events as usize, - ) - } - } - - #[inline] - pub(crate) fn events_raw_mut(&mut self) -> &mut [*const SysExEvent] { - use std::slice; - unsafe { - slice::from_raw_parts_mut( - &mut self.events[0] as *mut *mut _ as *mut *const _, - self.num_events as usize, - ) - } - } - - /// Use this in your impl of process_events() to process the incoming midi events. - /// - /// # Example - /// ```no_run - /// # use vst::plugin::{Info, Plugin, HostCallback}; - /// # use vst::buffer::{AudioBuffer, SendEventBuffer}; - /// # use vst::host::Host; - /// # use vst::api; - /// # use vst::event::{Event, MidiEvent}; - /// # struct ExamplePlugin { host: HostCallback, send_buf: SendEventBuffer } - /// # impl Plugin for ExamplePlugin { - /// # fn new(host: HostCallback) -> Self { Self { host, send_buf: Default::default() } } - /// # - /// # fn get_info(&self) -> Info { Default::default() } - /// # - /// fn process_events(&mut self, events: &api::Events) { - /// for e in events.events() { - /// match e { - /// Event::Midi(MidiEvent { data, .. }) => { - /// // ... - /// } - /// _ => () - /// } - /// } - /// } - /// # } - /// ``` - #[inline] - #[allow(clippy::needless_lifetimes)] - pub fn events<'a>(&'a self) -> impl Iterator> { - self.events_raw() - .iter() - .map(|ptr| unsafe { crate::event::Event::from_raw_event(*ptr) }) - } -} - -/// The type of event that has occurred. See `api::Event.event_type`. -#[repr(i32)] -#[derive(Copy, Clone, Debug)] -pub enum EventType { - /// Value used for uninitialized placeholder events. - _Placeholder = 0, - - /// Midi event. See `api::MidiEvent`. - Midi = 1, - - /// Deprecated. - _Audio, - /// Deprecated. - _Video, - /// Deprecated. - _Parameter, - /// Deprecated. - _Trigger, - - /// System exclusive event. See `api::SysExEvent`. - SysEx, -} - -/// A VST event intended to be casted to a corresponding type. -/// -/// The event types are not all guaranteed to be the same size, -/// so casting between them can be done -/// via `mem::transmute()` while leveraging pointers, e.g. -/// -/// ``` -/// # use vst::api::{Event, EventType, MidiEvent, SysExEvent}; -/// # let mut event: *mut Event = &mut unsafe { std::mem::zeroed() }; -/// // let event: *const Event = ...; -/// let midi_event: &MidiEvent = unsafe { std::mem::transmute(event) }; -/// ``` -#[repr(C)] -#[derive(Copy, Clone)] -pub struct Event { - /// The type of event. This lets you know which event this object should be casted to. - /// - /// # Example - /// - /// ``` - /// # use vst::api::{Event, EventType, MidiEvent, SysExEvent}; - /// # - /// # // Valid for test - /// # let mut event: *mut Event = &mut unsafe { std::mem::zeroed() }; - /// # - /// // let mut event: *mut Event = ... - /// match unsafe { (*event).event_type } { - /// EventType::Midi => { - /// let midi_event: &MidiEvent = unsafe { - /// std::mem::transmute(event) - /// }; - /// - /// // ... - /// } - /// EventType::SysEx => { - /// let sys_event: &SysExEvent = unsafe { - /// std::mem::transmute(event) - /// }; - /// - /// // ... - /// } - /// // ... - /// # _ => {} - /// } - /// ``` - pub event_type: EventType, - - /// Size of this structure; `mem::sizeof::()`. - pub byte_size: i32, - - /// Number of samples into the current processing block that this event occurs on. - /// - /// E.g. if the block size is 512 and this value is 123, the event will occur on sample - /// `samples[123]`. - pub delta_frames: i32, - - /// Generic flags, none defined in VST api yet. - pub _flags: i32, - - /// The `Event` type is cast appropriately, so this acts as reserved space. - /// - /// The actual size of the data may vary - ///as this type is not guaranteed to be the same size as the other event types. - pub _reserved: [u8; 16], -} - -/// A midi event. -#[repr(C)] -pub struct MidiEvent { - /// Should be `EventType::Midi`. - pub event_type: EventType, - - /// Size of this structure; `mem::sizeof::()`. - pub byte_size: i32, - - /// Number of samples into the current processing block that this event occurs on. - /// - /// E.g. if the block size is 512 and this value is 123, the event will occur on sample - /// `samples[123]`. - pub delta_frames: i32, - - /// See `MidiEventFlags`. - pub flags: i32, - - /// Length in sample frames of entire note if available, otherwise 0. - pub note_length: i32, - - /// Offset in samples into note from start if available, otherwise 0. - pub note_offset: i32, - - /// 1 to 3 midi bytes. TODO: Doc - pub midi_data: [u8; 3], - - /// Reserved midi byte (0). - pub _midi_reserved: u8, - - /// Detuning between -63 and +64 cents, - /// for scales other than 'well-tempered'. e.g. 'microtuning' - pub detune: i8, - - /// Note off velocity between 0 and 127. - pub note_off_velocity: u8, - - /// Reserved for future use. Should be 0. - pub _reserved1: u8, - /// Reserved for future use. Should be 0. - pub _reserved2: u8, -} - -/// A midi system exclusive event. -/// -/// This event only contains raw byte data, and is up to the plugin to interpret it correctly. -/// `plugin::CanDo` has a `ReceiveSysExEvent` variant which lets the host query the plugin as to -/// whether this event is supported. -#[repr(C)] -#[derive(Clone)] -pub struct SysExEvent { - /// Should be `EventType::SysEx`. - pub event_type: EventType, - - /// Size of this structure; `mem::sizeof::()`. - pub byte_size: i32, - - /// Number of samples into the current processing block that this event occurs on. - /// - /// E.g. if the block size is 512 and this value is 123, the event will occur on sample - /// `samples[123]`. - pub delta_frames: i32, - - /// Generic flags, none defined in VST api yet. - pub _flags: i32, - - /// Size of payload in bytes. - pub data_size: i32, - - /// Reserved for future use. Should be 0. - pub _reserved1: isize, - - /// Pointer to payload. - pub system_data: *mut u8, - - /// Reserved for future use. Should be 0. - pub _reserved2: isize, -} - -unsafe impl Send for SysExEvent {} - -#[repr(C)] -#[derive(Clone, Default, Copy)] -/// Describes the time at the start of the block currently being processed -pub struct TimeInfo { - /// current Position in audio samples (always valid) - pub sample_pos: f64, - - /// current Sample Rate in Hertz (always valid) - pub sample_rate: f64, - - /// System Time in nanoseconds (10^-9 second) - pub nanoseconds: f64, - - /// Musical Position, in Quarter Note (1.0 equals 1 Quarter Note) - pub ppq_pos: f64, - - /// current Tempo in BPM (Beats Per Minute) - pub tempo: f64, - - /// last Bar Start Position, in Quarter Note - pub bar_start_pos: f64, - - /// Cycle Start (left locator), in Quarter Note - pub cycle_start_pos: f64, - - /// Cycle End (right locator), in Quarter Note - pub cycle_end_pos: f64, - - /// Time Signature Numerator (e.g. 3 for 3/4) - pub time_sig_numerator: i32, - - /// Time Signature Denominator (e.g. 4 for 3/4) - pub time_sig_denominator: i32, - - /// SMPTE offset in SMPTE subframes (bits; 1/80 of a frame). - /// The current SMPTE position can be calculated using `sample_pos`, `sample_rate`, and `smpte_frame_rate`. - pub smpte_offset: i32, - - /// See `SmpteFrameRate` - pub smpte_frame_rate: SmpteFrameRate, - - /// MIDI Clock Resolution (24 Per Quarter Note), can be negative (nearest clock) - pub samples_to_next_clock: i32, - - /// See `TimeInfoFlags` - pub flags: i32, -} - -#[repr(i32)] -#[derive(Copy, Clone, Debug)] -/// SMPTE Frame Rates. -pub enum SmpteFrameRate { - /// 24 fps - Smpte24fps = 0, - /// 25 fps - Smpte25fps = 1, - /// 29.97 fps - Smpte2997fps = 2, - /// 30 fps - Smpte30fps = 3, - - /// 29.97 drop - Smpte2997dfps = 4, - /// 30 drop - Smpte30dfps = 5, - - /// Film 16mm - SmpteFilm16mm = 6, - /// Film 35mm - SmpteFilm35mm = 7, - - /// HDTV: 23.976 fps - Smpte239fps = 10, - /// HDTV: 24.976 fps - Smpte249fps = 11, - /// HDTV: 59.94 fps - Smpte599fps = 12, - /// HDTV: 60 fps - Smpte60fps = 13, -} -impl Default for SmpteFrameRate { - fn default() -> Self { - SmpteFrameRate::Smpte24fps - } -} - -bitflags! { - /// Flags for VST channels. - pub struct ChannelFlags: i32 { - /// Indicates channel is active. Ignored by host. - const ACTIVE = 1; - /// Indicates channel is first of stereo pair. - const STEREO = 1 << 1; - /// Use channel's specified speaker_arrangement instead of stereo flag. - const SPEAKER = 1 << 2; - } -} - -bitflags! { - /// Flags for VST plugins. - pub struct PluginFlags: i32 { - /// Plugin has an editor. - const HAS_EDITOR = 1; - /// Plugin can process 32 bit audio. (Mandatory in VST 2.4). - const CAN_REPLACING = 1 << 4; - /// Plugin preset data is handled in formatless chunks. - const PROGRAM_CHUNKS = 1 << 5; - /// Plugin is a synth. - const IS_SYNTH = 1 << 8; - /// Plugin does not produce sound when all input is silence. - const NO_SOUND_IN_STOP = 1 << 9; - /// Supports 64 bit audio processing. - const CAN_DOUBLE_REPLACING = 1 << 12; - } -} - -bitflags! { - /// Cross platform modifier key flags. - pub struct ModifierKey: u8 { - /// Shift key. - const SHIFT = 1; - /// Alt key. - const ALT = 1 << 1; - /// Control on mac. - const COMMAND = 1 << 2; - /// Command on mac, ctrl on other. - const CONTROL = 1 << 3; // Ctrl on PC, Apple on Mac - } -} - -bitflags! { - /// MIDI event flags. - pub struct MidiEventFlags: i32 { - /// This event is played live (not in playback from a sequencer track). This allows the - /// plugin to handle these flagged events with higher priority, especially when the - /// plugin has a big latency as per `plugin::Info::initial_delay`. - const REALTIME_EVENT = 1; - } -} - -bitflags! { - /// Used in the `flags` field of `TimeInfo`, and for querying the host for specific values - pub struct TimeInfoFlags : i32 { - /// Indicates that play, cycle or record state has changed. - const TRANSPORT_CHANGED = 1; - /// Set if Host sequencer is currently playing. - const TRANSPORT_PLAYING = 1 << 1; - /// Set if Host sequencer is in cycle mode. - const TRANSPORT_CYCLE_ACTIVE = 1 << 2; - /// Set if Host sequencer is in record mode. - const TRANSPORT_RECORDING = 1 << 3; - - /// Set if automation write mode active (record parameter changes). - const AUTOMATION_WRITING = 1 << 6; - /// Set if automation read mode active (play parameter changes). - const AUTOMATION_READING = 1 << 7; - - /// Set if TimeInfo::nanoseconds is valid. - const NANOSECONDS_VALID = 1 << 8; - /// Set if TimeInfo::ppq_pos is valid. - const PPQ_POS_VALID = 1 << 9; - /// Set if TimeInfo::tempo is valid. - const TEMPO_VALID = 1 << 10; - /// Set if TimeInfo::bar_start_pos is valid. - const BARS_VALID = 1 << 11; - /// Set if both TimeInfo::cycle_start_pos and VstTimeInfo::cycle_end_pos are valid. - const CYCLE_POS_VALID = 1 << 12; - /// Set if both TimeInfo::time_sig_numerator and TimeInfo::time_sig_denominator are valid. - const TIME_SIG_VALID = 1 << 13; - /// Set if both TimeInfo::smpte_offset and VstTimeInfo::smpte_frame_rate are valid. - const SMPTE_VALID = 1 << 14; - /// Set if TimeInfo::samples_to_next_clock is valid. - const VST_CLOCK_VALID = 1 << 15; - } -} - -#[cfg(test)] -mod tests { - use super::super::event; - use super::*; - use std::mem; - - // This container is used because we have to store somewhere the events - // that are pointed to by raw pointers in the events object. We heap allocate - // the event so the pointer in events stays consistent when the container is moved. - pub struct EventContainer { - stored_event: Box, - pub events: Events, - } - - // A convenience method which creates an api::Events object representing a midi event. - // This represents code that might be found in a VST host using this API. - fn encode_midi_message_as_events(message: [u8; 3]) -> EventContainer { - let midi_event: MidiEvent = MidiEvent { - event_type: EventType::Midi, - byte_size: mem::size_of::() as i32, - delta_frames: 0, - flags: 0, - note_length: 0, - note_offset: 0, - midi_data: [message[0], message[1], message[2]], - _midi_reserved: 0, - detune: 0, - note_off_velocity: 0, - _reserved1: 0, - _reserved2: 0, - }; - let mut event: Event = unsafe { std::mem::transmute(midi_event) }; - event.event_type = EventType::Midi; - - let events = Events { - num_events: 1, - _reserved: 0, - events: [&mut event, &mut event], // Second one is a dummy - }; - let mut ec = EventContainer { - stored_event: Box::new(event), - events, - }; - ec.events.events[0] = &mut *(ec.stored_event); // Overwrite ptrs, since we moved the event into ec - ec - } - - #[test] - fn encode_and_decode_gives_back_original_message() { - let message: [u8; 3] = [35, 16, 22]; - let encoded = encode_midi_message_as_events(message); - assert_eq!(encoded.events.num_events, 1); - assert_eq!(encoded.events.events.len(), 2); - let e_vec: Vec = encoded.events.events().collect(); - assert_eq!(e_vec.len(), 1); - - match e_vec[0] { - event::Event::Midi(event::MidiEvent { data, .. }) => { - assert_eq!(data, message); - } - _ => { - panic!("Not a midi event!"); - } - }; - } - - // This is a regression test for a bug fixed in PR #93 - // We check here that calling events() on an api::Events object - // does not mutate the underlying events. - #[test] - fn message_survives_calling_events() { - let message: [u8; 3] = [35, 16, 22]; - let encoded = encode_midi_message_as_events(message); - - for e in encoded.events.events() { - match e { - event::Event::Midi(event::MidiEvent { data, .. }) => { - assert_eq!(data, message); - } - _ => { - panic!("Not a midi event!"); - } - } - } - - for e in encoded.events.events() { - match e { - event::Event::Midi(event::MidiEvent { data, .. }) => { - assert_eq!(data, message); - } - _ => { - panic!("Not a midi event!"); // FAILS here! - } - } - } - } -} diff --git a/deps/vst/src/buffer.rs b/deps/vst/src/buffer.rs deleted file mode 100644 index 9a32d789..00000000 --- a/deps/vst/src/buffer.rs +++ /dev/null @@ -1,606 +0,0 @@ -//! Buffers to safely work with audio samples. - -use num_traits::Float; - -use std::slice; - -/// `AudioBuffer` contains references to the audio buffers for all input and output channels. -/// -/// To create an `AudioBuffer` in a host, use a [`HostBuffer`](../host/struct.HostBuffer.html). -pub struct AudioBuffer<'a, T: 'a + Float> { - inputs: &'a [*const T], - outputs: &'a mut [*mut T], - samples: usize, -} - -impl<'a, T: 'a + Float> AudioBuffer<'a, T> { - /// Create an `AudioBuffer` from raw pointers. - /// Only really useful for interacting with the VST API. - #[inline] - pub unsafe fn from_raw( - input_count: usize, - output_count: usize, - inputs_raw: *const *const T, - outputs_raw: *mut *mut T, - samples: usize, - ) -> Self { - Self { - inputs: slice::from_raw_parts(inputs_raw, input_count), - outputs: slice::from_raw_parts_mut(outputs_raw, output_count), - samples, - } - } - - /// The number of input channels that this buffer was created for - #[inline] - pub fn input_count(&self) -> usize { - self.inputs.len() - } - - /// The number of output channels that this buffer was created for - #[inline] - pub fn output_count(&self) -> usize { - self.outputs.len() - } - - /// The number of samples in this buffer (same for all channels) - #[inline] - pub fn samples(&self) -> usize { - self.samples - } - - /// The raw inputs to pass to processReplacing - #[inline] - pub(crate) fn raw_inputs(&self) -> &[*const T] { - self.inputs - } - - /// The raw outputs to pass to processReplacing - #[inline] - pub(crate) fn raw_outputs(&mut self) -> &mut [*mut T] { - &mut self.outputs - } - - /// Split this buffer into separate inputs and outputs. - #[inline] - pub fn split<'b>(&'b mut self) -> (Inputs<'b, T>, Outputs<'b, T>) - where - 'a: 'b, - { - ( - Inputs { - bufs: self.inputs, - samples: self.samples, - }, - Outputs { - bufs: self.outputs, - samples: self.samples, - }, - ) - } - - /// Create an iterator over pairs of input buffers and output buffers. - #[inline] - pub fn zip<'b>(&'b mut self) -> AudioBufferIterator<'a, 'b, T> { - AudioBufferIterator { - audio_buffer: self, - index: 0, - } - } -} - -/// Iterator over pairs of buffers of input channels and output channels. -pub struct AudioBufferIterator<'a, 'b, T> -where - T: 'a + Float, - 'a: 'b, -{ - audio_buffer: &'b mut AudioBuffer<'a, T>, - index: usize, -} - -impl<'a, 'b, T> Iterator for AudioBufferIterator<'a, 'b, T> -where - T: 'b + Float, -{ - type Item = (&'b [T], &'b mut [T]); - - fn next(&mut self) -> Option { - if self.index < self.audio_buffer.inputs.len() && self.index < self.audio_buffer.outputs.len() { - let input = - unsafe { slice::from_raw_parts(self.audio_buffer.inputs[self.index], self.audio_buffer.samples) }; - let output = - unsafe { slice::from_raw_parts_mut(self.audio_buffer.outputs[self.index], self.audio_buffer.samples) }; - let val = (input, output); - self.index += 1; - Some(val) - } else { - None - } - } -} - -use std::ops::{Index, IndexMut}; - -/// Wrapper type to access the buffers for the input channels of an `AudioBuffer` in a safe way. -/// Behaves like a slice. -#[derive(Copy, Clone)] -pub struct Inputs<'a, T: 'a> { - bufs: &'a [*const T], - samples: usize, -} - -impl<'a, T> Inputs<'a, T> { - /// Number of channels - pub fn len(&self) -> usize { - self.bufs.len() - } - - /// Returns true if the buffer is empty - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - /// Access channel at the given index - pub fn get(&self, i: usize) -> &'a [T] { - unsafe { slice::from_raw_parts(self.bufs[i], self.samples) } - } - - /// Split borrowing at the given index, like for slices - pub fn split_at(&self, i: usize) -> (Inputs<'a, T>, Inputs<'a, T>) { - let (l, r) = self.bufs.split_at(i); - ( - Inputs { - bufs: l, - samples: self.samples, - }, - Inputs { - bufs: r, - samples: self.samples, - }, - ) - } -} - -impl<'a, T> Index for Inputs<'a, T> { - type Output = [T]; - - fn index(&self, i: usize) -> &Self::Output { - self.get(i) - } -} - -/// Iterator over buffers for input channels of an `AudioBuffer`. -pub struct InputIterator<'a, T: 'a> { - data: Inputs<'a, T>, - i: usize, -} - -impl<'a, T> Iterator for InputIterator<'a, T> { - type Item = &'a [T]; - - fn next(&mut self) -> Option { - if self.i < self.data.len() { - let val = self.data.get(self.i); - self.i += 1; - Some(val) - } else { - None - } - } -} - -impl<'a, T: Sized> IntoIterator for Inputs<'a, T> { - type Item = &'a [T]; - type IntoIter = InputIterator<'a, T>; - - fn into_iter(self) -> Self::IntoIter { - InputIterator { data: self, i: 0 } - } -} - -/// Wrapper type to access the buffers for the output channels of an `AudioBuffer` in a safe way. -/// Behaves like a slice. -pub struct Outputs<'a, T: 'a> { - bufs: &'a [*mut T], - samples: usize, -} - -impl<'a, T> Outputs<'a, T> { - /// Number of channels - pub fn len(&self) -> usize { - self.bufs.len() - } - - /// Returns true if the buffer is empty - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - /// Access channel at the given index - pub fn get(&self, i: usize) -> &'a [T] { - unsafe { slice::from_raw_parts(self.bufs[i], self.samples) } - } - - /// Mutably access channel at the given index - pub fn get_mut(&mut self, i: usize) -> &'a mut [T] { - unsafe { slice::from_raw_parts_mut(self.bufs[i], self.samples) } - } - - /// Split borrowing at the given index, like for slices - pub fn split_at_mut(self, i: usize) -> (Outputs<'a, T>, Outputs<'a, T>) { - let (l, r) = self.bufs.split_at(i); - ( - Outputs { - bufs: l, - samples: self.samples, - }, - Outputs { - bufs: r, - samples: self.samples, - }, - ) - } -} - -impl<'a, T> Index for Outputs<'a, T> { - type Output = [T]; - - fn index(&self, i: usize) -> &Self::Output { - self.get(i) - } -} - -impl<'a, T> IndexMut for Outputs<'a, T> { - fn index_mut(&mut self, i: usize) -> &mut Self::Output { - self.get_mut(i) - } -} - -/// Iterator over buffers for output channels of an `AudioBuffer`. -pub struct OutputIterator<'a, 'b, T> -where - T: 'a, - 'a: 'b, -{ - data: &'b mut Outputs<'a, T>, - i: usize, -} - -impl<'a, 'b, T> Iterator for OutputIterator<'a, 'b, T> -where - T: 'b, -{ - type Item = &'b mut [T]; - - fn next(&mut self) -> Option { - if self.i < self.data.len() { - let val = self.data.get_mut(self.i); - self.i += 1; - Some(val) - } else { - None - } - } -} - -impl<'a, 'b, T: Sized> IntoIterator for &'b mut Outputs<'a, T> { - type Item = &'b mut [T]; - type IntoIter = OutputIterator<'a, 'b, T>; - - fn into_iter(self) -> Self::IntoIter { - OutputIterator { data: self, i: 0 } - } -} - -use crate::event::{Event, MidiEvent, SysExEvent}; - -/// This is used as a placeholder to pre-allocate space for a fixed number of -/// midi events in the re-useable `SendEventBuffer`, because `SysExEvent` is -/// larger than `MidiEvent`, so either one can be stored in a `SysExEvent`. -pub type PlaceholderEvent = api::SysExEvent; - -/// This trait is used by `SendEventBuffer::send_events` to accept iterators over midi events -pub trait WriteIntoPlaceholder { - /// writes an event into the given placeholder memory location - fn write_into(&self, out: &mut PlaceholderEvent); -} - -impl<'a, T: WriteIntoPlaceholder> WriteIntoPlaceholder for &'a T { - fn write_into(&self, out: &mut PlaceholderEvent) { - (*self).write_into(out); - } -} - -impl WriteIntoPlaceholder for MidiEvent { - fn write_into(&self, out: &mut PlaceholderEvent) { - let out = unsafe { &mut *(out as *mut _ as *mut _) }; - *out = api::MidiEvent { - event_type: api::EventType::Midi, - byte_size: mem::size_of::() as i32, - delta_frames: self.delta_frames, - flags: if self.live { - api::MidiEventFlags::REALTIME_EVENT.bits() - } else { - 0 - }, - note_length: self.note_length.unwrap_or(0), - note_offset: self.note_offset.unwrap_or(0), - midi_data: self.data, - _midi_reserved: 0, - detune: self.detune, - note_off_velocity: self.note_off_velocity, - _reserved1: 0, - _reserved2: 0, - }; - } -} - -impl<'a> WriteIntoPlaceholder for SysExEvent<'a> { - fn write_into(&self, out: &mut PlaceholderEvent) { - *out = PlaceholderEvent { - event_type: api::EventType::SysEx, - byte_size: mem::size_of::() as i32, - delta_frames: self.delta_frames, - _flags: 0, - data_size: self.payload.len() as i32, - _reserved1: 0, - system_data: self.payload.as_ptr() as *const u8 as *mut u8, - _reserved2: 0, - }; - } -} - -impl<'a> WriteIntoPlaceholder for Event<'a> { - fn write_into(&self, out: &mut PlaceholderEvent) { - match *self { - Event::Midi(ref ev) => { - ev.write_into(out); - } - Event::SysEx(ref ev) => { - ev.write_into(out); - } - Event::Deprecated(e) => { - let out = unsafe { &mut *(out as *mut _ as *mut _) }; - *out = e; - } - }; - } -} - -use crate::{api, host::Host}; -use std::mem; - -/// This buffer is used for sending midi events through the VST interface. -/// The purpose of this is to convert outgoing midi events from `event::Event` to `api::Events`. -/// It only allocates memory in new() and reuses the memory between calls. -pub struct SendEventBuffer { - buf: Vec, - api_events: Vec, // using SysExEvent to store both because it's larger than MidiEvent -} - -impl Default for SendEventBuffer { - fn default() -> Self { - SendEventBuffer::new(1024) - } -} - -impl SendEventBuffer { - /// Creates a buffer for sending up to the given number of midi events per frame - #[inline(always)] - pub fn new(capacity: usize) -> Self { - let header_size = mem::size_of::() - (mem::size_of::<*mut api::Event>() * 2); - let body_size = mem::size_of::<*mut api::Event>() * capacity; - let mut buf = vec![0u8; header_size + body_size]; - let api_events = vec![unsafe { mem::zeroed::() }; capacity]; - { - let ptrs = { - let e = Self::buf_as_api_events(&mut buf); - e.num_events = capacity as i32; - e.events_raw_mut() - }; - for (ptr, event) in ptrs.iter_mut().zip(&api_events) { - let (ptr, event): (&mut *const PlaceholderEvent, &PlaceholderEvent) = (ptr, event); - *ptr = event; - } - } - Self { buf, api_events } - } - - /// Sends events to the host. See the `fwd_midi` example. - /// - /// # Example - /// ```no_run - /// # use vst::plugin::{Info, Plugin, HostCallback}; - /// # use vst::buffer::{AudioBuffer, SendEventBuffer}; - /// # use vst::host::Host; - /// # use vst::event::*; - /// # struct ExamplePlugin { host: HostCallback, send_buffer: SendEventBuffer } - /// # impl Plugin for ExamplePlugin { - /// # fn new(host: HostCallback) -> Self { Self { host, send_buffer: Default::default() } } - /// # - /// # fn get_info(&self) -> Info { Default::default() } - /// # - /// fn process(&mut self, buffer: &mut AudioBuffer){ - /// let events: Vec = vec![ - /// // ... - /// ]; - /// self.send_buffer.send_events(&events, &mut self.host); - /// } - /// # } - /// ``` - #[inline(always)] - pub fn send_events, U: WriteIntoPlaceholder>(&mut self, events: T, host: &mut dyn Host) { - self.store_events(events); - host.process_events(self.events()); - } - - /// Stores events in the buffer, replacing the buffer's current content. - /// Use this in [`process_events`](crate::Plugin::process_events) to store received input events, then read them in [`process`](crate::Plugin::process) using [`events`](SendEventBuffer::events). - #[inline(always)] - pub fn store_events, U: WriteIntoPlaceholder>(&mut self, events: T) { - #[allow(clippy::suspicious_map)] - let count = events - .into_iter() - .zip(self.api_events.iter_mut()) - .map(|(ev, out)| ev.write_into(out)) - .count(); - self.set_num_events(count); - } - - /// Returns a reference to the stored events - #[inline(always)] - pub fn events(&self) -> &api::Events { - #[allow(clippy::cast_ptr_alignment)] - unsafe { - &*(self.buf.as_ptr() as *const api::Events) - } - } - - /// Clears the buffer - #[inline(always)] - pub fn clear(&mut self) { - self.set_num_events(0); - } - - #[inline(always)] - fn buf_as_api_events(buf: &mut [u8]) -> &mut api::Events { - #[allow(clippy::cast_ptr_alignment)] - unsafe { - &mut *(buf.as_mut_ptr() as *mut api::Events) - } - } - - #[inline(always)] - fn set_num_events(&mut self, events_len: usize) { - use std::cmp::min; - let e = Self::buf_as_api_events(&mut self.buf); - e.num_events = min(self.api_events.len(), events_len) as i32; - } -} - -#[cfg(test)] -mod tests { - use crate::buffer::AudioBuffer; - - /// Size of buffers used in tests. - const SIZE: usize = 1024; - - /// Test that creating and zipping buffers works. - /// - /// This test creates a channel for 2 inputs and 2 outputs. - /// The input channels are simply values - /// from 0 to `SIZE-1` (e.g. [0, 1, 2, 3, 4, .. , SIZE - 1]) - /// and the output channels are just 0. - /// This test assures that when the buffers are zipped together, - /// the input values do not change. - #[test] - fn buffer_zip() { - let in1: Vec = (0..SIZE).map(|x| x as f32).collect(); - let in2 = in1.clone(); - - let mut out1 = vec![0.0; SIZE]; - let mut out2 = out1.clone(); - - let inputs = vec![in1.as_ptr(), in2.as_ptr()]; - let mut outputs = vec![out1.as_mut_ptr(), out2.as_mut_ptr()]; - let mut buffer = unsafe { AudioBuffer::from_raw(2, 2, inputs.as_ptr(), outputs.as_mut_ptr(), SIZE) }; - - for (input, output) in buffer.zip() { - input.iter().zip(output.iter_mut()).fold(0, |acc, (input, output)| { - assert_eq!(*input, acc as f32); - assert_eq!(*output, 0.0); - acc + 1 - }); - } - } - - // Test that the `zip()` method returns an iterator that gives `n` elements - // where n is the number of inputs when this is lower than the number of outputs. - #[test] - fn buffer_zip_fewer_inputs_than_outputs() { - let in1 = vec![1.0; SIZE]; - let in2 = vec![2.0; SIZE]; - - let mut out1 = vec![3.0; SIZE]; - let mut out2 = vec![4.0; SIZE]; - let mut out3 = vec![5.0; SIZE]; - - let inputs = vec![in1.as_ptr(), in2.as_ptr()]; - let mut outputs = vec![out1.as_mut_ptr(), out2.as_mut_ptr(), out3.as_mut_ptr()]; - let mut buffer = unsafe { AudioBuffer::from_raw(2, 3, inputs.as_ptr(), outputs.as_mut_ptr(), SIZE) }; - - let mut iter = buffer.zip(); - if let Some((observed_in1, observed_out1)) = iter.next() { - assert_eq!(1.0, observed_in1[0]); - assert_eq!(3.0, observed_out1[0]); - } else { - unreachable!(); - } - - if let Some((observed_in2, observed_out2)) = iter.next() { - assert_eq!(2.0, observed_in2[0]); - assert_eq!(4.0, observed_out2[0]); - } else { - unreachable!(); - } - - assert_eq!(None, iter.next()); - } - - // Test that the `zip()` method returns an iterator that gives `n` elements - // where n is the number of outputs when this is lower than the number of inputs. - #[test] - fn buffer_zip_more_inputs_than_outputs() { - let in1 = vec![1.0; SIZE]; - let in2 = vec![2.0; SIZE]; - let in3 = vec![3.0; SIZE]; - - let mut out1 = vec![4.0; SIZE]; - let mut out2 = vec![5.0; SIZE]; - - let inputs = vec![in1.as_ptr(), in2.as_ptr(), in3.as_ptr()]; - let mut outputs = vec![out1.as_mut_ptr(), out2.as_mut_ptr()]; - let mut buffer = unsafe { AudioBuffer::from_raw(3, 2, inputs.as_ptr(), outputs.as_mut_ptr(), SIZE) }; - - let mut iter = buffer.zip(); - - if let Some((observed_in1, observed_out1)) = iter.next() { - assert_eq!(1.0, observed_in1[0]); - assert_eq!(4.0, observed_out1[0]); - } else { - unreachable!(); - } - - if let Some((observed_in2, observed_out2)) = iter.next() { - assert_eq!(2.0, observed_in2[0]); - assert_eq!(5.0, observed_out2[0]); - } else { - unreachable!(); - } - - assert_eq!(None, iter.next()); - } - - /// Test that creating buffers from raw pointers works. - #[test] - fn from_raw() { - let in1: Vec = (0..SIZE).map(|x| x as f32).collect(); - let in2 = in1.clone(); - - let mut out1 = vec![0.0; SIZE]; - let mut out2 = out1.clone(); - - let inputs = vec![in1.as_ptr(), in2.as_ptr()]; - let mut outputs = vec![out1.as_mut_ptr(), out2.as_mut_ptr()]; - let mut buffer = unsafe { AudioBuffer::from_raw(2, 2, inputs.as_ptr(), outputs.as_mut_ptr(), SIZE) }; - - for (input, output) in buffer.zip() { - input.iter().zip(output.iter_mut()).fold(0, |acc, (input, output)| { - assert_eq!(*input, acc as f32); - assert_eq!(*output, 0.0); - acc + 1 - }); - } - } -} diff --git a/deps/vst/src/cache.rs b/deps/vst/src/cache.rs deleted file mode 100644 index f6a1fd2e..00000000 --- a/deps/vst/src/cache.rs +++ /dev/null @@ -1,19 +0,0 @@ -use std::sync::Arc; - -use crate::{editor::Editor, prelude::*}; - -pub(crate) struct PluginCache { - pub info: Info, - pub params: Arc, - pub editor: Option>, -} - -impl PluginCache { - pub fn new(info: &Info, params: Arc, editor: Option>) -> Self { - Self { - info: info.clone(), - params, - editor, - } - } -} diff --git a/deps/vst/src/channels.rs b/deps/vst/src/channels.rs deleted file mode 100644 index e72879fd..00000000 --- a/deps/vst/src/channels.rs +++ /dev/null @@ -1,352 +0,0 @@ -//! Meta data for dealing with input / output channels. Not all hosts use this so it is not -//! necessary for plugin functionality. - -use crate::api; -use crate::api::consts::{MAX_LABEL, MAX_SHORT_LABEL}; - -/// Information about an input / output channel. This isn't necessary for a channel to function but -/// informs the host how the channel is meant to be used. -pub struct ChannelInfo { - name: String, - short_name: String, - active: bool, - arrangement_type: SpeakerArrangementType, -} - -impl ChannelInfo { - /// Construct a new `ChannelInfo` object. - /// - /// `name` is a user friendly name for this channel limited to `MAX_LABEL` characters. - /// `short_name` is an optional field which provides a short name limited to `MAX_SHORT_LABEL`. - /// `active` determines whether this channel is active. - /// `arrangement_type` describes the arrangement type for this channel. - pub fn new( - name: String, - short_name: Option, - active: bool, - arrangement_type: Option, - ) -> ChannelInfo { - ChannelInfo { - name: name.clone(), - - short_name: if let Some(short_name) = short_name { - short_name - } else { - name - }, - - active, - - arrangement_type: arrangement_type.unwrap_or(SpeakerArrangementType::Custom), - } - } -} - -impl Into for ChannelInfo { - /// Convert to the VST api equivalent of this structure. - fn into(self) -> api::ChannelProperties { - api::ChannelProperties { - name: { - let mut label = [0; MAX_LABEL as usize]; - for (b, c) in self.name.bytes().zip(label.iter_mut()) { - *c = b; - } - label - }, - flags: { - let mut flag = api::ChannelFlags::empty(); - if self.active { - flag |= api::ChannelFlags::ACTIVE - } - if self.arrangement_type.is_left_stereo() { - flag |= api::ChannelFlags::STEREO - } - if self.arrangement_type.is_speaker_type() { - flag |= api::ChannelFlags::SPEAKER - } - flag.bits() - }, - arrangement_type: self.arrangement_type.into(), - short_name: { - let mut label = [0; MAX_SHORT_LABEL as usize]; - for (b, c) in self.short_name.bytes().zip(label.iter_mut()) { - *c = b; - } - label - }, - future: [0; 48], - } - } -} - -impl From for ChannelInfo { - fn from(api: api::ChannelProperties) -> ChannelInfo { - ChannelInfo { - name: String::from_utf8_lossy(&api.name).to_string(), - short_name: String::from_utf8_lossy(&api.short_name).to_string(), - active: api::ChannelFlags::from_bits(api.flags) - .expect("Invalid bits in channel info") - .intersects(api::ChannelFlags::ACTIVE), - arrangement_type: SpeakerArrangementType::from(api), - } - } -} - -/// Target for Speaker arrangement type. Can be a cinema configuration or music configuration. Both -/// are technically identical but this provides extra information to the host. -pub enum ArrangementTarget { - /// Music arrangement. Technically identical to Cinema. - Music, - /// Cinematic arrangement. Technically identical to Music. - Cinema, -} - -/// An enum for all channels in a stereo configuration. -pub enum StereoChannel { - /// Left channel. - Left, - /// Right channel. - Right, -} - -/// Possible stereo speaker configurations. -#[allow(non_camel_case_types)] -pub enum StereoConfig { - /// Regular. - L_R, - /// Left surround, right surround. - Ls_Rs, - /// Left center, right center. - Lc_Rc, - /// Side left, side right. - Sl_Sr, - /// Center, low frequency effects. - C_Lfe, -} - -/// Possible surround speaker configurations. -#[allow(non_camel_case_types)] -pub enum SurroundConfig { - /// 3.0 surround sound. - /// Cinema: L R C - /// Music: L R S - S3_0(ArrangementTarget), - /// 3.1 surround sound. - /// Cinema: L R C Lfe - /// Music: L R Lfe S - S3_1(ArrangementTarget), - /// 4.0 surround sound. - /// Cinema: L R C S (LCRS) - /// Music: L R Ls Rs (Quadro) - S4_0(ArrangementTarget), - /// 4.1 surround sound. - /// Cinema: L R C Lfe S (LCRS + Lfe) - /// Music: L R Ls Rs (Quadro + Lfe) - S4_1(ArrangementTarget), - /// 5.0 surround sound. - /// Cinema and music: L R C Ls Rs - S5_0, - /// 5.1 surround sound. - /// Cinema and music: L R C Lfe Ls Rs - S5_1, - /// 6.0 surround sound. - /// Cinema: L R C Ls Rs Cs - /// Music: L R Ls Rs Sl Sr - S6_0(ArrangementTarget), - /// 6.1 surround sound. - /// Cinema: L R C Lfe Ls Rs Cs - /// Music: L R Ls Rs Sl Sr - S6_1(ArrangementTarget), - /// 7.0 surround sound. - /// Cinema: L R C Ls Rs Lc Rc - /// Music: L R C Ls Rs Sl Sr - S7_0(ArrangementTarget), - /// 7.1 surround sound. - /// Cinema: L R C Lfe Ls Rs Lc Rc - /// Music: L R C Lfe Ls Rs Sl Sr - S7_1(ArrangementTarget), - /// 8.0 surround sound. - /// Cinema: L R C Ls Rs Lc Rc Cs - /// Music: L R C Ls Rs Cs Sl Sr - S8_0(ArrangementTarget), - /// 8.1 surround sound. - /// Cinema: L R C Lfe Ls Rs Lc Rc Cs - /// Music: L R C Lfe Ls Rs Cs Sl Sr - S8_1(ArrangementTarget), - /// 10.2 surround sound. - /// Cinema + Music: L R C Lfe Ls Rs Tfl Tfc Tfr Trl Trr Lfe2 - S10_2, -} - -/// Type representing how a channel is used. Only useful for some hosts. -pub enum SpeakerArrangementType { - /// Custom arrangement not specified to host. - Custom, - /// Empty arrangement. - Empty, - /// Mono channel. - Mono, - /// Stereo channel. Contains type of stereo arrangement and speaker represented. - Stereo(StereoConfig, StereoChannel), - /// Surround channel. Contains surround arrangement and target (cinema or music). - Surround(SurroundConfig), -} - -impl Default for SpeakerArrangementType { - fn default() -> SpeakerArrangementType { - SpeakerArrangementType::Mono - } -} - -impl SpeakerArrangementType { - /// Determine whether this channel is part of a surround speaker arrangement. - pub fn is_speaker_type(&self) -> bool { - if let SpeakerArrangementType::Surround(..) = *self { - true - } else { - false - } - } - - /// Determine whether this channel is the left speaker in a stereo pair. - pub fn is_left_stereo(&self) -> bool { - if let SpeakerArrangementType::Stereo(_, StereoChannel::Left) = *self { - true - } else { - false - } - } -} - -impl Into for SpeakerArrangementType { - /// Convert to VST API arrangement type. - fn into(self) -> api::SpeakerArrangementType { - use self::ArrangementTarget::{Cinema, Music}; - use self::SpeakerArrangementType::*; - use api::SpeakerArrangementType as Raw; - - match self { - Custom => Raw::Custom, - Empty => Raw::Empty, - Mono => Raw::Mono, - Stereo(conf, _) => { - match conf { - // Stereo channels. - StereoConfig::L_R => Raw::Stereo, - StereoConfig::Ls_Rs => Raw::StereoSurround, - StereoConfig::Lc_Rc => Raw::StereoCenter, - StereoConfig::Sl_Sr => Raw::StereoSide, - StereoConfig::C_Lfe => Raw::StereoCLfe, - } - } - Surround(conf) => { - match conf { - // Surround channels. - SurroundConfig::S3_0(Music) => Raw::Music30, - SurroundConfig::S3_0(Cinema) => Raw::Cinema30, - - SurroundConfig::S3_1(Music) => Raw::Music31, - SurroundConfig::S3_1(Cinema) => Raw::Cinema31, - - SurroundConfig::S4_0(Music) => Raw::Music40, - SurroundConfig::S4_0(Cinema) => Raw::Cinema40, - - SurroundConfig::S4_1(Music) => Raw::Music41, - SurroundConfig::S4_1(Cinema) => Raw::Cinema41, - - SurroundConfig::S5_0 => Raw::Surround50, - SurroundConfig::S5_1 => Raw::Surround51, - - SurroundConfig::S6_0(Music) => Raw::Music60, - SurroundConfig::S6_0(Cinema) => Raw::Cinema60, - - SurroundConfig::S6_1(Music) => Raw::Music61, - SurroundConfig::S6_1(Cinema) => Raw::Cinema61, - - SurroundConfig::S7_0(Music) => Raw::Music70, - SurroundConfig::S7_0(Cinema) => Raw::Cinema70, - - SurroundConfig::S7_1(Music) => Raw::Music71, - SurroundConfig::S7_1(Cinema) => Raw::Cinema71, - - SurroundConfig::S8_0(Music) => Raw::Music80, - SurroundConfig::S8_0(Cinema) => Raw::Cinema80, - - SurroundConfig::S8_1(Music) => Raw::Music81, - SurroundConfig::S8_1(Cinema) => Raw::Cinema81, - - SurroundConfig::S10_2 => Raw::Surround102, - } - } - } - } -} - -/// Convert the VST API equivalent struct into something more usable. -/// -/// We implement `From` as `SpeakerArrangementType` contains extra info about -/// stereo speakers found in the channel flags. -impl From for SpeakerArrangementType { - fn from(api: api::ChannelProperties) -> SpeakerArrangementType { - use self::ArrangementTarget::{Cinema, Music}; - use self::SpeakerArrangementType::*; - use self::SurroundConfig::*; - use api::SpeakerArrangementType as Raw; - - let stereo = if api::ChannelFlags::from_bits(api.flags) - .expect("Invalid Channel Flags") - .intersects(api::ChannelFlags::STEREO) - { - StereoChannel::Left - } else { - StereoChannel::Right - }; - - match api.arrangement_type { - Raw::Custom => Custom, - Raw::Empty => Empty, - Raw::Mono => Mono, - - Raw::Stereo => Stereo(StereoConfig::L_R, stereo), - Raw::StereoSurround => Stereo(StereoConfig::Ls_Rs, stereo), - Raw::StereoCenter => Stereo(StereoConfig::Lc_Rc, stereo), - Raw::StereoSide => Stereo(StereoConfig::Sl_Sr, stereo), - Raw::StereoCLfe => Stereo(StereoConfig::C_Lfe, stereo), - - Raw::Music30 => Surround(S3_0(Music)), - Raw::Cinema30 => Surround(S3_0(Cinema)), - - Raw::Music31 => Surround(S3_1(Music)), - Raw::Cinema31 => Surround(S3_1(Cinema)), - - Raw::Music40 => Surround(S4_0(Music)), - Raw::Cinema40 => Surround(S4_0(Cinema)), - - Raw::Music41 => Surround(S4_1(Music)), - Raw::Cinema41 => Surround(S4_1(Cinema)), - - Raw::Surround50 => Surround(S5_0), - Raw::Surround51 => Surround(S5_1), - - Raw::Music60 => Surround(S6_0(Music)), - Raw::Cinema60 => Surround(S6_0(Cinema)), - - Raw::Music61 => Surround(S6_1(Music)), - Raw::Cinema61 => Surround(S6_1(Cinema)), - - Raw::Music70 => Surround(S7_0(Music)), - Raw::Cinema70 => Surround(S7_0(Cinema)), - - Raw::Music71 => Surround(S7_1(Music)), - Raw::Cinema71 => Surround(S7_1(Cinema)), - - Raw::Music80 => Surround(S8_0(Music)), - Raw::Cinema80 => Surround(S8_0(Cinema)), - - Raw::Music81 => Surround(S8_1(Music)), - Raw::Cinema81 => Surround(S8_1(Cinema)), - - Raw::Surround102 => Surround(S10_2), - } - } -} diff --git a/deps/vst/src/editor.rs b/deps/vst/src/editor.rs deleted file mode 100644 index 923cf873..00000000 --- a/deps/vst/src/editor.rs +++ /dev/null @@ -1,155 +0,0 @@ -//! All VST plugin editor related functionality. - -use num_enum::{IntoPrimitive, TryFromPrimitive}; - -use std::os::raw::c_void; - -/// Implemented by plugin editors. -#[allow(unused_variables)] -pub trait Editor { - /// Get the size of the editor window. - fn size(&self) -> (i32, i32); - - /// Get the coordinates of the editor window. - fn position(&self) -> (i32, i32); - - /// Editor idle call. Called by host. - fn idle(&mut self) {} - - /// Called when the editor window is closed. - fn close(&mut self) {} - - /// Called when the editor window is opened. - /// - /// `parent` is a window pointer that the new window should attach itself to. - /// **It is dependent upon the platform you are targeting.** - /// - /// A few examples: - /// - /// - On Windows, it should be interpreted as a `HWND` - /// - On Mac OS X (64 bit), it should be interpreted as a `NSView*` - /// - On X11 platforms, it should be interpreted as a `u32` (the ID number of the parent window) - /// - /// Return `true` if the window opened successfully, `false` otherwise. - fn open(&mut self, parent: *mut c_void) -> bool; - - /// Return whether the window is currently open. - fn is_open(&mut self) -> bool; - - /// Set the knob mode for this editor (if supported by host). - /// - /// Return `true` if the knob mode was set. - fn set_knob_mode(&mut self, mode: KnobMode) -> bool { - false - } - - /// Receive key up event. Return `true` if the key was used. - fn key_up(&mut self, keycode: KeyCode) -> bool { - false - } - - /// Receive key down event. Return `true` if the key was used. - fn key_down(&mut self, keycode: KeyCode) -> bool { - false - } -} - -/// Rectangle used to specify dimensions of editor window. -#[doc(hidden)] -#[derive(Copy, Clone, Debug)] -pub struct Rect { - /// Y value in pixels of top side. - pub top: i16, - /// X value in pixels of left side. - pub left: i16, - /// Y value in pixels of bottom side. - pub bottom: i16, - /// X value in pixels of right side. - pub right: i16, -} - -/// A platform independent key code. Includes modifier keys. -#[derive(Copy, Clone, Debug)] -pub struct KeyCode { - /// ASCII character for key pressed (if applicable). - pub character: char, - /// Key pressed. See `enums::Key`. - pub key: Key, - /// Modifier key bitflags. See `enums::flags::modifier_key`. - pub modifier: u8, -} - -/// Allows host to set how a parameter knob works. -#[repr(isize)] -#[derive(Copy, Clone, Debug, TryFromPrimitive, IntoPrimitive)] -#[allow(missing_docs)] -pub enum KnobMode { - Circular, - CircularRelative, - Linear, -} - -/// Platform independent key codes. -#[allow(missing_docs)] -#[repr(isize)] -#[derive(Debug, Copy, Clone, TryFromPrimitive, IntoPrimitive)] -pub enum Key { - None = 0, - Back, - Tab, - Clear, - Return, - Pause, - Escape, - Space, - Next, - End, - Home, - Left, - Up, - Right, - Down, - PageUp, - PageDown, - Select, - Print, - Enter, - Snapshot, - Insert, - Delete, - Help, - Numpad0, - Numpad1, - Numpad2, - Numpad3, - Numpad4, - Numpad5, - Numpad6, - Numpad7, - Numpad8, - Numpad9, - Multiply, - Add, - Separator, - Subtract, - Decimal, - Divide, - F1, - F2, - F3, - F4, - F5, - F6, - F7, - F8, - F9, - F10, - F11, - F12, - Numlock, - Scroll, - Shift, - Control, - Alt, - Equals, -} diff --git a/deps/vst/src/event.rs b/deps/vst/src/event.rs deleted file mode 100644 index 580fd54a..00000000 --- a/deps/vst/src/event.rs +++ /dev/null @@ -1,133 +0,0 @@ -//! Interfaces to VST events. -// TODO: Update and explain both host and plugin events - -use std::{mem, slice}; - -use crate::api; - -/// A VST event. -#[derive(Copy, Clone)] -pub enum Event<'a> { - /// A midi event. - /// - /// These are sent to the plugin before `Plugin::processing()` or `Plugin::processing_f64()` is - /// called. - Midi(MidiEvent), - - /// A system exclusive event. - /// - /// This is just a block of data and it is up to the plugin to interpret this. Generally used - /// by midi controllers. - SysEx(SysExEvent<'a>), - - /// A deprecated event. - /// - /// Passes the raw midi event structure along with this so that implementors can handle - /// optionally handle this event. - Deprecated(api::Event), -} - -/// A midi event. -/// -/// These are sent to the plugin before `Plugin::processing()` or `Plugin::processing_f64()` is -/// called. -#[derive(Copy, Clone)] -pub struct MidiEvent { - /// The raw midi data associated with this event. - pub data: [u8; 3], - - /// Number of samples into the current processing block that this event occurs on. - /// - /// E.g. if the block size is 512 and this value is 123, the event will occur on sample - /// `samples[123]`. - // TODO: Don't repeat this value in all event types - pub delta_frames: i32, - - /// This midi event was created live as opposed to being played back in the sequencer. - /// - /// This can give the plugin priority over this event if it introduces a lot of latency. - pub live: bool, - - /// The length of the midi note associated with this event, if available. - pub note_length: Option, - - /// Offset in samples into note from note start, if available. - pub note_offset: Option, - - /// Detuning between -63 and +64 cents. - pub detune: i8, - - /// Note off velocity between 0 and 127. - pub note_off_velocity: u8, -} - -/// A system exclusive event. -/// -/// This is just a block of data and it is up to the plugin to interpret this. Generally used -/// by midi controllers. -#[derive(Copy, Clone)] -pub struct SysExEvent<'a> { - /// The SysEx payload. - pub payload: &'a [u8], - - /// Number of samples into the current processing block that this event occurs on. - /// - /// E.g. if the block size is 512 and this value is 123, the event will occur on sample - /// `samples[123]`. - pub delta_frames: i32, -} - -impl<'a> Event<'a> { - /// Creates a high-level event from the given low-level API event. - /// - /// # Safety - /// - /// You must ensure that the given pointer refers to a valid event of the correct event type. - /// For example, if the event type is [`api::EventType::SysEx`], it should point to a - /// [`SysExEvent`]. In case of a [`SysExEvent`], `system_data` and `data_size` must be correct. - pub unsafe fn from_raw_event(event: *const api::Event) -> Event<'a> { - use api::EventType::*; - let event = &*event; - match event.event_type { - Midi => { - let event: api::MidiEvent = mem::transmute(*event); - - let length = if event.note_length > 0 { - Some(event.note_length) - } else { - None - }; - let offset = if event.note_offset > 0 { - Some(event.note_offset) - } else { - None - }; - let flags = api::MidiEventFlags::from_bits(event.flags).unwrap(); - - Event::Midi(MidiEvent { - data: event.midi_data, - delta_frames: event.delta_frames, - live: flags.intersects(api::MidiEventFlags::REALTIME_EVENT), - note_length: length, - note_offset: offset, - detune: event.detune, - note_off_velocity: event.note_off_velocity, - }) - } - - SysEx => Event::SysEx(SysExEvent { - payload: { - // We can safely cast the event pointer to a `SysExEvent` pointer as - // event_type refers to a `SysEx` type. - #[allow(clippy::cast_ptr_alignment)] - let event: &api::SysExEvent = &*(event as *const api::Event as *const api::SysExEvent); - slice::from_raw_parts(event.system_data, event.data_size as usize) - }, - - delta_frames: event.delta_frames, - }), - - _ => Event::Deprecated(*event), - } - } -} diff --git a/deps/vst/src/host.rs b/deps/vst/src/host.rs deleted file mode 100644 index 2ed684f6..00000000 --- a/deps/vst/src/host.rs +++ /dev/null @@ -1,962 +0,0 @@ -//! Host specific structures. - -use num_enum::{IntoPrimitive, TryFromPrimitive}; -use num_traits::Float; - -use libloading::Library; -use std::cell::UnsafeCell; -use std::convert::TryFrom; -use std::error::Error; -use std::ffi::CString; -use std::mem::MaybeUninit; -use std::os::raw::c_void; -use std::path::Path; -use std::sync::{Arc, Mutex}; -use std::{fmt, ptr, slice}; - -use crate::{ - api::{self, consts::*, AEffect, PluginFlags, PluginMain, Supported, TimeInfo}, - buffer::AudioBuffer, - channels::ChannelInfo, - editor::{Editor, Rect}, - interfaces, - plugin::{self, Category, HostCallback, Info, Plugin, PluginParameters}, -}; - -#[repr(i32)] -#[derive(Clone, Copy, Debug, TryFromPrimitive, IntoPrimitive)] -#[doc(hidden)] -pub enum OpCode { - /// [index]: parameter index - /// [opt]: parameter value - Automate = 0, - /// [return]: host vst version (e.g. 2400 for VST 2.4) - Version, - /// [return]: current plugin ID (useful for shell plugins to figure out which plugin to load in - /// `VSTPluginMain()`). - CurrentId, - /// No arguments. Give idle time to Host application, e.g. if plug-in editor is doing mouse - /// tracking in a modal loop. - Idle, - /// Deprecated. - _PinConnected = 4, - - /// Deprecated. - _WantMidi = 6, // Not a typo - /// [value]: request mask. see `VstTimeInfoFlags` - /// [return]: `VstTimeInfo` pointer or null if not supported. - GetTime, - /// Inform host that the plugin has MIDI events ready to be processed. Should be called at the - /// end of `Plugin::process`. - /// [ptr]: `VstEvents*` the events to be processed. - /// [return]: 1 if supported and processed OK. - ProcessEvents, - /// Deprecated. - _SetTime, - /// Deprecated. - _TempoAt, - /// Deprecated. - _GetNumAutomatableParameters, - /// Deprecated. - _GetParameterQuantization, - - /// Notifies the host that the input/output setup has changed. This can allow the host to check - /// numInputs/numOutputs or call `getSpeakerArrangement()`. - /// [return]: 1 if supported. - IOChanged, - - /// Deprecated. - _NeedIdle, - - /// Request the host to resize the plugin window. - /// [index]: new width. - /// [value]: new height. - SizeWindow, - /// [return]: the current sample rate. - GetSampleRate, - /// [return]: the current block size. - GetBlockSize, - /// [return]: the input latency in samples. - GetInputLatency, - /// [return]: the output latency in samples. - GetOutputLatency, - - /// Deprecated. - _GetPreviousPlug, - /// Deprecated. - _GetNextPlug, - /// Deprecated. - _WillReplaceOrAccumulate, - - /// [return]: the current process level, see `VstProcessLevels` - GetCurrentProcessLevel, - /// [return]: the current automation state, see `VstAutomationStates` - GetAutomationState, - - /// The plugin is ready to begin offline processing. - /// [index]: number of new audio files. - /// [value]: number of audio files. - /// [ptr]: `AudioFile*` the host audio files. Flags can be updated from plugin. - OfflineStart, - /// Called by the plugin to read data. - /// [index]: (bool) - /// VST offline processing allows a plugin to overwrite existing files. If this value is - /// true then the host will read the original file's samples, but if it is false it will - /// read the samples which the plugin has written via `OfflineWrite` - /// [value]: see `OfflineOption` - /// [ptr]: `OfflineTask*` describing the task. - /// [return]: 1 on success - OfflineRead, - /// Called by the plugin to write data. - /// [value]: see `OfflineOption` - /// [ptr]: `OfflineTask*` describing the task. - OfflineWrite, - /// Unknown. Used in offline processing. - OfflineGetCurrentPass, - /// Unknown. Used in offline processing. - OfflineGetCurrentMetaPass, - - /// Deprecated. - _SetOutputSampleRate, - /// Deprecated. - _GetOutputSpeakerArrangement, - - /// Get the vendor string. - /// [ptr]: `char*` for vendor string, limited to `MAX_VENDOR_STR_LEN`. - GetVendorString, - /// Get the product string. - /// [ptr]: `char*` for vendor string, limited to `MAX_PRODUCT_STR_LEN`. - GetProductString, - /// [return]: vendor-specific version - GetVendorVersion, - /// Vendor specific handling. - VendorSpecific, - - /// Deprecated. - _SetIcon, - - /// Check if the host supports a feature. - /// [ptr]: `char*` can do string - /// [return]: 1 if supported - CanDo, - /// Get the language of the host. - /// [return]: `VstHostLanguage` - GetLanguage, - - /// Deprecated. - _OpenWindow, - /// Deprecated. - _CloseWindow, - - /// Get the current directory. - /// [return]: `FSSpec` on OS X, `char*` otherwise - GetDirectory, - /// Tell the host that the plugin's parameters have changed, refresh the UI. - /// - /// No arguments. - UpdateDisplay, - /// Tell the host that if needed, it should record automation data for a control. - /// - /// Typically called when the plugin editor begins changing a control. - /// - /// [index]: index of the control. - /// [return]: true on success. - BeginEdit, - /// A control is no longer being changed. - /// - /// Typically called after the plugin editor is done. - /// - /// [index]: index of the control. - /// [return]: true on success. - EndEdit, - /// Open the host file selector. - /// [ptr]: `VstFileSelect*` - /// [return]: true on success. - OpenFileSelector, - /// Close the host file selector. - /// [ptr]: `VstFileSelect*` - /// [return]: true on success. - CloseFileSelector, - - /// Deprecated. - _EditFile, - /// Deprecated. - /// [ptr]: char[2048] or sizeof (FSSpec). - /// [return]: 1 if supported. - _GetChunkFile, - /// Deprecated. - _GetInputSpeakerArrangement, -} - -/// Implemented by all VST hosts. -#[allow(unused_variables)] -pub trait Host { - /// Automate a parameter; the value has been changed. - fn automate(&self, index: i32, value: f32) {} - - /// Signal that automation of a parameter started (the knob has been touched / mouse button down). - fn begin_edit(&self, index: i32) {} - - /// Signal that automation of a parameter ended (the knob is no longer been touched / mouse button up). - fn end_edit(&self, index: i32) {} - - /// Get the plugin ID of the currently loading plugin. - /// - /// This is only useful for shell plugins where this value will change the plugin returned. - /// `TODO: implement shell plugins` - fn get_plugin_id(&self) -> i32 { - // TODO: Handle this properly - 0 - } - - /// An idle call. - /// - /// This is useful when the plugin is doing something such as mouse tracking in the UI. - fn idle(&self) {} - - /// Get vendor and product information. - /// - /// Returns a tuple in the form of `(version, vendor_name, product_name)`. - fn get_info(&self) -> (isize, String, String) { - (1, "vendor string".to_owned(), "product string".to_owned()) - } - - /// Handle incoming events from the plugin. - fn process_events(&self, events: &api::Events) {} - - /// Get time information. - fn get_time_info(&self, mask: i32) -> Option { - None - } - - /// Get block size. - fn get_block_size(&self) -> isize { - 0 - } - - /// Refresh UI after the plugin's parameters changed. - /// - /// Note: some hosts will call some `PluginParameters` methods from within the `update_display` - /// call, including `get_parameter`, `get_parameter_label`, `get_parameter_name` - /// and `get_parameter_text`. - fn update_display(&self) {} -} - -/// All possible errors that can occur when loading a VST plugin. -#[derive(Debug)] -pub enum PluginLoadError { - /// Could not load given path. - InvalidPath, - - /// Given path is not a VST plugin. - NotAPlugin, - - /// Failed to create an instance of this plugin. - /// - /// This can happen for many reasons, such as if the plugin requires a different version of - /// the VST API to be used, or due to improper licensing. - InstanceFailed, - - /// The API version which the plugin used is not supported by this library. - InvalidApiVersion, -} - -impl fmt::Display for PluginLoadError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use self::PluginLoadError::*; - let description = match self { - InvalidPath => "Could not open the requested path", - NotAPlugin => "The given path does not contain a VST2.4 compatible library", - InstanceFailed => "Failed to create a plugin instance", - InvalidApiVersion => "The plugin API version is not compatible with this library", - }; - write!(f, "{}", description) - } -} - -impl Error for PluginLoadError {} - -/// Wrapper for an externally loaded VST plugin. -/// -/// The only functionality this struct provides is loading plugins, which can be done via the -/// [`load`](#method.load) method. -pub struct PluginLoader { - main: PluginMain, - lib: Arc, - host: Arc>, -} - -/// An instance of an externally loaded VST plugin. -#[allow(dead_code)] // To keep `lib` around. -pub struct PluginInstance { - params: Arc, - lib: Arc, - info: Info, - is_editor_active: bool, -} - -struct PluginParametersInstance { - effect: UnsafeCell<*mut AEffect>, -} - -unsafe impl Send for PluginParametersInstance {} -unsafe impl Sync for PluginParametersInstance {} - -impl Drop for PluginInstance { - fn drop(&mut self) { - self.dispatch(plugin::OpCode::Shutdown, 0, 0, ptr::null_mut(), 0.0); - } -} - -/// The editor of an externally loaded VST plugin. -struct EditorInstance { - params: Arc, - is_open: bool, -} - -impl EditorInstance { - fn get_rect(&self) -> Option { - let mut rect: *mut Rect = std::ptr::null_mut(); - let rect_ptr: *mut *mut Rect = &mut rect; - - let result = self - .params - .dispatch(plugin::OpCode::EditorGetRect, 0, 0, rect_ptr as *mut c_void, 0.0); - - if result == 0 || rect.is_null() { - return None; - } - Some(unsafe { *rect }) // TODO: Who owns rect? Who should free the memory? - } -} - -impl Editor for EditorInstance { - fn size(&self) -> (i32, i32) { - // Assuming coordinate origins from top-left - match self.get_rect() { - None => (0, 0), - Some(rect) => ((rect.right - rect.left) as i32, (rect.bottom - rect.top) as i32), - } - } - - fn position(&self) -> (i32, i32) { - // Assuming coordinate origins from top-left - match self.get_rect() { - None => (0, 0), - Some(rect) => (rect.left as i32, rect.top as i32), - } - } - - fn close(&mut self) { - self.params - .dispatch(plugin::OpCode::EditorClose, 0, 0, ptr::null_mut(), 0.0); - self.is_open = false; - } - - fn open(&mut self, parent: *mut c_void) -> bool { - let result = self.params.dispatch(plugin::OpCode::EditorOpen, 0, 0, parent, 0.0); - - let opened = result == 1; - if opened { - self.is_open = true; - } - - opened - } - - fn is_open(&mut self) -> bool { - self.is_open - } -} - -impl PluginLoader { - /// Load a plugin at the given path with the given host. - /// - /// Because of the possibility of multi-threading problems that can occur when using plugins, - /// the host must be passed in via an `Arc>` object. This makes sure that even if the - /// plugins are multi-threaded no data race issues can occur. - /// - /// Upon success, this method returns a [`PluginLoader`](.) object which you can use to call - /// [`instance`](#method.instance) to create a new instance of the plugin. - /// - /// # Example - /// - /// ```no_run - /// # use std::path::Path; - /// # use std::sync::{Arc, Mutex}; - /// # use vst::host::{Host, PluginLoader}; - /// # let path = Path::new("."); - /// # struct MyHost; - /// # impl MyHost { fn new() -> MyHost { MyHost } } - /// # impl Host for MyHost { - /// # fn automate(&self, _: i32, _: f32) {} - /// # fn get_plugin_id(&self) -> i32 { 0 } - /// # } - /// // ... - /// let host = Arc::new(Mutex::new(MyHost::new())); - /// - /// let mut plugin = PluginLoader::load(path, host.clone()).unwrap(); - /// - /// let instance = plugin.instance().unwrap(); - /// // ... - /// ``` - /// - /// # Linux/Windows - /// * This should be a path to the library, typically ending in `.so`/`.dll`. - /// * Possible full path: `/home/overdrivenpotato/.vst/u-he/Zebra2.64.so` - /// * Possible full path: `C:\Program Files (x86)\VSTPlugins\iZotope Ozone 5.dll` - /// - /// # OS X - /// * This should point to the mach-o file within the `.vst` bundle. - /// * Plugin: `/Library/Audio/Plug-Ins/VST/iZotope Ozone 5.vst` - /// * Possible full path: - /// `/Library/Audio/Plug-Ins/VST/iZotope Ozone 5.vst/Contents/MacOS/PluginHooksVST` - pub fn load(path: &Path, host: Arc>) -> Result, PluginLoadError> { - // Try loading the library at the given path - unsafe { - let lib = match Library::new(path) { - Ok(l) => l, - Err(_) => return Err(PluginLoadError::InvalidPath), - }; - - Ok(PluginLoader { - main: - // Search the library for the VSTAPI entry point - match lib.get(b"VSTPluginMain") { - Ok(s) => *s, - _ => return Err(PluginLoadError::NotAPlugin), - } - , - lib: Arc::new(lib), - host, - }) - } - } - - /// Call the VST entry point and retrieve a (possibly null) pointer. - unsafe fn call_main(&mut self) -> *mut AEffect { - LOAD_POINTER = Box::into_raw(Box::new(Arc::clone(&self.host))) as *mut c_void; - (self.main)(callback_wrapper::) - } - - /// Try to create an instance of this VST plugin. - /// - /// If the instance is successfully created, a [`PluginInstance`](struct.PluginInstance.html) - /// is returned. This struct implements the [`Plugin` trait](../plugin/trait.Plugin.html). - pub fn instance(&mut self) -> Result { - // Call the plugin main function. This also passes the plugin main function as the closure - // could not return an error if the symbol wasn't found - let effect = unsafe { self.call_main() }; - - if effect.is_null() { - return Err(PluginLoadError::InstanceFailed); - } - - unsafe { - // Move the host to the heap and add it to the `AEffect` struct for future reference - (*effect).reserved1 = Box::into_raw(Box::new(Arc::clone(&self.host))) as isize; - } - - let instance = PluginInstance::new(effect, Arc::clone(&self.lib)); - - let api_ver = instance.dispatch(plugin::OpCode::GetApiVersion, 0, 0, ptr::null_mut(), 0.0); - if api_ver >= 2400 { - Ok(instance) - } else { - trace!("Could not load plugin with api version {}", api_ver); - Err(PluginLoadError::InvalidApiVersion) - } - } -} - -impl PluginInstance { - fn new(effect: *mut AEffect, lib: Arc) -> PluginInstance { - use plugin::OpCode as op; - - let params = Arc::new(PluginParametersInstance { - effect: UnsafeCell::new(effect), - }); - let mut plug = PluginInstance { - params, - lib, - info: Default::default(), - is_editor_active: false, - }; - - unsafe { - let effect: &AEffect = &*effect; - let flags = PluginFlags::from_bits_truncate(effect.flags); - - plug.info = Info { - name: plug.read_string(op::GetProductName, MAX_PRODUCT_STR_LEN), - vendor: plug.read_string(op::GetVendorName, MAX_VENDOR_STR_LEN), - - presets: effect.numPrograms, - parameters: effect.numParams, - inputs: effect.numInputs, - outputs: effect.numOutputs, - - midi_inputs: 0, - midi_outputs: 0, - - unique_id: effect.uniqueId, - version: effect.version, - - category: Category::try_from(plug.opcode(op::GetCategory)).unwrap_or(Category::Unknown), - - initial_delay: effect.initialDelay, - - preset_chunks: flags.intersects(PluginFlags::PROGRAM_CHUNKS), - f64_precision: flags.intersects(PluginFlags::CAN_DOUBLE_REPLACING), - silent_when_stopped: flags.intersects(PluginFlags::NO_SOUND_IN_STOP), - }; - } - - plug - } -} - -trait Dispatch { - fn get_effect(&self) -> *mut AEffect; - - /// Send a dispatch message to the plugin. - fn dispatch(&self, opcode: plugin::OpCode, index: i32, value: isize, ptr: *mut c_void, opt: f32) -> isize { - let dispatcher = unsafe { (*self.get_effect()).dispatcher }; - if (dispatcher as *mut u8).is_null() { - panic!("Plugin was not loaded correctly."); - } - dispatcher(self.get_effect(), opcode.into(), index, value, ptr, opt) - } - - /// Send a lone opcode with no parameters. - fn opcode(&self, opcode: plugin::OpCode) -> isize { - self.dispatch(opcode, 0, 0, ptr::null_mut(), 0.0) - } - - /// Like `dispatch`, except takes a `&str` to send via `ptr`. - fn write_string(&self, opcode: plugin::OpCode, index: i32, value: isize, string: &str, opt: f32) -> isize { - let string = CString::new(string).expect("Invalid string data"); - self.dispatch(opcode, index, value, string.as_bytes().as_ptr() as *mut c_void, opt) - } - - fn read_string(&self, opcode: plugin::OpCode, max: usize) -> String { - self.read_string_param(opcode, 0, 0, 0.0, max) - } - - fn read_string_param(&self, opcode: plugin::OpCode, index: i32, value: isize, opt: f32, max: usize) -> String { - let mut buf = vec![0; max]; - self.dispatch(opcode, index, value, buf.as_mut_ptr() as *mut c_void, opt); - String::from_utf8_lossy(&buf) - .chars() - .take_while(|c| *c != '\0') - .collect() - } -} - -impl Dispatch for PluginInstance { - fn get_effect(&self) -> *mut AEffect { - self.params.get_effect() - } -} - -impl Dispatch for PluginParametersInstance { - fn get_effect(&self) -> *mut AEffect { - unsafe { *self.effect.get() } - } -} - -impl Plugin for PluginInstance { - fn get_info(&self) -> plugin::Info { - self.info.clone() - } - - fn new(_host: HostCallback) -> Self { - // Plugin::new is only called on client side and PluginInstance is only used on host side - unreachable!() - } - - fn init(&mut self) { - self.opcode(plugin::OpCode::Initialize); - } - - fn set_sample_rate(&mut self, rate: f32) { - self.dispatch(plugin::OpCode::SetSampleRate, 0, 0, ptr::null_mut(), rate); - } - - fn set_block_size(&mut self, size: i64) { - self.dispatch(plugin::OpCode::SetBlockSize, 0, size as isize, ptr::null_mut(), 0.0); - } - - fn resume(&mut self) { - self.dispatch(plugin::OpCode::StateChanged, 0, 1, ptr::null_mut(), 0.0); - } - - fn suspend(&mut self) { - self.dispatch(plugin::OpCode::StateChanged, 0, 0, ptr::null_mut(), 0.0); - } - - fn vendor_specific(&mut self, index: i32, value: isize, ptr: *mut c_void, opt: f32) -> isize { - self.dispatch(plugin::OpCode::VendorSpecific, index, value, ptr, opt) - } - - fn can_do(&self, can_do: plugin::CanDo) -> Supported { - let s: String = can_do.into(); - Supported::from(self.write_string(plugin::OpCode::CanDo, 0, 0, &s, 0.0)) - .expect("Invalid response received when querying plugin CanDo") - } - - fn get_tail_size(&self) -> isize { - self.opcode(plugin::OpCode::GetTailSize) - } - - fn process(&mut self, buffer: &mut AudioBuffer) { - if buffer.input_count() < self.info.inputs as usize { - panic!("Too few inputs in AudioBuffer"); - } - if buffer.output_count() < self.info.outputs as usize { - panic!("Too few outputs in AudioBuffer"); - } - unsafe { - ((*self.get_effect()).processReplacing)( - self.get_effect(), - buffer.raw_inputs().as_ptr() as *const *const _, - buffer.raw_outputs().as_mut_ptr() as *mut *mut _, - buffer.samples() as i32, - ) - } - } - - fn process_f64(&mut self, buffer: &mut AudioBuffer) { - if buffer.input_count() < self.info.inputs as usize { - panic!("Too few inputs in AudioBuffer"); - } - if buffer.output_count() < self.info.outputs as usize { - panic!("Too few outputs in AudioBuffer"); - } - unsafe { - ((*self.get_effect()).processReplacingF64)( - self.get_effect(), - buffer.raw_inputs().as_ptr() as *const *const _, - buffer.raw_outputs().as_mut_ptr() as *mut *mut _, - buffer.samples() as i32, - ) - } - } - - fn process_events(&mut self, events: &api::Events) { - self.dispatch(plugin::OpCode::ProcessEvents, 0, 0, events as *const _ as *mut _, 0.0); - } - - fn get_input_info(&self, input: i32) -> ChannelInfo { - let mut props: MaybeUninit = MaybeUninit::uninit(); - let ptr = props.as_mut_ptr() as *mut c_void; - - self.dispatch(plugin::OpCode::GetInputInfo, input, 0, ptr, 0.0); - - ChannelInfo::from(unsafe { props.assume_init() }) - } - - fn get_output_info(&self, output: i32) -> ChannelInfo { - let mut props: MaybeUninit = MaybeUninit::uninit(); - let ptr = props.as_mut_ptr() as *mut c_void; - - self.dispatch(plugin::OpCode::GetOutputInfo, output, 0, ptr, 0.0); - - ChannelInfo::from(unsafe { props.assume_init() }) - } - - fn get_parameter_object(&mut self) -> Arc { - Arc::clone(&self.params) as Arc - } - - fn get_editor(&mut self) -> Option> { - if self.is_editor_active { - // An editor is already active, the caller should be using the active editor instead of - // requesting for a new one. - return None; - } - - self.is_editor_active = true; - Some(Box::new(EditorInstance { - params: self.params.clone(), - is_open: false, - })) - } -} - -impl PluginParameters for PluginParametersInstance { - fn change_preset(&self, preset: i32) { - self.dispatch(plugin::OpCode::ChangePreset, 0, preset as isize, ptr::null_mut(), 0.0); - } - - fn get_preset_num(&self) -> i32 { - self.opcode(plugin::OpCode::GetCurrentPresetNum) as i32 - } - - fn set_preset_name(&self, name: String) { - self.write_string(plugin::OpCode::SetCurrentPresetName, 0, 0, &name, 0.0); - } - - fn get_preset_name(&self, preset: i32) -> String { - self.read_string_param(plugin::OpCode::GetPresetName, preset, 0, 0.0, MAX_PRESET_NAME_LEN) - } - - fn get_parameter_label(&self, index: i32) -> String { - self.read_string_param(plugin::OpCode::GetParameterLabel, index, 0, 0.0, MAX_PARAM_STR_LEN) - } - - fn get_parameter_text(&self, index: i32) -> String { - self.read_string_param(plugin::OpCode::GetParameterDisplay, index, 0, 0.0, MAX_PARAM_STR_LEN) - } - - fn get_parameter_name(&self, index: i32) -> String { - self.read_string_param(plugin::OpCode::GetParameterName, index, 0, 0.0, MAX_PARAM_STR_LEN) - } - - fn get_parameter(&self, index: i32) -> f32 { - unsafe { ((*self.get_effect()).getParameter)(self.get_effect(), index) } - } - - fn set_parameter(&self, index: i32, value: f32) { - unsafe { ((*self.get_effect()).setParameter)(self.get_effect(), index, value) } - } - - fn can_be_automated(&self, index: i32) -> bool { - self.dispatch(plugin::OpCode::CanBeAutomated, index, 0, ptr::null_mut(), 0.0) > 0 - } - - fn string_to_parameter(&self, index: i32, text: String) -> bool { - self.write_string(plugin::OpCode::StringToParameter, index, 0, &text, 0.0) > 0 - } - - // TODO: Editor - - fn get_preset_data(&self) -> Vec { - // Create a pointer that can be updated from the plugin. - let mut ptr: *mut u8 = ptr::null_mut(); - let len = self.dispatch( - plugin::OpCode::GetData, - 1, /*preset*/ - 0, - &mut ptr as *mut *mut u8 as *mut c_void, - 0.0, - ); - let slice = unsafe { slice::from_raw_parts(ptr, len as usize) }; - slice.to_vec() - } - - fn get_bank_data(&self) -> Vec { - // Create a pointer that can be updated from the plugin. - let mut ptr: *mut u8 = ptr::null_mut(); - let len = self.dispatch( - plugin::OpCode::GetData, - 0, /*bank*/ - 0, - &mut ptr as *mut *mut u8 as *mut c_void, - 0.0, - ); - let slice = unsafe { slice::from_raw_parts(ptr, len as usize) }; - slice.to_vec() - } - - fn load_preset_data(&self, data: &[u8]) { - self.dispatch( - plugin::OpCode::SetData, - 1, - data.len() as isize, - data.as_ptr() as *mut c_void, - 0.0, - ); - } - - fn load_bank_data(&self, data: &[u8]) { - self.dispatch( - plugin::OpCode::SetData, - 0, - data.len() as isize, - data.as_ptr() as *mut c_void, - 0.0, - ); - } -} - -/// Used for constructing `AudioBuffer` instances on the host. -/// -/// This struct contains all necessary allocations for an `AudioBuffer` apart -/// from the actual sample arrays. This way, the inner processing loop can -/// be allocation free even if `AudioBuffer` instances are repeatedly created. -/// -/// ```rust -/// # use vst::host::HostBuffer; -/// # use vst::plugin::Plugin; -/// # fn test(plugin: &mut P) { -/// let mut host_buffer: HostBuffer = HostBuffer::new(2, 2); -/// let inputs = vec![vec![0.0; 1000]; 2]; -/// let mut outputs = vec![vec![0.0; 1000]; 2]; -/// let mut audio_buffer = host_buffer.bind(&inputs, &mut outputs); -/// plugin.process(&mut audio_buffer); -/// # } -/// ``` -pub struct HostBuffer { - inputs: Vec<*const T>, - outputs: Vec<*mut T>, -} - -impl HostBuffer { - /// Create a `HostBuffer` for a given number of input and output channels. - pub fn new(input_count: usize, output_count: usize) -> HostBuffer { - HostBuffer { - inputs: vec![ptr::null(); input_count], - outputs: vec![ptr::null_mut(); output_count], - } - } - - /// Create a `HostBuffer` for the number of input and output channels - /// specified in an `Info` struct. - pub fn from_info(info: &Info) -> HostBuffer { - HostBuffer::new(info.inputs as usize, info.outputs as usize) - } - - /// Bind sample arrays to the `HostBuffer` to create an `AudioBuffer` to pass to a plugin. - /// - /// # Panics - /// This function will panic if more inputs or outputs are supplied than the `HostBuffer` - /// was created for, or if the sample arrays do not all have the same length. - pub fn bind<'a, I, O>(&'a mut self, input_arrays: &[I], output_arrays: &mut [O]) -> AudioBuffer<'a, T> - where - I: AsRef<[T]> + 'a, - O: AsMut<[T]> + 'a, - { - // Check that number of desired inputs and outputs fit in allocation - if input_arrays.len() > self.inputs.len() { - panic!("Too many inputs for HostBuffer"); - } - if output_arrays.len() > self.outputs.len() { - panic!("Too many outputs for HostBuffer"); - } - - // Initialize raw pointers and find common length - let mut length = None; - for (i, input) in input_arrays.iter().map(|r| r.as_ref()).enumerate() { - self.inputs[i] = input.as_ptr(); - match length { - None => length = Some(input.len()), - Some(old_length) => { - if input.len() != old_length { - panic!("Mismatching lengths of input arrays"); - } - } - } - } - for (i, output) in output_arrays.iter_mut().map(|r| r.as_mut()).enumerate() { - self.outputs[i] = output.as_mut_ptr(); - match length { - None => length = Some(output.len()), - Some(old_length) => { - if output.len() != old_length { - panic!("Mismatching lengths of output arrays"); - } - } - } - } - let length = length.unwrap_or(0); - - // Construct AudioBuffer - unsafe { - AudioBuffer::from_raw( - input_arrays.len(), - output_arrays.len(), - self.inputs.as_ptr(), - self.outputs.as_mut_ptr(), - length, - ) - } - } - - /// Number of input channels supported by this `HostBuffer`. - pub fn input_count(&self) -> usize { - self.inputs.len() - } - - /// Number of output channels supported by this `HostBuffer`. - pub fn output_count(&self) -> usize { - self.outputs.len() - } -} - -/// HACK: a pointer to store the host so that it can be accessed from the `callback_wrapper` -/// function passed to the plugin. -/// -/// When the plugin is being loaded, a `Box>>` is transmuted to a `*mut c_void` pointer -/// and placed here. When the plugin calls the callback during initialization, the host refers to -/// this pointer to get a handle to the Host. After initialization, this pointer is invalidated and -/// the host pointer is placed into a [reserved field] in the instance `AEffect` struct. -/// -/// The issue with this approach is that if 2 plugins are simultaneously loaded with 2 different -/// host instances, this might fail as one host may receive a pointer to the other one. In practice -/// this is a rare situation as you normally won't have 2 separate host instances loading at once. -/// -/// [reserved field]: ../api/struct.AEffect.html#structfield.reserved1 -static mut LOAD_POINTER: *mut c_void = 0 as *mut c_void; - -/// Function passed to plugin to handle dispatching host opcodes. -extern "C" fn callback_wrapper( - effect: *mut AEffect, - opcode: i32, - index: i32, - value: isize, - ptr: *mut c_void, - opt: f32, -) -> isize { - unsafe { - // If the effect pointer is not null and the host pointer is not null, the plugin has - // already been initialized - if !effect.is_null() && (*effect).reserved1 != 0 { - let reserved = (*effect).reserved1 as *const Arc>; - let host = &*reserved; - - let host = &mut *host.lock().unwrap(); - - interfaces::host_dispatch(host, effect, opcode, index, value, ptr, opt) - // In this case, the plugin is still undergoing initialization and so `LOAD_POINTER` is - // dereferenced - } else { - // Used only during the plugin initialization - let host = LOAD_POINTER as *const Arc>; - let host = &*host; - let host = &mut *host.lock().unwrap(); - - interfaces::host_dispatch(host, effect, opcode, index, value, ptr, opt) - } - } -} - -#[cfg(test)] -mod tests { - use crate::host::HostBuffer; - - #[test] - fn host_buffer() { - const LENGTH: usize = 1_000_000; - let mut host_buffer: HostBuffer = HostBuffer::new(2, 2); - let input_left = vec![1.0; LENGTH]; - let input_right = vec![1.0; LENGTH]; - let mut output_left = vec![0.0; LENGTH]; - let mut output_right = vec![0.0; LENGTH]; - { - let mut audio_buffer = { - // Slices given to `bind` need not persist, but the sample arrays do. - let inputs = [&input_left, &input_right]; - let mut outputs = [&mut output_left, &mut output_right]; - host_buffer.bind(&inputs, &mut outputs) - }; - for (input, output) in audio_buffer.zip() { - for (i, o) in input.iter().zip(output) { - *o = *i * 2.0; - } - } - } - assert_eq!(output_left, vec![2.0; LENGTH]); - assert_eq!(output_right, vec![2.0; LENGTH]); - } -} diff --git a/deps/vst/src/interfaces.rs b/deps/vst/src/interfaces.rs deleted file mode 100644 index 6b5261e7..00000000 --- a/deps/vst/src/interfaces.rs +++ /dev/null @@ -1,370 +0,0 @@ -//! Function interfaces for VST 2.4 API. - -#![doc(hidden)] - -use std::cell::Cell; -use std::os::raw::{c_char, c_void}; -use std::{mem, slice}; - -use crate::{ - api::{self, consts::*, AEffect, TimeInfo}, - buffer::AudioBuffer, - editor::{Key, KeyCode, KnobMode, Rect}, - host::Host, -}; - -/// Deprecated process function. -pub extern "C" fn process_deprecated( - _effect: *mut AEffect, - _raw_inputs: *const *const f32, - _raw_outputs: *mut *mut f32, - _samples: i32, -) { -} - -/// VST2.4 replacing function. -pub extern "C" fn process_replacing( - effect: *mut AEffect, - raw_inputs: *const *const f32, - raw_outputs: *mut *mut f32, - samples: i32, -) { - // Handle to the VST - let plugin = unsafe { (*effect).get_plugin() }; - let info = unsafe { (*effect).get_info() }; - let (input_count, output_count) = (info.inputs as usize, info.outputs as usize); - let mut buffer = - unsafe { AudioBuffer::from_raw(input_count, output_count, raw_inputs, raw_outputs, samples as usize) }; - plugin.process(&mut buffer); -} - -/// VST2.4 replacing function with `f64` values. -pub extern "C" fn process_replacing_f64( - effect: *mut AEffect, - raw_inputs: *const *const f64, - raw_outputs: *mut *mut f64, - samples: i32, -) { - let plugin = unsafe { (*effect).get_plugin() }; - let info = unsafe { (*effect).get_info() }; - let (input_count, output_count) = (info.inputs as usize, info.outputs as usize); - let mut buffer = - unsafe { AudioBuffer::from_raw(input_count, output_count, raw_inputs, raw_outputs, samples as usize) }; - plugin.process_f64(&mut buffer); -} - -/// VST2.4 set parameter function. -pub extern "C" fn set_parameter(effect: *mut AEffect, index: i32, value: f32) { - unsafe { (*effect).get_params() }.set_parameter(index, value); -} - -/// VST2.4 get parameter function. -pub extern "C" fn get_parameter(effect: *mut AEffect, index: i32) -> f32 { - unsafe { (*effect).get_params() }.get_parameter(index) -} - -/// Copy a string into a destination buffer. -/// -/// String will be cut at `max` characters. -fn copy_string(dst: *mut c_void, src: &str, max: usize) -> isize { - unsafe { - use libc::{memcpy, memset}; - use std::cmp::min; - - let dst = dst as *mut c_void; - memset(dst, 0, max); - memcpy(dst, src.as_ptr() as *const c_void, min(max, src.as_bytes().len())); - } - - 1 // Success -} - -/// VST2.4 dispatch function. This function handles dispatching all opcodes to the VST plugin. -pub extern "C" fn dispatch( - effect: *mut AEffect, - opcode: i32, - index: i32, - value: isize, - ptr: *mut c_void, - opt: f32, -) -> isize { - use crate::plugin::{CanDo, OpCode}; - - // Convert passed in opcode to enum - let opcode = OpCode::try_from(opcode); - // Only query plugin or editor when needed to avoid creating multiple - // concurrent mutable references to the same object. - let get_plugin = || unsafe { (*effect).get_plugin() }; - let get_editor = || unsafe { (*effect).get_editor() }; - let params = unsafe { (*effect).get_params() }; - - match opcode { - Ok(OpCode::Initialize) => get_plugin().init(), - Ok(OpCode::Shutdown) => unsafe { - (*effect).drop_plugin(); - drop(Box::from_raw(effect)) - }, - - Ok(OpCode::ChangePreset) => params.change_preset(value as i32), - Ok(OpCode::GetCurrentPresetNum) => return params.get_preset_num() as isize, - Ok(OpCode::SetCurrentPresetName) => params.set_preset_name(read_string(ptr)), - Ok(OpCode::GetCurrentPresetName) => { - let num = params.get_preset_num(); - return copy_string(ptr, ¶ms.get_preset_name(num), MAX_PRESET_NAME_LEN); - } - - Ok(OpCode::GetParameterLabel) => { - return copy_string(ptr, ¶ms.get_parameter_label(index), MAX_PARAM_STR_LEN) - } - Ok(OpCode::GetParameterDisplay) => { - return copy_string(ptr, ¶ms.get_parameter_text(index), MAX_PARAM_STR_LEN) - } - Ok(OpCode::GetParameterName) => return copy_string(ptr, ¶ms.get_parameter_name(index), MAX_PARAM_STR_LEN), - - Ok(OpCode::SetSampleRate) => get_plugin().set_sample_rate(opt), - Ok(OpCode::SetBlockSize) => get_plugin().set_block_size(value as i64), - Ok(OpCode::StateChanged) => { - if value == 1 { - get_plugin().resume(); - } else { - get_plugin().suspend(); - } - } - - Ok(OpCode::EditorGetRect) => { - if let Some(ref mut editor) = get_editor() { - let size = editor.size(); - let pos = editor.position(); - - unsafe { - // Given a Rect** structure - // TODO: Investigate whether we are given a valid Rect** pointer already - *(ptr as *mut *mut c_void) = Box::into_raw(Box::new(Rect { - left: pos.0 as i16, // x coord of position - top: pos.1 as i16, // y coord of position - right: (pos.0 + size.0) as i16, // x coord of pos + x coord of size - bottom: (pos.1 + size.1) as i16, // y coord of pos + y coord of size - })) as *mut _; // TODO: free memory - } - - return 1; - } - } - Ok(OpCode::EditorOpen) => { - if let Some(ref mut editor) = get_editor() { - // `ptr` is a window handle to the parent window. - // See the documentation for `Editor::open` for details. - if editor.open(ptr) { - return 1; - } - } - } - Ok(OpCode::EditorClose) => { - if let Some(ref mut editor) = get_editor() { - editor.close(); - } - } - - Ok(OpCode::EditorIdle) => { - if let Some(ref mut editor) = get_editor() { - editor.idle(); - } - } - - Ok(OpCode::GetData) => { - let mut chunks = if index == 0 { - params.get_bank_data() - } else { - params.get_preset_data() - }; - - chunks.shrink_to_fit(); - let len = chunks.len() as isize; // eventually we should be using ffi::size_t - - unsafe { - *(ptr as *mut *mut c_void) = chunks.as_ptr() as *mut c_void; - } - - mem::forget(chunks); - return len; - } - Ok(OpCode::SetData) => { - let chunks = unsafe { slice::from_raw_parts(ptr as *mut u8, value as usize) }; - - if index == 0 { - params.load_bank_data(chunks); - } else { - params.load_preset_data(chunks); - } - } - - Ok(OpCode::ProcessEvents) => { - get_plugin().process_events(unsafe { &*(ptr as *const api::Events) }); - } - Ok(OpCode::CanBeAutomated) => return params.can_be_automated(index) as isize, - Ok(OpCode::StringToParameter) => return params.string_to_parameter(index, read_string(ptr)) as isize, - - Ok(OpCode::GetPresetName) => return copy_string(ptr, ¶ms.get_preset_name(index), MAX_PRESET_NAME_LEN), - - Ok(OpCode::GetInputInfo) => { - if index >= 0 && index < get_plugin().get_info().inputs { - unsafe { - let ptr = ptr as *mut api::ChannelProperties; - *ptr = get_plugin().get_input_info(index).into(); - } - } - } - Ok(OpCode::GetOutputInfo) => { - if index >= 0 && index < get_plugin().get_info().outputs { - unsafe { - let ptr = ptr as *mut api::ChannelProperties; - *ptr = get_plugin().get_output_info(index).into(); - } - } - } - Ok(OpCode::GetCategory) => { - return get_plugin().get_info().category.into(); - } - - Ok(OpCode::GetEffectName) => return copy_string(ptr, &get_plugin().get_info().name, MAX_VENDOR_STR_LEN), - - Ok(OpCode::GetVendorName) => return copy_string(ptr, &get_plugin().get_info().vendor, MAX_VENDOR_STR_LEN), - Ok(OpCode::GetProductName) => return copy_string(ptr, &get_plugin().get_info().name, MAX_PRODUCT_STR_LEN), - Ok(OpCode::GetVendorVersion) => return get_plugin().get_info().version as isize, - Ok(OpCode::VendorSpecific) => return get_plugin().vendor_specific(index, value, ptr, opt), - Ok(OpCode::CanDo) => { - let can_do = CanDo::from_str(&read_string(ptr)); - return get_plugin().can_do(can_do).into(); - } - Ok(OpCode::GetTailSize) => { - if get_plugin().get_tail_size() == 0 { - return 1; - } else { - return get_plugin().get_tail_size(); - } - } - - //OpCode::GetParamInfo => { /*TODO*/ } - Ok(OpCode::GetApiVersion) => return 2400, - - Ok(OpCode::EditorKeyDown) => { - if let Some(ref mut editor) = get_editor() { - if let Ok(key) = Key::try_from(value) { - editor.key_down(KeyCode { - character: index as u8 as char, - key, - modifier: opt.to_bits() as u8, - }); - } - } - } - Ok(OpCode::EditorKeyUp) => { - if let Some(ref mut editor) = get_editor() { - if let Ok(key) = Key::try_from(value) { - editor.key_up(KeyCode { - character: index as u8 as char, - key, - modifier: opt.to_bits() as u8, - }); - } - } - } - Ok(OpCode::EditorSetKnobMode) => { - if let Some(ref mut editor) = get_editor() { - if let Ok(knob_mode) = KnobMode::try_from(value) { - editor.set_knob_mode(knob_mode); - } - } - } - - Ok(OpCode::StartProcess) => get_plugin().start_process(), - Ok(OpCode::StopProcess) => get_plugin().stop_process(), - - Ok(OpCode::GetNumMidiInputs) => return unsafe { (*effect).get_info() }.midi_inputs as isize, - Ok(OpCode::GetNumMidiOutputs) => return unsafe { (*effect).get_info() }.midi_outputs as isize, - - _ => { - debug!("Unimplemented opcode ({:?})", opcode); - trace!( - "Arguments; index: {}, value: {}, ptr: {:?}, opt: {}", - index, - value, - ptr, - opt - ); - } - } - - 0 -} - -pub fn host_dispatch( - host: &mut dyn Host, - effect: *mut AEffect, - opcode: i32, - index: i32, - value: isize, - ptr: *mut c_void, - opt: f32, -) -> isize { - use crate::host::OpCode; - - let opcode = OpCode::try_from(opcode); - match opcode { - Ok(OpCode::Version) => return 2400, - Ok(OpCode::Automate) => host.automate(index, opt), - Ok(OpCode::BeginEdit) => host.begin_edit(index), - Ok(OpCode::EndEdit) => host.end_edit(index), - - Ok(OpCode::Idle) => host.idle(), - - // ... - Ok(OpCode::CanDo) => { - info!("Plugin is asking if host can: {}.", read_string(ptr)); - } - - Ok(OpCode::GetVendorVersion) => return host.get_info().0, - Ok(OpCode::GetVendorString) => return copy_string(ptr, &host.get_info().1, MAX_VENDOR_STR_LEN), - Ok(OpCode::GetProductString) => return copy_string(ptr, &host.get_info().2, MAX_PRODUCT_STR_LEN), - Ok(OpCode::ProcessEvents) => { - host.process_events(unsafe { &*(ptr as *const api::Events) }); - } - - Ok(OpCode::GetTime) => { - return match host.get_time_info(value as i32) { - None => 0, - Some(result) => { - thread_local! { - static TIME_INFO: Cell = - Cell::new(TimeInfo::default()); - } - TIME_INFO.with(|time_info| { - (*time_info).set(result); - time_info.as_ptr() as isize - }) - } - }; - } - Ok(OpCode::GetBlockSize) => return host.get_block_size(), - - _ => { - trace!("VST: Got unimplemented host opcode ({:?})", opcode); - trace!( - "Arguments; effect: {:?}, index: {}, value: {}, ptr: {:?}, opt: {}", - effect, - index, - value, - ptr, - opt - ); - } - } - 0 -} - -// Read a string from the `ptr` buffer -fn read_string(ptr: *mut c_void) -> String { - use std::ffi::CStr; - - String::from_utf8_lossy(unsafe { CStr::from_ptr(ptr as *mut c_char).to_bytes() }).into_owned() -} diff --git a/deps/vst/src/lib.rs b/deps/vst/src/lib.rs deleted file mode 100755 index b0c26de3..00000000 --- a/deps/vst/src/lib.rs +++ /dev/null @@ -1,416 +0,0 @@ -#![warn(missing_docs)] - -//! A rust implementation of the VST2.4 API. -//! -//! The VST API is multi-threaded. A VST host calls into a plugin generally from two threads - -//! the *processing* thread and the *UI* thread. The organization of this crate reflects this -//! structure to ensure that the threading assumptions of Safe Rust are fulfilled and data -//! races are avoided. -//! -//! # Plugins -//! All Plugins must implement the `Plugin` trait and `std::default::Default`. -//! The `plugin_main!` macro must also be called in order to export the necessary functions -//! for the plugin to function. -//! -//! ## `Plugin` Trait -//! All methods in this trait have a default implementation except for the `get_info` method which -//! must be implemented by the plugin. Any of the default implementations may be overridden for -//! custom functionality; the defaults do nothing on their own. -//! -//! ## `PluginParameters` Trait -//! The methods in this trait handle access to plugin parameters. Since the host may call these -//! methods concurrently with audio processing, it needs to be separate from the main `Plugin` -//! trait. -//! -//! To support parameters, a plugin must provide an implementation of the `PluginParameters` -//! trait, wrap it in an `Arc` (so it can be accessed from both threads) and -//! return a reference to it from the `get_parameter_object` method in the `Plugin`. -//! -//! ## `plugin_main!` macro -//! `plugin_main!` will export the necessary functions to create a proper VST plugin. This must be -//! called with your VST plugin struct name in order for the vst to work. -//! -//! ## Example plugin -//! A barebones VST plugin: -//! -//! ```no_run -//! #[macro_use] -//! extern crate vst; -//! -//! use vst::plugin::{HostCallback, Info, Plugin}; -//! -//! struct BasicPlugin; -//! -//! impl Plugin for BasicPlugin { -//! fn new(_host: HostCallback) -> Self { -//! BasicPlugin -//! } -//! -//! fn get_info(&self) -> Info { -//! Info { -//! name: "Basic Plugin".to_string(), -//! unique_id: 1357, // Used by hosts to differentiate between plugins. -//! -//! ..Default::default() -//! } -//! } -//! } -//! -//! plugin_main!(BasicPlugin); // Important! -//! # fn main() {} // For `extern crate vst` -//! ``` -//! -//! # Hosts -//! -//! ## `Host` Trait -//! All hosts must implement the [`Host` trait](host/trait.Host.html). To load a VST plugin, you -//! need to wrap your host in an `Arc>` wrapper for thread safety reasons. Along with the -//! plugin path, this can be passed to the [`PluginLoader::load`] method to create a plugin loader -//! which can spawn plugin instances. -//! -//! ## Example Host -//! ```no_run -//! extern crate vst; -//! -//! use std::sync::{Arc, Mutex}; -//! use std::path::Path; -//! -//! use vst::host::{Host, PluginLoader}; -//! use vst::plugin::Plugin; -//! -//! struct SampleHost; -//! -//! impl Host for SampleHost { -//! fn automate(&self, index: i32, value: f32) { -//! println!("Parameter {} had its value changed to {}", index, value); -//! } -//! } -//! -//! fn main() { -//! let host = Arc::new(Mutex::new(SampleHost)); -//! let path = Path::new("/path/to/vst"); -//! -//! let mut loader = PluginLoader::load(path, host.clone()).unwrap(); -//! let mut instance = loader.instance().unwrap(); -//! -//! println!("Loaded {}", instance.get_info().name); -//! -//! instance.init(); -//! println!("Initialized instance!"); -//! -//! println!("Closing instance..."); -//! // Not necessary as the instance is shut down when it goes out of scope anyway. -//! // drop(instance); -//! } -//! -//! ``` -//! -//! [`PluginLoader::load`]: host/struct.PluginLoader.html#method.load -//! - -extern crate libc; -extern crate libloading; -extern crate num_enum; -extern crate num_traits; -#[macro_use] -extern crate log; -#[macro_use] -extern crate bitflags; - -use std::ptr; - -pub mod api; -pub mod buffer; -mod cache; -pub mod channels; -pub mod editor; -pub mod event; -pub mod host; -mod interfaces; -pub mod plugin; -pub mod prelude; -pub mod util; - -use api::consts::VST_MAGIC; -use api::{AEffect, HostCallbackProc}; -use cache::PluginCache; -use plugin::{HostCallback, Plugin}; - -/// Exports the necessary symbols for the plugin to be used by a VST host. -/// -/// This macro takes a type which must implement the `Plugin` trait. -#[macro_export] -macro_rules! plugin_main { - ($t:ty) => { - #[cfg(target_os = "macos")] - #[no_mangle] - pub extern "system" fn main_macho(callback: $crate::api::HostCallbackProc) -> *mut $crate::api::AEffect { - VSTPluginMain(callback) - } - - #[cfg(target_os = "windows")] - #[allow(non_snake_case)] - #[no_mangle] - pub extern "system" fn MAIN(callback: $crate::api::HostCallbackProc) -> *mut $crate::api::AEffect { - VSTPluginMain(callback) - } - - #[allow(non_snake_case)] - #[no_mangle] - pub extern "C" fn VSTPluginMain(callback: $crate::api::HostCallbackProc) -> *mut $crate::api::AEffect { - $crate::main::<$t>(callback) - } - }; -} - -/// Initializes a VST plugin and returns a raw pointer to an AEffect struct. -#[doc(hidden)] -pub fn main(callback: HostCallbackProc) -> *mut AEffect { - // Initialize as much of the AEffect as we can before creating the plugin. - // In particular, initialize all the function pointers, since initializing - // these to zero is undefined behavior. - let boxed_effect = Box::new(AEffect { - magic: VST_MAGIC, - dispatcher: interfaces::dispatch, // fn pointer - - _process: interfaces::process_deprecated, // fn pointer - - setParameter: interfaces::set_parameter, // fn pointer - getParameter: interfaces::get_parameter, // fn pointer - - numPrograms: 0, // To be updated with plugin specific value. - numParams: 0, // To be updated with plugin specific value. - numInputs: 0, // To be updated with plugin specific value. - numOutputs: 0, // To be updated with plugin specific value. - - flags: 0, // To be updated with plugin specific value. - - reserved1: 0, - reserved2: 0, - - initialDelay: 0, // To be updated with plugin specific value. - - _realQualities: 0, - _offQualities: 0, - _ioRatio: 0.0, - - object: ptr::null_mut(), - user: ptr::null_mut(), - - uniqueId: 0, // To be updated with plugin specific value. - version: 0, // To be updated with plugin specific value. - - processReplacing: interfaces::process_replacing, // fn pointer - processReplacingF64: interfaces::process_replacing_f64, //fn pointer - - future: [0u8; 56], - }); - let raw_effect = Box::into_raw(boxed_effect); - - let host = HostCallback::wrap(callback, raw_effect); - if host.vst_version() == 0 { - // TODO: Better criteria would probably be useful here... - return ptr::null_mut(); - } - - trace!("Creating VST plugin instance..."); - let mut plugin = T::new(host); - let info = plugin.get_info(); - let params = plugin.get_parameter_object(); - let editor = plugin.get_editor(); - - // Update AEffect in place - let effect = unsafe { &mut *raw_effect }; - effect.numPrograms = info.presets; - effect.numParams = info.parameters; - effect.numInputs = info.inputs; - effect.numOutputs = info.outputs; - effect.flags = { - use api::PluginFlags; - - let mut flag = PluginFlags::CAN_REPLACING; - - if info.f64_precision { - flag |= PluginFlags::CAN_DOUBLE_REPLACING; - } - - if editor.is_some() { - flag |= PluginFlags::HAS_EDITOR; - } - - if info.preset_chunks { - flag |= PluginFlags::PROGRAM_CHUNKS; - } - - if let plugin::Category::Synth = info.category { - flag |= PluginFlags::IS_SYNTH; - } - - if info.silent_when_stopped { - flag |= PluginFlags::NO_SOUND_IN_STOP; - } - - flag.bits() - }; - effect.initialDelay = info.initial_delay; - effect.object = Box::into_raw(Box::new(Box::new(plugin) as Box)) as *mut _; - effect.user = Box::into_raw(Box::new(PluginCache::new(&info, params, editor))) as *mut _; - effect.uniqueId = info.unique_id; - effect.version = info.version; - - effect -} - -#[cfg(test)] -mod tests { - use std::ptr; - - use std::os::raw::c_void; - - use crate::{ - api::{consts::VST_MAGIC, AEffect}, - interfaces, - plugin::{HostCallback, Info, Plugin}, - }; - - struct TestPlugin; - - impl Plugin for TestPlugin { - fn new(_host: HostCallback) -> Self { - TestPlugin - } - - fn get_info(&self) -> Info { - Info { - name: "Test Plugin".to_string(), - vendor: "overdrivenpotato".to_string(), - - presets: 1, - parameters: 1, - - unique_id: 5678, - version: 1234, - - initial_delay: 123, - - ..Default::default() - } - } - } - - plugin_main!(TestPlugin); - - extern "C" fn pass_callback( - _effect: *mut AEffect, - _opcode: i32, - _index: i32, - _value: isize, - _ptr: *mut c_void, - _opt: f32, - ) -> isize { - 1 - } - - extern "C" fn fail_callback( - _effect: *mut AEffect, - _opcode: i32, - _index: i32, - _value: isize, - _ptr: *mut c_void, - _opt: f32, - ) -> isize { - 0 - } - - #[cfg(target_os = "windows")] - #[test] - fn old_hosts() { - assert_eq!(MAIN(fail_callback), ptr::null_mut()); - } - - #[cfg(target_os = "macos")] - #[test] - fn old_hosts() { - assert_eq!(main_macho(fail_callback), ptr::null_mut()); - } - - #[test] - fn host_callback() { - assert_eq!(VSTPluginMain(fail_callback), ptr::null_mut()); - } - - #[test] - fn aeffect_created() { - let aeffect = VSTPluginMain(pass_callback); - assert!(!aeffect.is_null()); - } - - #[test] - fn plugin_drop() { - static mut DROP_TEST: bool = false; - - impl Drop for TestPlugin { - fn drop(&mut self) { - unsafe { - DROP_TEST = true; - } - } - } - - let aeffect = VSTPluginMain(pass_callback); - assert!(!aeffect.is_null()); - - unsafe { (*aeffect).drop_plugin() }; - - // Assert that the VST is shut down and dropped. - assert!(unsafe { DROP_TEST }); - } - - #[test] - fn plugin_no_drop() { - let aeffect = VSTPluginMain(pass_callback); - assert!(!aeffect.is_null()); - - // Make sure this doesn't crash. - unsafe { (*aeffect).drop_plugin() }; - } - - #[test] - fn plugin_deref() { - let aeffect = VSTPluginMain(pass_callback); - assert!(!aeffect.is_null()); - - let plugin = unsafe { (*aeffect).get_plugin() }; - // Assert that deref works correctly. - assert!(plugin.get_info().name == "Test Plugin"); - } - - #[test] - fn aeffect_params() { - // Assert that 2 function pointers are equal. - macro_rules! assert_fn_eq { - ($a:expr, $b:expr) => { - assert_eq!($a as usize, $b as usize); - }; - } - - let aeffect = unsafe { &mut *VSTPluginMain(pass_callback) }; - - assert_eq!(aeffect.magic, VST_MAGIC); - assert_fn_eq!(aeffect.dispatcher, interfaces::dispatch); - assert_fn_eq!(aeffect._process, interfaces::process_deprecated); - assert_fn_eq!(aeffect.setParameter, interfaces::set_parameter); - assert_fn_eq!(aeffect.getParameter, interfaces::get_parameter); - assert_eq!(aeffect.numPrograms, 1); - assert_eq!(aeffect.numParams, 1); - assert_eq!(aeffect.numInputs, 2); - assert_eq!(aeffect.numOutputs, 2); - assert_eq!(aeffect.reserved1, 0); - assert_eq!(aeffect.reserved2, 0); - assert_eq!(aeffect.initialDelay, 123); - assert_eq!(aeffect.uniqueId, 5678); - assert_eq!(aeffect.version, 1234); - assert_fn_eq!(aeffect.processReplacing, interfaces::process_replacing); - assert_fn_eq!(aeffect.processReplacingF64, interfaces::process_replacing_f64); - } -} diff --git a/deps/vst/src/plugin.rs b/deps/vst/src/plugin.rs deleted file mode 100644 index 90a3fb17..00000000 --- a/deps/vst/src/plugin.rs +++ /dev/null @@ -1,1086 +0,0 @@ -//! Plugin specific structures. - -use num_enum::{IntoPrimitive, TryFromPrimitive}; - -use std::os::raw::c_void; -use std::ptr; -use std::sync::Arc; - -use crate::{ - api::{self, consts::VST_MAGIC, AEffect, HostCallbackProc, Supported, TimeInfo}, - buffer::AudioBuffer, - channels::ChannelInfo, - editor::Editor, - host::{self, Host}, -}; - -/// Plugin type. Generally either Effect or Synth. -/// -/// Other types are not necessary to build a plugin and are only useful for the host to categorize -/// the plugin. -#[repr(isize)] -#[derive(Clone, Copy, Debug, TryFromPrimitive, IntoPrimitive)] -pub enum Category { - /// Unknown / not implemented - Unknown, - /// Any effect - Effect, - /// VST instrument - Synth, - /// Scope, tuner, spectrum analyser, etc. - Analysis, - /// Dynamics, etc. - Mastering, - /// Panners, etc. - Spacializer, - /// Delays and Reverbs - RoomFx, - /// Dedicated surround processor. - SurroundFx, - /// Denoiser, etc. - Restoration, - /// Offline processing. - OfflineProcess, - /// Contains other plugins. - Shell, - /// Tone generator, etc. - Generator, -} - -#[repr(i32)] -#[derive(Clone, Copy, Debug, TryFromPrimitive, IntoPrimitive)] -#[doc(hidden)] -pub enum OpCode { - /// Called when plugin is initialized. - Initialize, - /// Called when plugin is being shut down. - Shutdown, - - /// [value]: preset number to change to. - ChangePreset, - /// [return]: current preset number. - GetCurrentPresetNum, - /// [ptr]: char array with new preset name, limited to `consts::MAX_PRESET_NAME_LEN`. - SetCurrentPresetName, - /// [ptr]: char buffer for current preset name, limited to `consts::MAX_PRESET_NAME_LEN`. - GetCurrentPresetName, - - /// [index]: parameter - /// [ptr]: char buffer, limited to `consts::MAX_PARAM_STR_LEN` (e.g. "db", "ms", etc) - GetParameterLabel, - /// [index]: parameter - /// [ptr]: char buffer, limited to `consts::MAX_PARAM_STR_LEN` (e.g. "0.5", "ROOM", etc). - GetParameterDisplay, - /// [index]: parameter - /// [ptr]: char buffer, limited to `consts::MAX_PARAM_STR_LEN` (e.g. "Release", "Gain") - GetParameterName, - - /// Deprecated. - _GetVu, - - /// [opt]: new sample rate. - SetSampleRate, - /// [value]: new maximum block size. - SetBlockSize, - /// [value]: 1 when plugin enabled, 0 when disabled. - StateChanged, - - /// [ptr]: Rect** receiving pointer to editor size. - EditorGetRect, - /// [ptr]: system dependent window pointer, eg HWND on Windows. - EditorOpen, - /// Close editor. No arguments. - EditorClose, - - /// Deprecated. - _EditorDraw, - /// Deprecated. - _EditorMouse, - /// Deprecated. - _EditorKey, - - /// Idle call from host. - EditorIdle, - - /// Deprecated. - _EditorTop, - /// Deprecated. - _EditorSleep, - /// Deprecated. - _EditorIdentify, - - /// [ptr]: pointer for chunk data address (void**). - /// [index]: 0 for bank, 1 for program - GetData, - /// [ptr]: data (void*) - /// [value]: data size in bytes - /// [index]: 0 for bank, 1 for program - SetData, - - /// [ptr]: VstEvents* TODO: Events - ProcessEvents, - /// [index]: param index - /// [return]: 1=true, 0=false - CanBeAutomated, - /// [index]: param index - /// [ptr]: parameter string - /// [return]: true for success - StringToParameter, - - /// Deprecated. - _GetNumCategories, - - /// [index]: program name - /// [ptr]: char buffer for name, limited to `consts::MAX_PRESET_NAME_LEN` - /// [return]: true for success - GetPresetName, - - /// Deprecated. - _CopyPreset, - /// Deprecated. - _ConnectIn, - /// Deprecated. - _ConnectOut, - - /// [index]: input index - /// [ptr]: `VstPinProperties` - /// [return]: 1 if supported - GetInputInfo, - /// [index]: output index - /// [ptr]: `VstPinProperties` - /// [return]: 1 if supported - GetOutputInfo, - /// [return]: `PluginCategory` category. - GetCategory, - - /// Deprecated. - _GetCurrentPosition, - /// Deprecated. - _GetDestinationBuffer, - - /// [ptr]: `VstAudioFile` array - /// [value]: count - /// [index]: start flag - OfflineNotify, - /// [ptr]: `VstOfflineTask` array - /// [value]: count - OfflinePrepare, - /// [ptr]: `VstOfflineTask` array - /// [value]: count - OfflineRun, - - /// [ptr]: `VstVariableIo` - /// [use]: used for variable I/O processing (offline e.g. timestretching) - ProcessVarIo, - /// TODO: implement - /// [value]: input `*mut VstSpeakerArrangement`. - /// [ptr]: output `*mut VstSpeakerArrangement`. - SetSpeakerArrangement, - - /// Deprecated. - _SetBlocksizeAndSampleRate, - - /// Soft bypass (automatable). - /// [value]: 1 = bypass, 0 = nobypass. - SoftBypass, - // [ptr]: buffer for effect name, limited to `kVstMaxEffectNameLen` - GetEffectName, - - /// Deprecated. - _GetErrorText, - - /// [ptr]: buffer for vendor name, limited to `consts::MAX_VENDOR_STR_LEN`. - GetVendorName, - /// [ptr]: buffer for product name, limited to `consts::MAX_PRODUCT_STR_LEN`. - GetProductName, - /// [return]: vendor specific version. - GetVendorVersion, - /// no definition, vendor specific. - VendorSpecific, - /// [ptr]: "Can do" string. - /// [return]: 1 = yes, 0 = maybe, -1 = no. - CanDo, - /// [return]: tail size (e.g. reverb time). 0 is default, 1 means no tail. - GetTailSize, - - /// Deprecated. - _Idle, - /// Deprecated. - _GetIcon, - /// Deprecated. - _SetVewPosition, - - /// [index]: param index - /// [ptr]: `*mut VstParamInfo` //TODO: Implement - /// [return]: 1 if supported - GetParamInfo, - - /// Deprecated. - _KeysRequired, - - /// [return]: 2400 for vst 2.4. - GetApiVersion, - - /// [index]: ASCII char. - /// [value]: `Key` keycode. - /// [opt]: `flags::modifier_key` bitmask. - /// [return]: 1 if used. - EditorKeyDown, - /// [index]: ASCII char. - /// [value]: `Key` keycode. - /// [opt]: `flags::modifier_key` bitmask. - /// [return]: 1 if used. - EditorKeyUp, - /// [value]: 0 = circular, 1 = circular relative, 2 = linear. - EditorSetKnobMode, - - /// [index]: MIDI channel. - /// [ptr]: `*mut MidiProgramName`. //TODO: Implement - /// [return]: number of used programs, 0 = unsupported. - GetMidiProgramName, - /// [index]: MIDI channel. - /// [ptr]: `*mut MidiProgramName`. //TODO: Implement - /// [return]: index of current program. - GetCurrentMidiProgram, - /// [index]: MIDI channel. - /// [ptr]: `*mut MidiProgramCategory`. //TODO: Implement - /// [return]: number of used categories. - GetMidiProgramCategory, - /// [index]: MIDI channel. - /// [return]: 1 if `MidiProgramName` or `MidiKeyName` has changed. //TODO: Implement - HasMidiProgramsChanged, - /// [index]: MIDI channel. - /// [ptr]: `*mut MidiKeyName`. //TODO: Implement - /// [return]: 1 = supported 0 = not. - GetMidiKeyName, - - /// Called before a preset is loaded. - BeginSetPreset, - /// Called after a preset is loaded. - EndSetPreset, - - /// [value]: inputs `*mut VstSpeakerArrangement` //TODO: Implement - /// [ptr]: Outputs `*mut VstSpeakerArrangement` - GetSpeakerArrangement, - /// [ptr]: buffer for plugin name, limited to `consts::MAX_PRODUCT_STR_LEN`. - /// [return]: next plugin's uniqueID. - ShellGetNextPlugin, - - /// No args. Called once before start of process call. This indicates that the process call - /// will be interrupted (e.g. Host reconfiguration or bypass when plugin doesn't support - /// SoftBypass) - StartProcess, - /// No arguments. Called after stop of process call. - StopProcess, - /// [value]: number of samples to process. Called in offline mode before process. - SetTotalSampleToProcess, - /// [value]: pan law `PanLaw`. //TODO: Implement - /// [opt]: gain. - SetPanLaw, - - /// [ptr]: `*mut VstPatchChunkInfo`. //TODO: Implement - /// [return]: -1 = bank cant be loaded, 1 = can be loaded, 0 = unsupported. - BeginLoadBank, - /// [ptr]: `*mut VstPatchChunkInfo`. //TODO: Implement - /// [return]: -1 = bank cant be loaded, 1 = can be loaded, 0 = unsupported. - BeginLoadPreset, - - /// [value]: 0 if 32 bit, anything else if 64 bit. - SetPrecision, - - /// [return]: number of used MIDI Inputs (1-15). - GetNumMidiInputs, - /// [return]: number of used MIDI Outputs (1-15). - GetNumMidiOutputs, -} - -/// A structure representing static plugin information. -#[derive(Clone, Debug)] -pub struct Info { - /// Plugin Name. - pub name: String, - - /// Plugin Vendor. - pub vendor: String, - - /// Number of different presets. - pub presets: i32, - - /// Number of parameters. - pub parameters: i32, - - /// Number of inputs. - pub inputs: i32, - - /// Number of outputs. - pub outputs: i32, - - /// Number of MIDI input channels (1-16), or 0 for the default of 16 channels. - pub midi_inputs: i32, - - /// Number of MIDI output channels (1-16), or 0 for the default of 16 channels. - pub midi_outputs: i32, - - /// Unique plugin ID. Can be registered with Steinberg to prevent conflicts with other plugins. - /// - /// This ID is used to identify a plugin during save and load of a preset and project. - pub unique_id: i32, - - /// Plugin version (e.g. 0001 = `v0.0.0.1`, 1283 = `v1.2.8.3`). - pub version: i32, - - /// Plugin category. Possible values are found in `enums::PluginCategory`. - pub category: Category, - - /// Latency of the plugin in samples. - /// - /// This reports how many samples it takes for the plugin to create an output (group delay). - pub initial_delay: i32, - - /// Indicates that preset data is handled in formatless chunks. - /// - /// If false, host saves and restores plugin by reading/writing parameter data. If true, it is - /// up to the plugin to manage saving preset data by implementing the - /// `{get, load}_{preset, bank}_chunks()` methods. Default is `false`. - pub preset_chunks: bool, - - /// Indicates whether this plugin can process f64 based `AudioBuffer` buffers. - /// - /// Default is `false`. - pub f64_precision: bool, - - /// If this is true, the plugin will not produce sound when the input is silence. - /// - /// Default is `false`. - pub silent_when_stopped: bool, -} - -impl Default for Info { - fn default() -> Info { - Info { - name: "VST".to_string(), - vendor: String::new(), - - presets: 1, // default preset - parameters: 0, - inputs: 2, // Stereo in,out - outputs: 2, - - midi_inputs: 0, - midi_outputs: 0, - - unique_id: 0, // This must be changed. - version: 1, // v0.0.0.1 - - category: Category::Effect, - - initial_delay: 0, - - preset_chunks: false, - f64_precision: false, - silent_when_stopped: false, - } - } -} - -/// Features which are optionally supported by a plugin. These are queried by the host at run time. -#[derive(Debug)] -#[allow(missing_docs)] -pub enum CanDo { - SendEvents, - SendMidiEvent, - ReceiveEvents, - ReceiveMidiEvent, - ReceiveTimeInfo, - Offline, - MidiProgramNames, - Bypass, - ReceiveSysExEvent, - - //Bitwig specific? - MidiSingleNoteTuningChange, - MidiKeyBasedInstrumentControl, - - Other(String), -} - -impl CanDo { - // TODO: implement FromStr - #![allow(clippy::should_implement_trait)] - /// Converts a string to a `CanDo` instance. Any given string that does not match the predefined - /// values will return a `CanDo::Other` value. - pub fn from_str(s: &str) -> CanDo { - use self::CanDo::*; - - match s { - "sendVstEvents" => SendEvents, - "sendVstMidiEvent" => SendMidiEvent, - "receiveVstEvents" => ReceiveEvents, - "receiveVstMidiEvent" => ReceiveMidiEvent, - "receiveVstTimeInfo" => ReceiveTimeInfo, - "offline" => Offline, - "midiProgramNames" => MidiProgramNames, - "bypass" => Bypass, - - "receiveVstSysexEvent" => ReceiveSysExEvent, - "midiSingleNoteTuningChange" => MidiSingleNoteTuningChange, - "midiKeyBasedInstrumentControl" => MidiKeyBasedInstrumentControl, - otherwise => Other(otherwise.to_string()), - } - } -} - -impl Into for CanDo { - fn into(self) -> String { - use self::CanDo::*; - - match self { - SendEvents => "sendVstEvents".to_string(), - SendMidiEvent => "sendVstMidiEvent".to_string(), - ReceiveEvents => "receiveVstEvents".to_string(), - ReceiveMidiEvent => "receiveVstMidiEvent".to_string(), - ReceiveTimeInfo => "receiveVstTimeInfo".to_string(), - Offline => "offline".to_string(), - MidiProgramNames => "midiProgramNames".to_string(), - Bypass => "bypass".to_string(), - - ReceiveSysExEvent => "receiveVstSysexEvent".to_string(), - MidiSingleNoteTuningChange => "midiSingleNoteTuningChange".to_string(), - MidiKeyBasedInstrumentControl => "midiKeyBasedInstrumentControl".to_string(), - Other(other) => other, - } - } -} - -/// Must be implemented by all VST plugins. -/// -/// All methods except `new` and `get_info` provide a default implementation -/// which does nothing and can be safely overridden. -/// -/// At any time, a plugin is in one of two states: *suspended* or *resumed*. -/// While a plugin is in the *suspended* state, various processing parameters, -/// such as the sample rate and block size, can be changed by the host, but no -/// audio processing takes place. While a plugin is in the *resumed* state, -/// audio processing methods and parameter access methods can be called by -/// the host. A plugin starts in the *suspended* state and is switched between -/// the states by the host using the `resume` and `suspend` methods. -/// -/// Hosts call methods of the plugin on two threads: the UI thread and the -/// processing thread. For this reason, the plugin API is separated into two -/// traits: The `Plugin` trait containing setup and processing methods, and -/// the `PluginParameters` trait containing methods for parameter access. -#[cfg_attr( - not(feature = "disable_deprecation_warning"), - deprecated = "This crate has been deprecated. See https://github.com/RustAudio/vst-rs for more information." -)] -#[allow(unused_variables)] -pub trait Plugin: Send { - /// This method must return an `Info` struct. - fn get_info(&self) -> Info; - - /// Called during initialization to pass a `HostCallback` to the plugin. - /// - /// This method can be overridden to set `host` as a field in the plugin struct. - /// - /// # Example - /// - /// ``` - /// // ... - /// # extern crate vst; - /// # #[macro_use] extern crate log; - /// # use vst::plugin::{Plugin, Info}; - /// use vst::plugin::HostCallback; - /// - /// struct ExamplePlugin { - /// host: HostCallback - /// } - /// - /// impl Plugin for ExamplePlugin { - /// fn new(host: HostCallback) -> ExamplePlugin { - /// ExamplePlugin { - /// host - /// } - /// } - /// - /// fn init(&mut self) { - /// info!("loaded with host vst version: {}", self.host.vst_version()); - /// } - /// - /// // ... - /// # fn get_info(&self) -> Info { - /// # Info { - /// # name: "Example Plugin".to_string(), - /// # ..Default::default() - /// # } - /// # } - /// } - /// - /// # fn main() {} - /// ``` - fn new(host: HostCallback) -> Self - where - Self: Sized; - - /// Called when plugin is fully initialized. - /// - /// This method is only called while the plugin is in the *suspended* state. - fn init(&mut self) { - trace!("Initialized vst plugin."); - } - - /// Called when sample rate is changed by host. - /// - /// This method is only called while the plugin is in the *suspended* state. - fn set_sample_rate(&mut self, rate: f32) {} - - /// Called when block size is changed by host. - /// - /// This method is only called while the plugin is in the *suspended* state. - fn set_block_size(&mut self, size: i64) {} - - /// Called to transition the plugin into the *resumed* state. - fn resume(&mut self) {} - - /// Called to transition the plugin into the *suspended* state. - fn suspend(&mut self) {} - - /// Vendor specific handling. - fn vendor_specific(&mut self, index: i32, value: isize, ptr: *mut c_void, opt: f32) -> isize { - 0 - } - - /// Return whether plugin supports specified action. - /// - /// This method is only called while the plugin is in the *suspended* state. - fn can_do(&self, can_do: CanDo) -> Supported { - info!("Host is asking if plugin can: {:?}.", can_do); - Supported::Maybe - } - - /// Get the tail size of plugin when it is stopped. Used in offline processing as well. - fn get_tail_size(&self) -> isize { - 0 - } - - /// Process an audio buffer containing `f32` values. - /// - /// # Example - /// ```no_run - /// # use vst::plugin::{HostCallback, Info, Plugin}; - /// # use vst::buffer::AudioBuffer; - /// # - /// # struct ExamplePlugin; - /// # impl Plugin for ExamplePlugin { - /// # fn new(_host: HostCallback) -> Self { Self } - /// # - /// # fn get_info(&self) -> Info { Default::default() } - /// # - /// // Processor that clips samples above 0.4 or below -0.4: - /// fn process(&mut self, buffer: &mut AudioBuffer){ - /// // For each input and output - /// for (input, output) in buffer.zip() { - /// // For each input sample and output sample in buffer - /// for (in_sample, out_sample) in input.into_iter().zip(output.into_iter()) { - /// *out_sample = if *in_sample > 0.4 { - /// 0.4 - /// } else if *in_sample < -0.4 { - /// -0.4 - /// } else { - /// *in_sample - /// }; - /// } - /// } - /// } - /// # } - /// ``` - /// - /// This method is only called while the plugin is in the *resumed* state. - fn process(&mut self, buffer: &mut AudioBuffer) { - // For each input and output - for (input, output) in buffer.zip() { - // For each input sample and output sample in buffer - for (in_frame, out_frame) in input.iter().zip(output.iter_mut()) { - *out_frame = *in_frame; - } - } - } - - /// Process an audio buffer containing `f64` values. - /// - /// # Example - /// ```no_run - /// # use vst::plugin::{HostCallback, Info, Plugin}; - /// # use vst::buffer::AudioBuffer; - /// # - /// # struct ExamplePlugin; - /// # impl Plugin for ExamplePlugin { - /// # fn new(_host: HostCallback) -> Self { Self } - /// # - /// # fn get_info(&self) -> Info { Default::default() } - /// # - /// // Processor that clips samples above 0.4 or below -0.4: - /// fn process_f64(&mut self, buffer: &mut AudioBuffer){ - /// // For each input and output - /// for (input, output) in buffer.zip() { - /// // For each input sample and output sample in buffer - /// for (in_sample, out_sample) in input.into_iter().zip(output.into_iter()) { - /// *out_sample = if *in_sample > 0.4 { - /// 0.4 - /// } else if *in_sample < -0.4 { - /// -0.4 - /// } else { - /// *in_sample - /// }; - /// } - /// } - /// } - /// # } - /// ``` - /// - /// This method is only called while the plugin is in the *resumed* state. - fn process_f64(&mut self, buffer: &mut AudioBuffer) { - // For each input and output - for (input, output) in buffer.zip() { - // For each input sample and output sample in buffer - for (in_frame, out_frame) in input.iter().zip(output.iter_mut()) { - *out_frame = *in_frame; - } - } - } - - /// Handle incoming events sent from the host. - /// - /// This is always called before the start of `process` or `process_f64`. - /// - /// This method is only called while the plugin is in the *resumed* state. - fn process_events(&mut self, events: &api::Events) {} - - /// Get a reference to the shared parameter object. - fn get_parameter_object(&mut self) -> Arc { - Arc::new(DummyPluginParameters) - } - - /// Get information about an input channel. Only used by some hosts. - fn get_input_info(&self, input: i32) -> ChannelInfo { - ChannelInfo::new( - format!("Input channel {}", input), - Some(format!("In {}", input)), - true, - None, - ) - } - - /// Get information about an output channel. Only used by some hosts. - fn get_output_info(&self, output: i32) -> ChannelInfo { - ChannelInfo::new( - format!("Output channel {}", output), - Some(format!("Out {}", output)), - true, - None, - ) - } - - /// Called one time before the start of process call. - /// - /// This indicates that the process call will be interrupted (due to Host reconfiguration - /// or bypass state when the plug-in doesn't support softBypass). - /// - /// This method is only called while the plugin is in the *resumed* state. - fn start_process(&mut self) {} - - /// Called after the stop of process call. - /// - /// This method is only called while the plugin is in the *resumed* state. - fn stop_process(&mut self) {} - - /// Return handle to plugin editor if supported. - /// The method need only return the object on the first call. - /// Subsequent calls can just return `None`. - /// - /// The editor object will typically contain an `Arc` reference to the parameter - /// object through which it can communicate with the audio processing. - fn get_editor(&mut self) -> Option> { - None - } -} - -/// Parameter object shared between the UI and processing threads. -/// Since access is shared, all methods take `self` by immutable reference. -/// All mutation must thus be performed using thread-safe interior mutability. -#[allow(unused_variables)] -pub trait PluginParameters: Sync { - /// Set the current preset to the index specified by `preset`. - /// - /// This method can be called on the processing thread for automation. - fn change_preset(&self, preset: i32) {} - - /// Get the current preset index. - fn get_preset_num(&self) -> i32 { - 0 - } - - /// Set the current preset name. - fn set_preset_name(&self, name: String) {} - - /// Get the name of the preset at the index specified by `preset`. - fn get_preset_name(&self, preset: i32) -> String { - "".to_string() - } - - /// Get parameter label for parameter at `index` (e.g. "db", "sec", "ms", "%"). - fn get_parameter_label(&self, index: i32) -> String { - "".to_string() - } - - /// Get the parameter value for parameter at `index` (e.g. "1.0", "150", "Plate", "Off"). - fn get_parameter_text(&self, index: i32) -> String { - format!("{:.3}", self.get_parameter(index)) - } - - /// Get the name of parameter at `index`. - fn get_parameter_name(&self, index: i32) -> String { - format!("Param {}", index) - } - - /// Get the value of parameter at `index`. Should be value between 0.0 and 1.0. - fn get_parameter(&self, index: i32) -> f32 { - 0.0 - } - - /// Set the value of parameter at `index`. `value` is between 0.0 and 1.0. - /// - /// This method can be called on the processing thread for automation. - fn set_parameter(&self, index: i32, value: f32) {} - - /// Return whether parameter at `index` can be automated. - fn can_be_automated(&self, index: i32) -> bool { - true - } - - /// Use String as input for parameter value. Used by host to provide an editable field to - /// adjust a parameter value. E.g. "100" may be interpreted as 100hz for parameter. Returns if - /// the input string was used. - fn string_to_parameter(&self, index: i32, text: String) -> bool { - false - } - - /// If `preset_chunks` is set to true in plugin info, this should return the raw chunk data for - /// the current preset. - fn get_preset_data(&self) -> Vec { - Vec::new() - } - - /// If `preset_chunks` is set to true in plugin info, this should return the raw chunk data for - /// the current plugin bank. - fn get_bank_data(&self) -> Vec { - Vec::new() - } - - /// If `preset_chunks` is set to true in plugin info, this should load a preset from the given - /// chunk data. - fn load_preset_data(&self, data: &[u8]) {} - - /// If `preset_chunks` is set to true in plugin info, this should load a preset bank from the - /// given chunk data. - fn load_bank_data(&self, data: &[u8]) {} -} - -struct DummyPluginParameters; - -impl PluginParameters for DummyPluginParameters {} - -/// A reference to the host which allows the plugin to call back and access information. -/// -/// # Panics -/// -/// All methods in this struct will panic if the `HostCallback` was constructed using -/// `Default::default()` rather than being set to the value passed to `Plugin::new`. -#[derive(Copy, Clone)] -pub struct HostCallback { - callback: Option, - effect: *mut AEffect, -} - -/// `HostCallback` implements `Default` so that the plugin can implement `Default` and have a -/// `HostCallback` field. -impl Default for HostCallback { - fn default() -> HostCallback { - HostCallback { - callback: None, - effect: ptr::null_mut(), - } - } -} - -unsafe impl Send for HostCallback {} -unsafe impl Sync for HostCallback {} - -impl HostCallback { - /// Wrap callback in a function to avoid using fn pointer notation. - #[doc(hidden)] - fn callback( - &self, - effect: *mut AEffect, - opcode: host::OpCode, - index: i32, - value: isize, - ptr: *mut c_void, - opt: f32, - ) -> isize { - let callback = self.callback.unwrap_or_else(|| panic!("Host not yet initialized.")); - callback(effect, opcode.into(), index, value, ptr, opt) - } - - /// Check whether the plugin has been initialized. - #[doc(hidden)] - fn is_effect_valid(&self) -> bool { - // Check whether `effect` points to a valid AEffect struct - unsafe { (*self.effect).magic as i32 == VST_MAGIC } - } - - /// Create a new Host structure wrapping a host callback. - #[doc(hidden)] - pub fn wrap(callback: HostCallbackProc, effect: *mut AEffect) -> HostCallback { - HostCallback { - callback: Some(callback), - effect, - } - } - - /// Get the VST API version supported by the host e.g. `2400 = VST 2.4`. - pub fn vst_version(&self) -> i32 { - self.callback(self.effect, host::OpCode::Version, 0, 0, ptr::null_mut(), 0.0) as i32 - } - - /// Get the callback for calling host-specific extensions - #[inline(always)] - pub fn raw_callback(&self) -> Option { - self.callback - } - - /// Get the effect pointer for calling host-specific extensions - #[inline(always)] - pub fn raw_effect(&self) -> *mut AEffect { - self.effect - } - - fn read_string(&self, opcode: host::OpCode, max: usize) -> String { - self.read_string_param(opcode, 0, 0, 0.0, max) - } - - fn read_string_param(&self, opcode: host::OpCode, index: i32, value: isize, opt: f32, max: usize) -> String { - let mut buf = vec![0; max]; - self.callback(self.effect, opcode, index, value, buf.as_mut_ptr() as *mut c_void, opt); - String::from_utf8_lossy(&buf) - .chars() - .take_while(|c| *c != '\0') - .collect() - } -} - -impl Host for HostCallback { - /// Signal the host that the value for the parameter has changed. - /// - /// Make sure to also call `begin_edit` and `end_edit` when a parameter - /// has been touched. This is important for the host to determine - /// if a user interaction is happening and the automation should be recorded. - fn automate(&self, index: i32, value: f32) { - if self.is_effect_valid() { - // TODO: Investigate removing this check, should be up to host - self.callback(self.effect, host::OpCode::Automate, index, 0, ptr::null_mut(), value); - } - } - - /// Signal the host the start of a parameter change a gesture (mouse down on knob dragging). - fn begin_edit(&self, index: i32) { - self.callback(self.effect, host::OpCode::BeginEdit, index, 0, ptr::null_mut(), 0.0); - } - - /// Signal the host the end of a parameter change gesture (mouse up after knob dragging). - fn end_edit(&self, index: i32) { - self.callback(self.effect, host::OpCode::EndEdit, index, 0, ptr::null_mut(), 0.0); - } - - fn get_plugin_id(&self) -> i32 { - self.callback(self.effect, host::OpCode::CurrentId, 0, 0, ptr::null_mut(), 0.0) as i32 - } - - fn idle(&self) { - self.callback(self.effect, host::OpCode::Idle, 0, 0, ptr::null_mut(), 0.0); - } - - fn get_info(&self) -> (isize, String, String) { - use api::consts::*; - let version = self.callback(self.effect, host::OpCode::CurrentId, 0, 0, ptr::null_mut(), 0.0) as isize; - let vendor_name = self.read_string(host::OpCode::GetVendorString, MAX_VENDOR_STR_LEN); - let product_name = self.read_string(host::OpCode::GetProductString, MAX_PRODUCT_STR_LEN); - (version, vendor_name, product_name) - } - - /// Send events to the host. - /// - /// This should only be called within [`process`] or [`process_f64`]. Calling `process_events` - /// anywhere else is undefined behaviour and may crash some hosts. - /// - /// [`process`]: trait.Plugin.html#method.process - /// [`process_f64`]: trait.Plugin.html#method.process_f64 - fn process_events(&self, events: &api::Events) { - self.callback( - self.effect, - host::OpCode::ProcessEvents, - 0, - 0, - events as *const _ as *mut _, - 0.0, - ); - } - - /// Request time information from Host. - /// - /// The mask parameter is composed of the same flags which will be found in the `flags` field of `TimeInfo` when returned. - /// That is, if you want the host's tempo, the parameter passed to `get_time_info()` should have the `TEMPO_VALID` flag set. - /// This request and delivery system is important, as a request like this may cause - /// significant calculations at the application's end, which may take a lot of our precious time. - /// This obviously means you should only set those flags that are required to get the information you need. - /// - /// Also please be aware that requesting information does not necessarily mean that that information is provided in return. - /// Check the flags field in the `TimeInfo` structure to see if your request was actually met. - fn get_time_info(&self, mask: i32) -> Option { - let opcode = host::OpCode::GetTime; - let mask = mask as isize; - let null = ptr::null_mut(); - let ptr = self.callback(self.effect, opcode, 0, mask, null, 0.0); - - match ptr { - 0 => None, - ptr => Some(unsafe { *(ptr as *const TimeInfo) }), - } - } - - /// Get block size. - fn get_block_size(&self) -> isize { - self.callback(self.effect, host::OpCode::GetBlockSize, 0, 0, ptr::null_mut(), 0.0) - } - - /// Refresh UI after the plugin's parameters changed. - fn update_display(&self) { - self.callback(self.effect, host::OpCode::UpdateDisplay, 0, 0, ptr::null_mut(), 0.0); - } -} - -#[cfg(test)] -mod tests { - use std::ptr; - - use crate::plugin; - - /// Create a plugin instance. - /// - /// This is a macro to allow you to specify attributes on the created struct. - macro_rules! make_plugin { - ($($attr:meta) *) => { - use std::convert::TryFrom; - use std::os::raw::c_void; - - use crate::main; - use crate::api::AEffect; - use crate::host::{Host, OpCode}; - use crate::plugin::{HostCallback, Info, Plugin}; - - $(#[$attr]) * - struct TestPlugin { - host: HostCallback - } - - impl Plugin for TestPlugin { - fn get_info(&self) -> Info { - Info { - name: "Test Plugin".to_string(), - ..Default::default() - } - } - - fn new(host: HostCallback) -> TestPlugin { - TestPlugin { - host - } - } - - fn init(&mut self) { - info!("Loaded with host vst version: {}", self.host.vst_version()); - assert_eq!(2400, self.host.vst_version()); - assert_eq!(9876, self.host.get_plugin_id()); - // Callback will assert these. - self.host.begin_edit(123); - self.host.automate(123, 12.3); - self.host.end_edit(123); - self.host.idle(); - } - } - - #[allow(dead_code)] - fn instance() -> *mut AEffect { - extern "C" fn host_callback( - _effect: *mut AEffect, - opcode: i32, - index: i32, - _value: isize, - _ptr: *mut c_void, - opt: f32, - ) -> isize { - match OpCode::try_from(opcode) { - Ok(OpCode::BeginEdit) => { - assert_eq!(index, 123); - 0 - }, - Ok(OpCode::Automate) => { - assert_eq!(index, 123); - assert_eq!(opt, 12.3); - 0 - }, - Ok(OpCode::EndEdit) => { - assert_eq!(index, 123); - 0 - }, - Ok(OpCode::Version) => 2400, - Ok(OpCode::CurrentId) => 9876, - Ok(OpCode::Idle) => 0, - _ => 0 - } - } - - main::(host_callback) - } - } - } - - make_plugin!(derive(Default)); - - #[test] - #[should_panic] - fn null_panic() { - make_plugin!(/* no `derive(Default)` */); - - impl Default for TestPlugin { - fn default() -> TestPlugin { - let plugin = TestPlugin { - host: Default::default(), - }; - - // Should panic - let version = plugin.host.vst_version(); - info!("Loaded with host vst version: {}", version); - - plugin - } - } - - TestPlugin::default(); - } - - #[test] - fn host_callbacks() { - let aeffect = instance(); - (unsafe { (*aeffect).dispatcher })(aeffect, plugin::OpCode::Initialize.into(), 0, 0, ptr::null_mut(), 0.0); - } -} diff --git a/deps/vst/src/prelude.rs b/deps/vst/src/prelude.rs deleted file mode 100644 index dda5705e..00000000 --- a/deps/vst/src/prelude.rs +++ /dev/null @@ -1,12 +0,0 @@ -//! A collection of commonly used items for implement a Plugin - -#[doc(no_inline)] -pub use crate::api::{Events, Supported}; -#[doc(no_inline)] -pub use crate::buffer::{AudioBuffer, SendEventBuffer}; -#[doc(no_inline)] -pub use crate::event::{Event, MidiEvent}; -#[doc(no_inline)] -pub use crate::plugin::{CanDo, Category, HostCallback, Info, Plugin, PluginParameters}; -#[doc(no_inline)] -pub use crate::util::{AtomicFloat, ParameterTransfer}; diff --git a/deps/vst/src/util/atomic_float.rs b/deps/vst/src/util/atomic_float.rs deleted file mode 100644 index e1cce2df..00000000 --- a/deps/vst/src/util/atomic_float.rs +++ /dev/null @@ -1,59 +0,0 @@ -use std::sync::atomic::{AtomicU32, Ordering}; - -/// Simple atomic floating point variable with relaxed ordering. -/// -/// Designed for the common case of sharing VST parameters between -/// multiple threads when no synchronization or change notification -/// is needed. -pub struct AtomicFloat { - atomic: AtomicU32, -} - -impl AtomicFloat { - /// New atomic float with initial value `value`. - pub fn new(value: f32) -> AtomicFloat { - AtomicFloat { - atomic: AtomicU32::new(value.to_bits()), - } - } - - /// Get the current value of the atomic float. - pub fn get(&self) -> f32 { - f32::from_bits(self.atomic.load(Ordering::Relaxed)) - } - - /// Set the value of the atomic float to `value`. - pub fn set(&self, value: f32) { - self.atomic.store(value.to_bits(), Ordering::Relaxed) - } -} - -impl Default for AtomicFloat { - fn default() -> Self { - AtomicFloat::new(0.0) - } -} - -impl std::fmt::Debug for AtomicFloat { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - std::fmt::Debug::fmt(&self.get(), f) - } -} - -impl std::fmt::Display for AtomicFloat { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - std::fmt::Display::fmt(&self.get(), f) - } -} - -impl From for AtomicFloat { - fn from(value: f32) -> Self { - AtomicFloat::new(value) - } -} - -impl From for f32 { - fn from(value: AtomicFloat) -> Self { - value.get() - } -} diff --git a/deps/vst/src/util/mod.rs b/deps/vst/src/util/mod.rs deleted file mode 100644 index fbe7a87e..00000000 --- a/deps/vst/src/util/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! Structures for easing the implementation of VST plugins. - -mod atomic_float; -mod parameter_transfer; - -pub use self::atomic_float::AtomicFloat; -pub use self::parameter_transfer::{ParameterTransfer, ParameterTransferIterator}; diff --git a/deps/vst/src/util/parameter_transfer.rs b/deps/vst/src/util/parameter_transfer.rs deleted file mode 100644 index 37ebc92b..00000000 --- a/deps/vst/src/util/parameter_transfer.rs +++ /dev/null @@ -1,187 +0,0 @@ -use std::mem::size_of; -use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering}; - -const USIZE_BITS: usize = size_of::() * 8; - -fn word_and_bit(index: usize) -> (usize, usize) { - (index / USIZE_BITS, 1usize << (index & (USIZE_BITS - 1))) -} - -/// A set of parameters that can be shared between threads. -/// -/// Supports efficient iteration over parameters that changed since last iteration. -#[derive(Default)] -pub struct ParameterTransfer { - values: Vec, - changed: Vec, -} - -impl ParameterTransfer { - /// Create a new parameter set with `parameter_count` parameters. - pub fn new(parameter_count: usize) -> Self { - let bit_words = (parameter_count + USIZE_BITS - 1) / USIZE_BITS; - ParameterTransfer { - values: (0..parameter_count).map(|_| AtomicU32::new(0)).collect(), - changed: (0..bit_words).map(|_| AtomicUsize::new(0)).collect(), - } - } - - /// Set the value of the parameter with index `index` to `value` and mark - /// it as changed. - pub fn set_parameter(&self, index: usize, value: f32) { - let (word, bit) = word_and_bit(index); - self.values[index].store(value.to_bits(), Ordering::Relaxed); - self.changed[word].fetch_or(bit, Ordering::AcqRel); - } - - /// Get the current value of the parameter with index `index`. - pub fn get_parameter(&self, index: usize) -> f32 { - f32::from_bits(self.values[index].load(Ordering::Relaxed)) - } - - /// Iterate over all parameters marked as changed. If `acquire` is `true`, - /// mark all returned parameters as no longer changed. - /// - /// The iterator returns a pair of `(index, value)` for each changed parameter. - /// - /// When parameters have been changed on the current thread, the iterator is - /// precise: it reports all changed parameters with the values they were last - /// changed to. - /// - /// When parameters are changed on a different thread, the iterator is - /// conservative, in the sense that it is guaranteed to report changed - /// parameters eventually, but if a parameter is changed multiple times in - /// a short period of time, it may skip some of the changes (but never the - /// last) and may report an extra, spurious change at the end. - /// - /// The changed parameters are reported in increasing index order, and the same - /// parameter is never reported more than once in the same iteration. - pub fn iterate(&self, acquire: bool) -> ParameterTransferIterator { - ParameterTransferIterator { - pt: self, - word: 0, - bit: 1, - acquire, - } - } -} - -/// An iterator over changed parameters. -/// Returned by [`iterate`](struct.ParameterTransfer.html#method.iterate). -pub struct ParameterTransferIterator<'pt> { - pt: &'pt ParameterTransfer, - word: usize, - bit: usize, - acquire: bool, -} - -impl<'pt> Iterator for ParameterTransferIterator<'pt> { - type Item = (usize, f32); - - fn next(&mut self) -> Option<(usize, f32)> { - let bits = loop { - if self.word == self.pt.changed.len() { - return None; - } - let bits = self.pt.changed[self.word].load(Ordering::Acquire) & self.bit.wrapping_neg(); - if bits != 0 { - break bits; - } - self.word += 1; - self.bit = 1; - }; - - let bit_index = bits.trailing_zeros() as usize; - let bit = 1usize << bit_index; - let index = self.word * USIZE_BITS + bit_index; - - if self.acquire { - self.pt.changed[self.word].fetch_and(!bit, Ordering::AcqRel); - } - - let next_bit = bit << 1; - if next_bit == 0 { - self.word += 1; - self.bit = 1; - } else { - self.bit = next_bit; - } - - Some((index, self.pt.get_parameter(index))) - } -} - -#[cfg(test)] -mod tests { - extern crate rand; - - use crate::util::ParameterTransfer; - - use std::sync::mpsc::channel; - use std::sync::Arc; - use std::thread; - use std::time::Duration; - - use self::rand::rngs::StdRng; - use self::rand::{Rng, SeedableRng}; - - const THREADS: usize = 3; - const PARAMETERS: usize = 1000; - const UPDATES: usize = 1_000_000; - - #[test] - fn parameter_transfer() { - let transfer = Arc::new(ParameterTransfer::new(PARAMETERS)); - let (tx, rx) = channel(); - - // Launch threads that change parameters - for t in 0..THREADS { - let t_transfer = Arc::clone(&transfer); - let t_tx = tx.clone(); - let mut t_rng = StdRng::seed_from_u64(t as u64); - thread::spawn(move || { - let mut values = vec![0f32; PARAMETERS]; - for _ in 0..UPDATES { - let p: usize = t_rng.gen_range(0..PARAMETERS); - let v: f32 = t_rng.gen_range(0.0..1.0); - values[p] = v; - t_transfer.set_parameter(p, v); - } - t_tx.send(values).unwrap(); - }); - } - - // Continually receive updates from threads - let mut values = vec![0f32; PARAMETERS]; - let mut results = vec![]; - let mut acquire_rng = StdRng::seed_from_u64(42); - while results.len() < THREADS { - let mut last_p = -1; - for (p, v) in transfer.iterate(acquire_rng.gen_bool(0.9)) { - assert!(p as isize > last_p); - last_p = p as isize; - values[p] = v; - } - thread::sleep(Duration::from_micros(100)); - while let Ok(result) = rx.try_recv() { - results.push(result); - } - } - - // One last iteration to pick up all updates - let mut last_p = -1; - for (p, v) in transfer.iterate(true) { - assert!(p as isize > last_p); - last_p = p as isize; - values[p] = v; - } - - // Now there should be no more updates - assert!(transfer.iterate(true).next().is_none()); - - // Verify final values - for p in 0..PARAMETERS { - assert!((0..THREADS).any(|t| results[t][p] == values[p])); - } - } -} diff --git a/device/Cargo.toml b/device/Cargo.toml deleted file mode 100644 index 345640b0..00000000 --- a/device/Cargo.toml +++ /dev/null @@ -1,59 +0,0 @@ -[package] -name = "tek_device" -edition = { workspace = true } -version = { workspace = true } - -[lib] -path = "device.rs" - -[target.'cfg(target_os = "linux")'] -rustflags = ["-C", "link-arg=-fuse-ld=mold"] - -[dependencies] -tek_engine = { path = "../engine" } - -atomic_float = { workspace = true } -backtrace = { workspace = true } -clap = { workspace = true, optional = true } -jack = { workspace = true } -konst = { workspace = true } -livi = { workspace = true, optional = true } -midly = { workspace = true } -palette = { workspace = true } -rand = { workspace = true } -symphonia = { workspace = true, optional = true } -tengri = { workspace = true } -toml = { workspace = true } -uuid = { workspace = true, optional = true } -wavers = { workspace = true, optional = true } -winit = { workspace = true, optional = true } -xdg = { workspace = true } - -[dev-dependencies] -proptest = { workspace = true } -proptest-derive = { workspace = true } - -[features] -arranger = ["port", "editor", "sequencer", "editor", "track", "scene", "clip", "select"] -browse = [] -clap = [] -cli = ["dep:clap"] -clip = [] -clock = [] -default = ["cli", "arranger", "sampler", "track", "lv2"] -editor = [] -host = ["lv2"] -lv2 = ["port", "livi"] -lv2_gui = ["lv2", "winit"] -meter = [] -mixer = [] -pool = [] -port = [] -sampler = ["port", "meter", "mixer", "browse", "symphonia", "wavers"] -select = [] -scene = [] -sequencer = ["port", "clock", "uuid", "pool"] -sf2 = [] -track = [] -vst2 = [] -vst3 = [] diff --git a/device/arranger.rs b/device/arranger.rs deleted file mode 100644 index 10a624b1..00000000 --- a/device/arranger.rs +++ /dev/null @@ -1,626 +0,0 @@ -use crate::*; - -#[derive(Default, Debug)] pub struct Arrangement { - /// Project name. - pub name: Arc, - /// Base color. - pub color: ItemTheme, - /// JACK client handle. - pub jack: Jack<'static>, - /// FIXME a render of the project arrangement, redrawn on update. - /// TODO rename to "render_cache" or smth - pub arranger: Arc>, - /// Display size - pub size: Measure, - /// Display size of clips area - pub size_inner: Measure, - /// Source of time - #[cfg(feature = "clock")] pub clock: Clock, - /// Allows one MIDI clip to be edited - #[cfg(feature = "editor")] pub editor: Option, - /// List of global midi inputs - #[cfg(feature = "port")] pub midi_ins: Vec, - /// List of global midi outputs - #[cfg(feature = "port")] pub midi_outs: Vec, - /// List of global audio inputs - #[cfg(feature = "port")] pub audio_ins: Vec, - /// List of global audio outputs - #[cfg(feature = "port")] pub audio_outs: Vec, - /// Selected UI element - #[cfg(feature = "select")] pub selection: Selection, - /// Last track number (to avoid duplicate port names) - #[cfg(feature = "track")] pub track_last: usize, - /// List of tracks - #[cfg(feature = "track")] pub tracks: Vec, - /// Scroll offset of tracks - #[cfg(feature = "track")] pub track_scroll: usize, - /// List of scenes - #[cfg(feature = "scene")] pub scenes: Vec, - /// Scroll offset of scenes - #[cfg(feature = "scene")] pub scene_scroll: usize, -} -impl HasJack<'static> for Arrangement { - fn jack (&self) -> &Jack<'static> { - &self.jack - } -} -has!(Jack<'static>: |self: Arrangement|self.jack); -has!(Measure: |self: Arrangement|self.size); -#[cfg(feature = "editor")] has!(Option: |self: Arrangement|self.editor); -#[cfg(feature = "port")] has!(Vec: |self: Arrangement|self.midi_ins); -#[cfg(feature = "port")] has!(Vec: |self: Arrangement|self.midi_outs); -#[cfg(feature = "clock")] has!(Clock: |self: Arrangement|self.clock); -#[cfg(feature = "select")] has!(Selection: |self: Arrangement|self.selection); -#[cfg(all(feature = "select", feature = "track"))] has!(Vec: |self: Arrangement|self.tracks); -#[cfg(all(feature = "select", feature = "track"))] maybe_has!(Track: |self: Arrangement| - { Has::::get(self).track().map(|index|Has::>::get(self).get(index)).flatten() }; - { Has::::get(self).track().map(|index|Has::>::get_mut(self).get_mut(index)).flatten() }); -#[cfg(all(feature = "select", feature = "scene"))] has!(Vec: |self: Arrangement|self.scenes); -#[cfg(all(feature = "select", feature = "scene"))] maybe_has!(Scene: |self: Arrangement| - { Has::::get(self).track().map(|index|Has::>::get(self).get(index)).flatten() }; - { Has::::get(self).track().map(|index|Has::>::get_mut(self).get_mut(index)).flatten() }); - -#[cfg(feature = "select")] -impl Arrangement { - #[cfg(feature = "clip")] fn selected_clip (&self) -> Option { todo!() } - #[cfg(feature = "scene")] fn selected_scene (&self) -> Option { todo!() } - #[cfg(feature = "track")] fn selected_track (&self) -> Option { todo!() } - #[cfg(feature = "port")] fn selected_midi_in (&self) -> Option { todo!() } - #[cfg(feature = "port")] fn selected_midi_out (&self) -> Option { todo!() } - fn selected_device (&self) -> Option { todo!() } - fn unselect (&self) -> Selection { - Selection::Nothing - } -} - -impl Arrangement { - /// Width of display - pub fn w (&self) -> u16 { - self.size.w() as u16 - } - /// Width allocated for sidebar. - pub fn w_sidebar (&self, is_editing: bool) -> u16 { - self.w() / if is_editing { 16 } else { 8 } as u16 - } - /// Width available to display tracks. - pub fn w_tracks_area (&self, is_editing: bool) -> u16 { - self.w().saturating_sub(self.w_sidebar(is_editing)) - } - /// Height of display - pub fn h (&self) -> u16 { - self.size.h() as u16 - } - /// Height taken by visible device slots. - pub fn h_devices (&self) -> u16 { - 2 - //1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) - } -} - -#[cfg(feature = "track")] -impl TracksView for Arrangement {} - -#[cfg(feature = "track")] -impl Arrangement { - /// Get the active track - pub fn get_track (&self) -> Option<&Track> { - let index = self.selection().track()?; - Has::>::get(self).get(index) - } - /// Get a mutable reference to the active track - pub fn get_track_mut (&mut self) -> Option<&mut Track> { - let index = self.selection().track()?; - Has::>::get_mut(self).get_mut(index) - } - /// Add multiple tracks - pub fn tracks_add ( - &mut self, - count: usize, width: Option, - mins: &[Connect], mouts: &[Connect], - ) -> Usually<()> { - let track_color_1 = ItemColor::random(); - let track_color_2 = ItemColor::random(); - for i in 0..count { - let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into(); - let track = self.track_add(None, Some(color), mins, mouts)?.1; - if let Some(width) = width { - track.width = width; - } - } - Ok(()) - } - /// Add a track - pub fn track_add ( - &mut self, - name: Option<&str>, color: Option, - mins: &[Connect], mouts: &[Connect], - ) -> Usually<(usize, &mut Track)> { - let name: Arc = name.map_or_else( - ||format!("trk{:02}", self.track_last).into(), - |x|x.to_string().into() - ); - self.track_last += 1; - let track = Track { - width: (name.len() + 2).max(12), - color: color.unwrap_or_else(ItemTheme::random), - sequencer: Sequencer::new( - &format!("{name}"), - self.jack(), - Some(self.clock()), - None, - mins, - mouts - )?, - name, - ..Default::default() - }; - self.tracks_mut().push(track); - let len = self.tracks().len(); - let index = len - 1; - for scene in self.scenes_mut().iter_mut() { - while scene.clips.len() < len { - scene.clips.push(None); - } - } - Ok((index, &mut self.tracks_mut()[index])) - } - - pub fn view_inputs (&self, _theme: ItemTheme) -> impl Content + '_ { - Bsp::s( - Fixed::Y(1, self.view_inputs_header()), - Thunk::new(|to: &mut TuiOut|{ - for (index, port) in self.midi_ins().iter().enumerate() { - to.place(&Push::X(index as u16 * 10, Fixed::Y(1, self.view_inputs_row(port)))) - } - }) - ) - } - - fn view_inputs_header (&self) -> impl Content + '_ { - Bsp::e(Fixed::X(20, Align::w(button_3("i", "nput ", format!("{}", self.midi_ins.len()), false))), - Bsp::w(Fixed::X(4, button_2("I", "+", false)), Thunk::new(move|to: &mut TuiOut|for (_index, track, x1, _x2) in self.tracks_with_sizes() { - #[cfg(feature = "track")] - to.place(&Push::X(x1 as u16, Tui::bg(track.color.dark.rgb, Align::w(Fixed::X(track.width as u16, row!( - Either::new(track.sequencer.monitoring, Tui::fg(Green, "mon "), "mon "), - Either::new(track.sequencer.recording, Tui::fg(Red, "rec "), "rec "), - Either::new(track.sequencer.overdub, Tui::fg(Yellow, "dub "), "dub "), - )))))) - }))) - } - - fn view_inputs_row (&self, port: &MidiInput) -> impl Content { - Bsp::e(Fixed::X(20, Align::w(Bsp::e(" ● ", Tui::bold(true, Tui::fg(Rgb(255,255,255), port.port_name()))))), - Bsp::w(Fixed::X(4, ()), Thunk::new(move|to: &mut TuiOut|for (_index, track, _x1, _x2) in self.tracks_with_sizes() { - #[cfg(feature = "track")] - to.place(&Tui::bg(track.color.darker.rgb, Align::w(Fixed::X(track.width as u16, row!( - Either::new(track.sequencer.monitoring, Tui::fg(Green, " ● "), " · "), - Either::new(track.sequencer.recording, Tui::fg(Red, " ● "), " · "), - Either::new(track.sequencer.overdub, Tui::fg(Yellow, " ● "), " · "), - ))))) - }))) - } - - pub fn view_outputs (&self, theme: ItemTheme) -> impl Content { - let mut h = 1; - for output in self.midi_outs().iter() { - h += 1 + output.connections.len(); - } - let h = h as u16; - let list = Bsp::s( - Fixed::Y(1, Fill::X(Align::w(button_3("o", "utput", format!("{}", self.midi_outs.len()), false)))), - Fixed::Y(h - 1, Fill::XY(Align::nw(Thunk::new(|to: &mut TuiOut|{ - for (_index, port) in self.midi_outs().iter().enumerate() { - to.place(&Fixed::Y(1,Fill::X(Bsp::e( - Align::w(Bsp::e(" ● ", Tui::fg(Rgb(255,255,255),Tui::bold(true, port.port_name())))), - Fill::X(Align::e(format!("{}/{} ", - port.port().get_connections().len(), - port.connections.len()))))))); - for (index, conn) in port.connections.iter().enumerate() { - to.place(&Fixed::Y(1, Fill::X(Align::w(format!(" c{index:02}{}", conn.info()))))); - } - } - }))))); - Fixed::Y(h, view_track_row_section(theme, list, button_2("O", "+", false), - Tui::bg(theme.darker.rgb, Align::w(Fill::X( - Thunk::new(|to: &mut TuiOut|{ - for (index, track, _x1, _x2) in self.tracks_with_sizes() { - to.place(&Fixed::X(track_width(index, track), - Thunk::new(|to: &mut TuiOut|{ - to.place(&Fixed::Y(1, Align::w(Bsp::e( - Either::new(true, Tui::fg(Green, "play "), "play "), - Either::new(false, Tui::fg(Yellow, "solo "), "solo "), - )))); - for (_index, port) in self.midi_outs().iter().enumerate() { - to.place(&Fixed::Y(1, Align::w(Bsp::e( - Either::new(true, Tui::fg(Green, " ● "), " · "), - Either::new(false, Tui::fg(Yellow, " ● "), " · "), - )))); - for (_index, _conn) in port.connections.iter().enumerate() { - to.place(&Fixed::Y(1, Fill::X(""))); - } - }})))}})))))) - } - - pub fn view_track_devices (&self, theme: ItemTheme) -> impl Content { - let mut h = 2u16; - for track in self.tracks().iter() { - h = h.max(track.devices.len() as u16 * 2); - } - view_track_row_section(theme, - button_3("d", "evice", format!("{}", self.track().map(|t|t.devices.len()).unwrap_or(0)), false), - button_2("D", "+", false), - Thunk::new(move|to: &mut TuiOut|for (index, track, _x1, _x2) in self.tracks_with_sizes() { - to.place(&Fixed::XY(track_width(index, track), h + 1, - Tui::bg(track.color.dark.rgb, Align::nw(Map::south(2, move||0..h, - |_, _index|Fixed::XY(track.width as u16, 2, - Tui::fg_bg( - ItemTheme::G[32].lightest.rgb, - ItemTheme::G[32].dark.rgb, - Align::nw(format!(" · {}", "--"))))))))); - })) - } - -} - -#[cfg(feature = "track")] -pub fn view_track_row_section ( - _theme: ItemTheme, - button: impl Content, - button_add: impl Content, - content: impl Content, -) -> impl Content { - Bsp::w(Fill::Y(Fixed::X(4, Align::nw(button_add))), - Bsp::e(Fixed::X(20, Fill::Y(Align::nw(button))), Fill::XY(Align::c(content)))) -} - -#[cfg(feature = "scene")] -impl Arrangement { - /// Get the active scene - pub fn get_scene (&self) -> Option<&Scene> { - let index = self.selection().scene()?; - Has::>::get(self).get(index) - } - /// Get a mutable reference to the active scene - pub fn get_scene_mut (&mut self) -> Option<&mut Scene> { - let index = self.selection().scene()?; - Has::>::get_mut(self).get_mut(index) - } -} - -#[cfg(feature = "scene")] -impl ScenesView for Arrangement { - fn h_scenes (&self) -> u16 { - (self.height() as u16).saturating_sub(20) - } - fn w_side (&self) -> u16 { - (self.width() as u16 * 2 / 10).max(20) - } - fn w_mid (&self) -> u16 { - (self.width() as u16).saturating_sub(2 * self.w_side()).max(40) - } -} - -#[cfg(feature = "clip")] -impl Arrangement { - /// Get the active clip - pub fn get_clip (&self) -> Option>> { - self.get_scene()?.clips.get(self.selection().track()?)?.clone() - } - /// Put a clip in a slot - pub fn clip_put ( - &mut self, track: usize, scene: usize, clip: Option>> - ) -> Option>> { - let old = self.scenes[scene].clips[track].clone(); - self.scenes[scene].clips[track] = clip; - old - } - /// Change the color of a clip, returning the previous one - pub fn clip_set_color (&self, track: usize, scene: usize, color: ItemTheme) - -> Option - { - self.scenes[scene].clips[track].as_ref().map(|clip|{ - let mut clip = clip.write().unwrap(); - let old = clip.color.clone(); - clip.color = color.clone(); - panic!("{color:?} {old:?}"); - old - }) - } - /// Toggle looping for the active clip - pub fn toggle_loop (&mut self) { - if let Some(clip) = self.get_clip() { - clip.write().unwrap().toggle_loop() - } - } -} - -#[cfg(feature = "sampler")] -impl Arrangement { - /// Get the first sampler of the active track - pub fn sampler (&self) -> Option<&Sampler> { - self.get_track()?.sampler(0) - } - /// Get the first sampler of the active track - pub fn sampler_mut (&mut self) -> Option<&mut Sampler> { - self.get_track_mut()?.sampler_mut(0) - } -} - -pub(crate) fn wrap (bg: Color, fg: Color, content: impl Content) -> impl Content { - let left = Tui::fg_bg(bg, Reset, Fixed::X(1, RepeatV("▐"))); - let right = Tui::fg_bg(bg, Reset, Fixed::X(1, RepeatV("▌"))); - Bsp::e(left, Bsp::w(right, Tui::fg_bg(fg, bg, content))) -} - -pub trait HasClipsSize { - fn clips_size (&self) -> &Measure; -} - -impl HasClipsSize for Arrangement { - fn clips_size (&self) -> &Measure { &self.size_inner } -} - -pub trait HasWidth { - const MIN_WIDTH: usize; - /// Increment track width. - fn width_inc (&mut self); - /// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH]. - fn width_dec (&mut self); -} - -//def_command!(ArrangementCommand: |arranger: Arrangement| { - - //Home => { arranger.editor = None; Ok(None) }, - - //Edit => { - //let selection = arranger.selection().clone(); - //arranger.editor = if arranger.editor.is_some() { - //None - //} else { - //match selection { - //Selection::TrackClip { track, scene } => { - //let clip = &mut arranger.scenes_mut()[scene].clips[track]; - //if clip.is_none() { - ////app.clip_auto_create(); - //*clip = Some(Arc::new(RwLock::new(MidiClip::new( - //&format!("t{track:02}s{scene:02}"), - //false, 384, None, Some(ItemTheme::random()) - //)))); - //} - //clip.as_ref().map(|c|c.into()) - //} - //_ => { - //None - //} - //} - //}; - //if let Some(editor) = arranger.editor.as_mut() { - //if let Some(clip) = editor.clip() { - //let length = clip.read().unwrap().length.max(1); - //let width = arranger.size_inner.w().saturating_sub(20).max(1); - //editor.set_time_zoom(length / width); - //editor.redraw(); - //} - //} - //Ok(None) - //}, - - ////// Set the selection - //Select { selection: Selection } => { *arranger.selection_mut() = *selection; Ok(None) }, - - ////// Launch the selected clip or scene - //Launch => { - //match *arranger.selection() { - //Selection::Track(t) => { - //arranger.tracks[t].sequencer.enqueue_next(None) - //}, - //Selection::TrackClip { track, scene } => { - //arranger.tracks[track].sequencer.enqueue_next(arranger.scenes[scene].clips[track].as_ref()) - //}, - //Selection::Scene(s) => { - //for t in 0..arranger.tracks.len() { - //arranger.tracks[t].sequencer.enqueue_next(arranger.scenes[s].clips[t].as_ref()) - //} - //}, - //_ => {} - //}; - //Ok(None) - //}, - - ////// Set the color of the selected entity - //SetColor { palette: Option } => { - //let mut palette = palette.unwrap_or_else(||ItemTheme::random()); - //let selection = *arranger.selection(); - //Ok(Some(Self::SetColor { palette: Some(match selection { - //Selection::Mix => { - //std::mem::swap(&mut palette, &mut arranger.color); - //palette - //}, - //Selection::Scene(s) => { - //std::mem::swap(&mut palette, &mut arranger.scenes[s].color); - //palette - //} - //Selection::Track(t) => { - //std::mem::swap(&mut palette, &mut arranger.tracks[t].color); - //palette - //} - //Selection::TrackClip { track, scene } => { - //if let Some(ref clip) = arranger.scenes[scene].clips[track] { - //let mut clip = clip.write().unwrap(); - //std::mem::swap(&mut palette, &mut clip.color); - //palette - //} else { - //return Ok(None) - //} - //}, - //_ => todo!() - //}) })) - //}, - - //Track { track: TrackCommand } => { todo!("delegate") }, - - //TrackAdd => { - //let index = arranger.track_add(None, None, &[], &[])?.0; - //*arranger.selection_mut() = match arranger.selection() { - //Selection::Track(_) => Selection::Track(index), - //Selection::TrackClip { track: _, scene } => Selection::TrackClip { - //track: index, scene: *scene - //}, - //_ => *arranger.selection() - //}; - //Ok(Some(Self::TrackDelete { index })) - //}, - - //TrackSwap { index: usize, other: usize } => { - //let index = *index; - //let other = *other; - //Ok(Some(Self::TrackSwap { index, other })) - //}, - - //TrackDelete { index: usize } => { - //let index = *index; - //let exists = arranger.tracks().get(index).is_some(); - //if exists { - //let track = arranger.tracks_mut().remove(index); - //let Track { sequencer: Sequencer { midi_ins, midi_outs, .. }, .. } = track; - //for port in midi_ins.into_iter() { - //port.close()?; - //} - //for port in midi_outs.into_iter() { - //port.close()?; - //} - //for scene in arranger.scenes_mut().iter_mut() { - //scene.clips.remove(index); - //} - //} - //Ok(None) - ////TODO:Ok(Some(Self::TrackAdd ( index, track: Some(deleted_track) }) - //}, - - //MidiIn { input: MidiInputCommand } => { - //todo!("delegate"); Ok(None) - //}, - - //MidiInAdd => { - //arranger.midi_in_add()?; - //Ok(None) - //}, - - //MidiOut { output: MidiOutputCommand } => { - //todo!("delegate"); - //Ok(None) - //}, - - //MidiOutAdd => { - //arranger.midi_out_add()?; - //Ok(None) - //}, - - //Device { command: DeviceCommand } => { - //todo!("delegate"); - //Ok(None) - //}, - - //DeviceAdd { index: usize } => { - //todo!("delegate"); - //Ok(None) - //}, - - //Scene { scene: SceneCommand } => { - //todo!("delegate"); - //Ok(None) - //}, - - //OutputAdd => { - //arranger.midi_outs.push(MidiOutput::new( - //arranger.jack(), - //&format!("/M{}", arranger.midi_outs.len() + 1), - //&[] - //)?); - //Ok(None) - //}, - - //InputAdd => { - //arranger.midi_ins.push(MidiInput::new( - //arranger.jack(), - //&format!("M{}/", arranger.midi_ins.len() + 1), - //&[] - //)?); - //Ok(None) - //}, - - //SceneAdd => { - //let index = arranger.scene_add(None, None)?.0; - //*arranger.selection_mut() = match arranger.selection() { - //Selection::Scene(_) => Selection::Scene(index), - //Selection::TrackClip { track, scene } => Selection::TrackClip { - //track: *track, - //scene: index - //}, - //_ => *arranger.selection() - //}; - //Ok(None) // TODO - //}, - - //SceneSwap { index: usize, other: usize } => { - //let index = *index; - //let other = *other; - //Ok(Some(Self::SceneSwap { index, other })) - //}, - - //SceneDelete { index: usize } => { - //let index = *index; - //let scenes = arranger.scenes_mut(); - //Ok(if scenes.get(index).is_some() { - //let _scene = scenes.remove(index); - //None - //} else { - //None - //}) - //}, - - //SceneLaunch { index: usize } => { - //let index = *index; - //for track in 0..arranger.tracks.len() { - //let clip = arranger.scenes[index].clips[track].as_ref(); - //arranger.tracks[track].sequencer.enqueue_next(clip); - //} - //Ok(None) - //}, - - //Clip { scene: ClipCommand } => { - //todo!("delegate") - //}, - - //ClipGet { a: usize, b: usize } => { - ////(Get [a: usize, b: usize] cmd_todo!("\n\rtodo: clip: get: {a} {b}")) - ////("get" [a: usize, b: usize] Some(Self::Get(a.unwrap(), b.unwrap()))) - //todo!() - //}, - - //ClipPut { a: usize, b: usize } => { - ////(Put [t: usize, s: usize, c: MaybeClip] - ////Some(Self::Put(t, s, arranger.clip_put(t, s, c)))) - ////("put" [a: usize, b: usize, c: MaybeClip] Some(Self::Put(a.unwrap(), b.unwrap(), c.unwrap()))) - //todo!() - //}, - - //ClipDel { a: usize, b: usize } => { - ////("delete" [a: usize, b: usize] Some(Self::Put(a.unwrap(), b.unwrap(), None)))) - //todo!() - //}, - - //ClipEnqueue { a: usize, b: usize } => { - ////(Enqueue [t: usize, s: usize] - ////cmd!(arranger.tracks[t].sequencer.enqueue_next(arranger.scenes[s].clips[t].as_ref()))) - ////("enqueue" [a: usize, b: usize] Some(Self::Enqueue(a.unwrap(), b.unwrap()))) - //todo!() - //}, - - //ClipSwap { a: usize, b: usize }=> { - ////(Edit [clip: MaybeClip] cmd_todo!("\n\rtodo: clip: edit: {clip:?}")) - ////("edit" [a: MaybeClip] Some(Self::Edit(a.unwrap()))) - //todo!() - //}, - -//}); diff --git a/device/browse.rs b/device/browse.rs deleted file mode 100644 index 02d41448..00000000 --- a/device/browse.rs +++ /dev/null @@ -1,220 +0,0 @@ -use crate::*; -use std::path::PathBuf; -use std::ffi::OsString; - -#[derive(Clone, Debug)] -pub enum BrowseTarget { - SaveProject, - LoadProject, - ImportSample(Arc>>), - ExportSample(Arc>>), - ImportClip(Arc>>), - ExportClip(Arc>>), -} - -impl PartialEq for BrowseTarget { - fn eq (&self, other: &Self) -> bool { - match self { - Self::ImportSample(_) => false, - Self::ExportSample(_) => false, - Self::ImportClip(_) => false, - Self::ExportClip(_) => false, - t => matches!(other, t) - } - } -} - -/// Browses for phrase to import/export -#[derive(Debug, Clone, Default, PartialEq)] -pub struct Browse { - pub cwd: PathBuf, - pub dirs: Vec<(OsString, String)>, - pub files: Vec<(OsString, String)>, - pub filter: String, - pub index: usize, - pub scroll: usize, - pub size: Measure, -} - -impl Browse { - - pub fn new (cwd: Option) -> Usually { - let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? }; - let mut dirs = vec![]; - let mut files = vec![]; - for entry in std::fs::read_dir(&cwd)? { - let entry = entry?; - let name = entry.file_name(); - let decoded = name.clone().into_string().unwrap_or_else(|_|"".to_string()); - let meta = entry.metadata()?; - if meta.is_dir() { - dirs.push((name, format!("📁 {decoded}"))); - } else if meta.is_file() { - files.push((name, format!("📄 {decoded}"))); - } - } - Ok(Self { - cwd, - dirs, - files, - filter: "".to_string(), - index: 0, - scroll: 0, - size: Measure::new(), - }) - } - - pub fn len (&self) -> usize { - self.dirs.len() + self.files.len() - } - - pub fn is_dir (&self) -> bool { - self.index < self.dirs.len() - } - - pub fn is_file (&self) -> bool { - self.index >= self.dirs.len() - } - - pub fn path (&self) -> PathBuf { - self.cwd.join(if self.is_dir() { - &self.dirs[self.index].0 - } else if self.is_file() { - &self.files[self.index - self.dirs.len()].0 - } else { - unreachable!() - }) - } - - pub fn chdir (&self) -> Usually { - Self::new(Some(self.path())) - } - -} - -impl Browse { - fn _todo_stub_path_buf (&self) -> PathBuf { - todo!() - } - fn _todo_stub_usize (&self) -> usize { - todo!() - } - fn _todo_stub_arc_str (&self) -> Arc { - todo!() - } -} - -def_command!(BrowseCommand: |browse: Browse| { - SetVisible => Ok(None), - SetPath { address: PathBuf } => Ok(None), - SetSearch { filter: Arc } => Ok(None), - SetCursor { cursor: usize } => Ok(None), -}); - -impl HasContent for Browse { - fn content (&self) -> impl Content { - Map::south(1, ||EntriesIterator { - offset: 0, - index: 0, - length: self.dirs.len() + self.files.len(), - browser: self, - }, |entry, _index|Fill::X(Align::w(entry))) - } -} - -struct EntriesIterator<'a> { - browser: &'a Browse, - offset: usize, - length: usize, - index: usize, -} - -impl<'a> Iterator for EntriesIterator<'a> { - type Item = Modify<&'a str>; - fn next (&mut self) -> Option { - let dirs = self.browser.dirs.len(); - let files = self.browser.files.len(); - let index = self.index; - if self.index < dirs { - self.index += 1; - Some(Tui::bold(true, self.browser.dirs[index].1.as_str())) - } else if self.index < dirs + files { - self.index += 1; - Some(Tui::bold(false, self.browser.files[index - dirs].1.as_str())) - } else { - None - } - } -} - -// Commands supported by [Browse] -//#[derive(Debug, Clone, PartialEq)] -//pub enum BrowseCommand { - //Begin, - //Cancel, - //Confirm, - //Select(usize), - //Chdir(PathBuf), - //Filter(Arc), -//} - //fn begin (browse: &mut Browse) => { - //unreachable!(); - //} - //fn cancel (browse: &mut Browse) => { - //todo!() - ////browse.mode = None; - ////Ok(None) - //} - //fn confirm (browse: &mut Browse) => { - //todo!() - ////Ok(match browse.mode { - ////Some(PoolMode::Import(index, ref mut browse)) => { - ////if browse.is_file() { - ////let path = browse.path(); - ////browse.mode = None; - ////let _undo = PoolClipCommand::import(browse, index, path)?; - ////None - ////} else if browse.is_dir() { - ////browse.mode = Some(PoolMode::Import(index, browse.chdir()?)); - ////None - ////} else { - ////None - ////} - ////}, - ////Some(PoolMode::Export(index, ref mut browse)) => { - ////todo!() - ////}, - ////_ => unreachable!(), - ////}) - //} - //fn select (browse: &mut Browse, index: usize) => { - //todo!() - ////Ok(match browse.mode { - ////Some(PoolMode::Import(index, ref mut browse)) => { - ////browse.index = index; - ////None - ////}, - ////Some(PoolMode::Export(index, ref mut browse)) => { - ////browse.index = index; - ////None - ////}, - ////_ => unreachable!(), - ////}) - //} - //fn chdir (browse: &mut Browse, dir: PathBuf) => { - //todo!() - ////Ok(match browse.mode { - ////Some(PoolMode::Import(index, ref mut browse)) => { - ////browse.mode = Some(PoolMode::Import(index, Browse::new(Some(dir))?)); - ////None - ////}, - ////Some(PoolMode::Export(index, ref mut browse)) => { - ////browse.mode = Some(PoolMode::Export(index, Browse::new(Some(dir))?)); - ////None - ////}, - ////_ => unreachable!(), - ////}) - //} - //fn filter (browse: &mut Browse, filter: Arc) => { - //todo!() - //} diff --git a/device/clip.rs b/device/clip.rs deleted file mode 100644 index ac0ca95b..00000000 --- a/device/clip.rs +++ /dev/null @@ -1,215 +0,0 @@ -use crate::*; - -pub trait HasMidiClip { - fn clip (&self) -> Option>>; -} - -#[macro_export] macro_rules! has_clip { - (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? HasMidiClip for $Struct $(<$($L),*$($T),*>)? { - fn clip (&$self) -> Option>> { $cb } - } - } -} - -/// A MIDI sequence. -#[derive(Debug, Clone, Default)] -pub struct MidiClip { - pub uuid: uuid::Uuid, - /// Name of clip - pub name: Arc, - /// Temporal resolution in pulses per quarter note - pub ppq: usize, - /// Length of clip in pulses - pub length: usize, - /// Notes in clip - pub notes: MidiData, - /// Whether to loop the clip or play it once - pub looped: bool, - /// Start of loop - pub loop_start: usize, - /// Length of loop - pub loop_length: usize, - /// All notes are displayed with minimum length - pub percussive: bool, - /// Identifying color of clip - pub color: ItemTheme, -} - -/// MIDI message structural -pub type MidiData = Vec>; - -impl MidiClip { - pub fn new ( - name: impl AsRef, - looped: bool, - length: usize, - notes: Option, - color: Option, - ) -> Self { - Self { - uuid: uuid::Uuid::new_v4(), - name: name.as_ref().into(), - ppq: PPQ, - length, - notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]), - looped, - loop_start: 0, - loop_length: length, - percussive: true, - color: color.unwrap_or_else(ItemTheme::random) - } - } - pub fn count_midi_messages (&self) -> usize { - let mut count = 0; - for tick in self.notes.iter() { - count += tick.len(); - } - count - } - pub fn set_length (&mut self, length: usize) { - self.length = length; - self.notes = vec![Vec::with_capacity(16);length]; - } - pub fn duplicate (&self) -> Self { - let mut clone = self.clone(); - clone.uuid = uuid::Uuid::new_v4(); - clone - } - pub fn toggle_loop (&mut self) { self.looped = !self.looped; } - pub fn record_event (&mut self, pulse: usize, message: MidiMessage) { - if pulse >= self.length { panic!("extend clip first") } - self.notes[pulse].push(message); - } - /// Check if a range `start..end` contains MIDI Note On `k` - pub fn contains_note_on (&self, k: u7, start: usize, end: usize) -> bool { - for events in self.notes[start.max(0)..end.min(self.notes.len())].iter() { - for event in events.iter() { - if let MidiMessage::NoteOn {key,..} = event { if *key == k { return true } } - } - } - false - } - pub fn stop_all () -> Self { - Self::new( - "Stop", - false, - 1, - Some(vec![vec![MidiMessage::Controller { - controller: 123.into(), - value: 0.into() - }]]), - Some(ItemColor::from_rgb(Color::Rgb(32, 32, 32)).into()) - ) - } -} - -impl PartialEq for MidiClip { - fn eq (&self, other: &Self) -> bool { - self.uuid == other.uuid - } -} - -impl Eq for MidiClip {} - -impl MidiClip { - fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } - fn _todo_bool_stub_ (&self) -> bool { todo!() } - fn _todo_usize_stub_ (&self) -> usize { todo!() } - fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } - fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } - fn _todo_opt_item_theme_stub (&self) -> Option { todo!() } -} - -def_command!(ClipCommand: |clip: MidiClip| { - - SetColor { color: Option } => { - //(SetColor [t: usize, s: usize, c: ItemTheme] - //clip.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o))))); - //("color" [a: usize, b: usize] Some(Self::SetColor(a.unwrap(), b.unwrap(), ItemTheme::random()))) - todo!() - }, - - SetLoop { looping: Option } => { - //(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}")) - //("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap()))) - todo!() - } - -}); - -pub trait ClipsView: - TracksView + - ScenesView + - HasClipsSize + - Send + - Sync -{ - fn view_scenes_clips <'a> (&'a self) - -> impl Content + 'a - { - self.clips_size().of(Fill::XY(Bsp::a( - Fill::XY(Align::se(Tui::fg(Green, format!("{}x{}", self.clips_size().w(), self.clips_size().h())))), - Thunk::new(|to: &mut TuiOut|for ( - track_index, track, _, _ - ) in self.tracks_with_sizes() { - to.place(&Fixed::X(track.width as u16, - Fill::Y(self.view_track_clips(track_index, track)))) - })))) - } - - fn view_track_clips <'a> (&'a self, track_index: usize, track: &'a Track) -> impl Content + 'a { - Thunk::new(move|to: &mut TuiOut|for ( - scene_index, scene, .. - ) in self.scenes_with_sizes() { - let (name, theme): (Arc, ItemTheme) = if let Some(Some(clip)) = &scene.clips.get(track_index) { - let clip = clip.read().unwrap(); - (format!(" ⏹ {}", &clip.name).into(), clip.color) - } else { - (" ⏹ -- ".into(), ItemTheme::G[32]) - }; - let fg = theme.lightest.rgb; - let mut outline = theme.base.rgb; - let bg = if self.selection().track() == Some(track_index) - && self.selection().scene() == Some(scene_index) - { - outline = theme.lighter.rgb; - theme.light.rgb - } else if self.selection().track() == Some(track_index) - || self.selection().scene() == Some(scene_index) - { - outline = theme.darkest.rgb; - theme.base.rgb - } else { - theme.dark.rgb - }; - let w = if self.selection().track() == Some(track_index) - && let Some(editor) = self.editor () - { - editor.width().max(24).max(track.width) - } else { - track.width - } as u16; - let y = if self.selection().scene() == Some(scene_index) - && let Some(editor) = self.editor () - { - editor.height().max(12) - } else { - Self::H_SCENE - } as u16; - - to.place(&Fixed::XY(w, y, Bsp::b( - Fill::XY(Outer(true, Style::default().fg(outline))), - Fill::XY(Bsp::b( - Bsp::b( - Tui::fg_bg(outline, bg, Fill::XY("")), - Fill::XY(Align::nw(Tui::fg_bg(fg, bg, Tui::bold(true, name)))), - ), - Fill::XY(When::new(self.selection().track() == Some(track_index) - && self.selection().scene() == Some(scene_index) - && self.is_editing(), self.editor()))))))); - }) - } -} -//take!(ClipCommand |state: Arrangement, iter|state.selected_clip().as_ref() - //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); diff --git a/device/clock.rs b/device/clock.rs deleted file mode 100644 index 17ab9d36..00000000 --- a/device/clock.rs +++ /dev/null @@ -1,421 +0,0 @@ -use crate::*; -use std::fmt::Write; - -pub trait HasClock: Send + Sync { - fn clock (&self) -> &Clock; - fn clock_mut (&mut self) -> &mut Clock; -} - -impl> HasClock for T { - fn clock (&self) -> &Clock { self.get() } - fn clock_mut (&mut self) -> &mut Clock { self.get_mut() } -} - -#[derive(Clone, Default)] -pub struct Clock { - /// JACK transport handle. - pub transport: Arc>, - /// Global temporal resolution (shared by [Moment] fields) - pub timebase: Arc, - /// Current global sample and usec (monotonic from JACK clock) - pub global: Arc, - /// Global sample and usec at which playback started - pub started: Arc>>, - /// Playback offset (when playing not from start) - pub offset: Arc, - /// Current playhead position - pub playhead: Arc, - /// Note quantization factor - pub quant: Arc, - /// Launch quantization factor - pub sync: Arc, - /// Size of buffer in samples - pub chunk: Arc, - // Cache of formatted strings - pub view_cache: Arc>, - /// For syncing the clock to an external source - #[cfg(feature = "port")] pub midi_in: Arc>>, - /// For syncing other devices to this clock - #[cfg(feature = "port")] pub midi_out: Arc>>, - /// For emitting a metronome - #[cfg(feature = "port")] pub click_out: Arc>>, -} - -impl std::fmt::Debug for Clock { - fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("Clock") - .field("timebase", &self.timebase) - .field("chunk", &self.chunk) - .field("quant", &self.quant) - .field("sync", &self.sync) - .field("global", &self.global) - .field("playhead", &self.playhead) - .field("started", &self.started) - .finish() - } -} - -impl Clock { - pub fn new (jack: &Jack<'static>, bpm: Option) -> Usually { - let (chunk, transport) = jack.with_client(|c|(c.buffer_size(), c.transport())); - let timebase = Arc::new(Timebase::default()); - let clock = Self { - quant: Arc::new(24.into()), - sync: Arc::new(384.into()), - transport: Arc::new(Some(transport)), - chunk: Arc::new((chunk as usize).into()), - global: Arc::new(Moment::zero(&timebase)), - playhead: Arc::new(Moment::zero(&timebase)), - offset: Arc::new(Moment::zero(&timebase)), - started: RwLock::new(None).into(), - timebase, - midi_in: Arc::new(RwLock::new(Some(MidiInput::new(jack, &"M/clock", &[])?))), - midi_out: Arc::new(RwLock::new(Some(MidiOutput::new(jack, &"clock/M", &[])?))), - click_out: Arc::new(RwLock::new(Some(AudioOutput::new(jack, &"click", &[])?))), - ..Default::default() - }; - if let Some(bpm) = bpm { - clock.timebase.bpm.set(bpm); - } - Ok(clock) - } - pub fn timebase (&self) -> &Arc { - &self.timebase - } - /// Current sample rate - pub fn sr (&self) -> &SampleRate { - &self.timebase.sr - } - /// Current tempo - pub fn bpm (&self) -> &BeatsPerMinute { - &self.timebase.bpm - } - /// Current MIDI resolution - pub fn ppq (&self) -> &PulsesPerQuaver { - &self.timebase.ppq - } - /// Next pulse that matches launch sync (for phrase switchover) - pub fn next_launch_pulse (&self) -> usize { - let sync = self.sync.get() as usize; - let pulse = self.playhead.pulse.get() as usize; - if pulse % sync == 0 { - pulse - } else { - (pulse / sync + 1) * sync - } - } - /// Start playing, optionally seeking to a given location beforehand - pub fn play_from (&self, start: Option) -> Usually<()> { - if let Some(transport) = self.transport.as_ref() { - if let Some(start) = start { - transport.locate(start)?; - } - transport.start()?; - } - Ok(()) - } - /// Pause, optionally seeking to a given location afterwards - pub fn pause_at (&self, pause: Option) -> Usually<()> { - if let Some(transport) = self.transport.as_ref() { - transport.stop()?; - if let Some(pause) = pause { - transport.locate(pause)?; - } - } - Ok(()) - } - /// Is currently paused? - pub fn is_stopped (&self) -> bool { - self.started.read().unwrap().is_none() - } - /// Is currently playing? - pub fn is_rolling (&self) -> bool { - self.started.read().unwrap().is_some() - } - /// Update chunk size - pub fn set_chunk (&self, n_frames: usize) { - self.chunk.store(n_frames, Relaxed); - } - pub fn update_from_scope (&self, scope: &ProcessScope) -> Usually<()> { - // Store buffer length - self.set_chunk(scope.n_frames() as usize); - - // Store reported global frame and usec - let CycleTimes { current_frames, current_usecs, .. } = scope.cycle_times()?; - self.global.sample.set(current_frames as f64); - self.global.usec.set(current_usecs as f64); - - let mut started = self.started.write().unwrap(); - - // If transport has just started or just stopped, - // update starting point: - if let Some(transport) = self.transport.as_ref() { - match (transport.query_state()?, started.as_ref()) { - (TransportState::Rolling, None) => { - let moment = Moment::zero(&self.timebase); - moment.sample.set(current_frames as f64); - moment.usec.set(current_usecs as f64); - *started = Some(moment); - }, - (TransportState::Stopped, Some(_)) => { - *started = None; - }, - _ => {} - }; - } - - self.playhead.update_from_sample(started.as_ref() - .map(|started|current_frames as f64 - started.sample.get()) - .unwrap_or(0.)); - - Ok(()) - } - - pub fn bbt (&self) -> PositionBBT { - let pulse = self.playhead.pulse.get() as i32; - let ppq = self.timebase.ppq.get() as i32; - let bpm = self.timebase.bpm.get(); - let bar = (pulse / ppq) / 4; - PositionBBT { - bar: 1 + bar, - beat: 1 + (pulse / ppq) % 4, - tick: (pulse % ppq), - bar_start_tick: (bar * 4 * ppq) as f64, - beat_type: 4., - beats_per_bar: 4., - beats_per_minute: bpm, - ticks_per_beat: ppq as f64 - } - } - - pub fn next_launch_instant (&self) -> Moment { - Moment::from_pulse(self.timebase(), self.next_launch_pulse() as f64) - } - - /// Get index of first sample to populate. - /// - /// Greater than 0 means that the first pulse of the clip - /// falls somewhere in the middle of the chunk. - pub fn get_sample_offset (&self, scope: &ProcessScope, started: &Moment) -> usize{ - (scope.last_frame_time() as usize).saturating_sub( - started.sample.get() as usize + - self.started.read().unwrap().as_ref().unwrap().sample.get() as usize - ) - } - - // Get iterator that emits sample paired with pulse. - // - // * Sample: index into output buffer at which to write MIDI event - // * Pulse: index into clip from which to take the MIDI event - // - // Emitted for each sample of the output buffer that corresponds to a MIDI pulse. - pub fn get_pulses (&self, scope: &ProcessScope, offset: usize) -> TicksIterator { - self.timebase().pulses_between_samples(offset, offset + scope.n_frames() as usize) - } -} - -impl Clock { - fn _todo_provide_u32 (&self) -> u32 { - todo!() - } - fn _todo_provide_opt_u32 (&self) -> Option { - todo!() - } - fn _todo_provide_f64 (&self) -> f64 { - todo!() - } -} - -impl Command for ClockCommand { - fn execute (&self, state: &mut T) -> Perhaps { - self.execute(state.clock_mut()) // awesome - } -} - -def_command!(ClockCommand: |clock: Clock| { - SeekUsec { usec: f64 } => { - clock.playhead.update_from_usec(*usec); Ok(None) }, - SeekSample { sample: f64 } => { - clock.playhead.update_from_sample(*sample); Ok(None) }, - SeekPulse { pulse: f64 } => { - clock.playhead.update_from_pulse(*pulse); Ok(None) }, - SetBpm { bpm: f64 } => Ok(Some( - Self::SetBpm { bpm: clock.timebase().bpm.set(*bpm) })), - SetQuant { quant: f64 } => Ok(Some( - Self::SetQuant { quant: clock.quant.set(*quant) })), - SetSync { sync: f64 } => Ok(Some( - Self::SetSync { sync: clock.sync.set(*sync) })), - - Play { position: Option } => { - clock.play_from(*position)?; Ok(None) /* TODO Some(Pause(previousPosition)) */ }, - Pause { position: Option } => { - clock.pause_at(*position)?; Ok(None) }, - - TogglePlayback { position: u32 } => Ok(if clock.is_rolling() { - clock.pause_at(Some(*position))?; None - } else { - clock.play_from(Some(*position))?; None - }), -}); - -pub fn view_transport ( - play: bool, - bpm: Arc>, - beat: Arc>, - time: Arc>, -) -> impl Content { - let theme = ItemTheme::G[96]; - Tui::bg(Black, row!(Bsp::a( - Fill::XY(Align::w(button_play_pause(play))), - Fill::XY(Align::e(row!( - FieldH(theme, "BPM", bpm), - FieldH(theme, "Beat", beat), - FieldH(theme, "Time", time), - ))) - ))) -} - -pub fn view_status ( - sel: Option>, - sr: Arc>, - buf: Arc>, - lat: Arc>, -) -> impl Content { - let theme = ItemTheme::G[96]; - Tui::bg(Black, row!(Bsp::a( - Fill::XY(Align::w(sel.map(|sel|FieldH(theme, "Selected", sel)))), - Fill::XY(Align::e(row!( - FieldH(theme, "SR", sr), - FieldH(theme, "Buf", buf), - FieldH(theme, "Lat", lat), - ))) - ))) -} - -pub(crate) fn button_play_pause (playing: bool) -> impl Content { - let compact = true;//self.is_editing(); - Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, - Either::new(compact, - Thunk::new(move|to: &mut TuiOut|to.place(&Fixed::X(9, Either::new(playing, - Tui::fg(Rgb(0, 255, 0), " PLAYING "), - Tui::fg(Rgb(255, 128, 0), " STOPPED "))) - )), - Thunk::new(move|to: &mut TuiOut|to.place(&Fixed::X(5, Either::new(playing, - Tui::fg(Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), - Tui::fg(Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",)))) - )) - ) - ) -} - -#[derive(Debug)] pub struct ViewCache { - pub sr: Memo, String>, - pub buf: Memo, String>, - pub lat: Memo, String>, - pub bpm: Memo, String>, - pub beat: Memo, String>, - pub time: Memo, String>, -} - -impl Default for ViewCache { - fn default () -> Self { - let mut beat = String::with_capacity(16); - let _ = write!(beat, "{}", Self::BEAT_EMPTY); - let mut time = String::with_capacity(16); - let _ = write!(time, "{}", Self::TIME_EMPTY); - let mut bpm = String::with_capacity(16); - let _ = write!(bpm, "{}", Self::BPM_EMPTY); - Self { - beat: Memo::new(None, beat), - time: Memo::new(None, time), - bpm: Memo::new(None, bpm), - sr: Memo::new(None, String::with_capacity(16)), - buf: Memo::new(None, String::with_capacity(16)), - lat: Memo::new(None, String::with_capacity(16)), - } - } -} - -impl ViewCache { - pub const BEAT_EMPTY: &'static str = "-.-.--"; - pub const TIME_EMPTY: &'static str = "-.---s"; - pub const BPM_EMPTY: &'static str = "---.---"; - - //pub fn track_counter (cache: &Arc>, track: usize, tracks: usize) - //-> Arc> - //{ - //let data = (track, tracks); - //cache.write().unwrap().trks.update(Some(data), rewrite!(buf, "{}/{}", data.0, data.1)); - //cache.read().unwrap().trks.view.clone() - //} - - //pub fn scene_add (cache: &Arc>, scene: usize, scenes: usize, is_editing: bool) - //-> impl Content - //{ - //let data = (scene, scenes); - //cache.write().unwrap().scns.update(Some(data), rewrite!(buf, "({}/{})", data.0, data.1)); - //button_3("S", "add scene", cache.read().unwrap().scns.view.clone(), is_editing) - //} - - pub fn update_clock (cache: &Arc>, clock: &Clock, compact: bool) { - let rate = clock.timebase.sr.get(); - let chunk = clock.chunk.load(Relaxed) as f64; - let lat = chunk / rate * 1000.; - let delta = |start: &Moment|clock.global.usec.get() - start.usec.get(); - let mut cache = cache.write().unwrap(); - cache.buf.update(Some(chunk), rewrite!(buf, "{chunk}")); - cache.lat.update(Some(lat), rewrite!(buf, "{lat:.1}ms")); - cache.sr.update(Some((compact, rate)), |buf,_,_|{ - buf.clear(); - if compact { - write!(buf, "{:.1}kHz", rate / 1000.) - } else { - write!(buf, "{:.0}Hz", rate) - } - }); - if let Some(now) = clock.started.read().unwrap().as_ref().map(delta) { - let pulse = clock.timebase.usecs_to_pulse(now); - let time = now/1000000.; - let bpm = clock.timebase.bpm.get(); - cache.beat.update(Some(pulse), |buf, _, _|{ - buf.clear(); - clock.timebase.format_beats_1_to(buf, pulse) - }); - cache.time.update(Some(time), rewrite!(buf, "{:.3}s", time)); - cache.bpm.update(Some(bpm), rewrite!(buf, "{:.3}", bpm)); - } else { - cache.beat.update(None, rewrite!(buf, "{}", ViewCache::BEAT_EMPTY)); - cache.time.update(None, rewrite!(buf, "{}", ViewCache::TIME_EMPTY)); - cache.bpm.update(None, rewrite!(buf, "{}", ViewCache::BPM_EMPTY)); - } - } - - //pub fn view_h2 (&self) -> impl Content { - //let cache = self.project.clock.view_cache.clone(); - //let cache = cache.read().unwrap(); - //add(&Fixed::x(15, Align::w(Bsp::s( - //FieldH(theme, "Beat", cache.beat.view.clone()), - //FieldH(theme, "Time", cache.time.view.clone()), - //)))); - //add(&Fixed::x(13, Align::w(Bsp::s( - //Fill::X(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))), - //Fill::X(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))), - //)))); - //add(&Fixed::x(12, Align::w(Bsp::s( - //Fill::X(Align::w(FieldH(theme, "Buf", cache.buf.view.clone()))), - //Fill::X(Align::w(FieldH(theme, "Lat", cache.lat.view.clone()))), - //)))); - //add(&Bsp::s( - //Fill::X(Align::w(FieldH(theme, "Selected", Align::w(self.selection().describe( - //self.tracks(), - //self.scenes() - //))))), - //Fill::X(Align::w(FieldH(theme, format!("History ({})", self.history.len()), - //self.history.last().map(|last|Fill::X(Align::w(format!("{:?}", last.0))))))) - //)); - ////if let Some(last) = self.history.last() { - ////add(&FieldV(theme, format!("History ({})", self.history.len()), - ////Fill::X(Align::w(format!("{:?}", last.0))))); - ////} - //} -} diff --git a/device/device.rs b/device/device.rs deleted file mode 100644 index 0c808131..00000000 --- a/device/device.rs +++ /dev/null @@ -1,186 +0,0 @@ -#![feature(trait_alias)] - -pub use tek_engine; - -pub(crate) use ::{ - tek_engine::*, - tek_engine::tengri::{ - Usually, Perhaps, Has, MaybeHas, has, maybe_has, from, - input::*, - output::*, - tui::*, - tui::ratatui, - tui::ratatui::widgets::{Widget, canvas::{Canvas, Line}}, - tui::ratatui::prelude::{Rect, Style, Stylize, Buffer, Color::{self, *}}, - }, - std::{ - cmp::Ord, - ffi::OsString, - fmt::{Debug, Formatter}, - fs::File, - path::PathBuf, - sync::{Arc, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}}, - } -}; - -#[cfg(feature = "sampler")] pub(crate) use symphonia::{ - core::{ - formats::Packet, - codecs::{Decoder, CODEC_TYPE_NULL}, - //errors::Error as SymphoniaError, - io::MediaSourceStream, - probe::Hint, - audio::SampleBuffer, - }, - default::get_codecs, -}; - -#[cfg(feature = "lv2")] use std::thread::{spawn, JoinHandle}; - -#[cfg(feature = "lv2_gui")] use ::winit::{ - application::ApplicationHandler, - event::WindowEvent, - event_loop::{ActiveEventLoop, ControlFlow, EventLoop}, - window::{Window, WindowId}, - platform::x11::EventLoopBuilderExtX11 -}; - -/// Define a type alias for iterators of sized items (columns). -macro_rules! def_sizes_iter { - ($Type:ident => $($Item:ty),+) => { - pub trait $Type<'a> = - Iterator + Send + Sync + 'a; - } -} - -#[cfg(feature = "arranger")] mod arranger; #[cfg(feature = "arranger")] pub use self::arranger::*; -#[cfg(feature = "browse")] mod browse; #[cfg(feature = "browse")] pub use self::browse::*; -#[cfg(feature = "clap")] mod clap; #[cfg(feature = "clap")] pub use self::clap::*; -#[cfg(feature = "clip")] mod clip; #[cfg(feature = "clip")] pub use self::clip::*; -#[cfg(feature = "clock")] mod clock; #[cfg(feature = "clock")] pub use self::clock::*; -#[cfg(feature = "editor")] mod editor; #[cfg(feature = "editor")] pub use self::editor::*; -#[cfg(feature = "lv2")] mod lv2; #[cfg(feature = "lv2")] pub use self::lv2::*; -#[cfg(feature = "meter")] mod meter; #[cfg(feature = "meter")] pub use self::meter::*; -#[cfg(feature = "mixer")] mod mixer; #[cfg(feature = "mixer")] pub use self::mixer::*; -#[cfg(feature = "pool")] mod pool; #[cfg(feature = "pool")] pub use self::pool::*; -#[cfg(feature = "port")] mod port; #[cfg(feature = "port")] pub use self::port::*; -#[cfg(feature = "sampler")] mod sampler; #[cfg(feature = "sampler")] pub use self::sampler::*; -#[cfg(feature = "scene")] mod scene; #[cfg(feature = "scene")] pub use self::scene::*; -#[cfg(feature = "select")] mod select; #[cfg(feature = "select")] pub use self::select::*; -#[cfg(feature = "sequencer")] mod sequencer; #[cfg(feature = "sequencer")] pub use self::sequencer::*; -#[cfg(feature = "sf2")] mod sf2; #[cfg(feature = "sf2")] pub use self::sf2::*; -#[cfg(feature = "track")] mod track; #[cfg(feature = "track")] pub use self::track::*; -#[cfg(feature = "vst2")] mod vst2; #[cfg(feature = "vst2")] pub use self::vst2::*; -#[cfg(feature = "vst3")] mod vst3; #[cfg(feature = "vst3")] pub use self::vst3::*; - -pub fn swap_value ( - target: &mut T, value: &T, returned: impl Fn(T)->U -) -> Perhaps { - if *target == *value { - Ok(None) - } else { - let mut value = value.clone(); - std::mem::swap(target, &mut value); - Ok(Some(returned(value))) - } -} - -pub fn toggle_bool ( - target: &mut bool, value: &Option, returned: impl Fn(Option)->U -) -> Perhaps { - let mut value = value.unwrap_or(!*target); - if value == *target { - Ok(None) - } else { - std::mem::swap(target, &mut value); - Ok(Some(returned(Some(value)))) - } -} - -pub fn device_kinds () -> &'static [&'static str] { - &[ - #[cfg(feature = "sampler")] "Sampler", - #[cfg(feature = "lv2")] "Plugin (LV2)", - ] -} - -impl>> HasDevices for T { - fn devices (&self) -> &Vec { - self.get() - } - fn devices_mut (&mut self) -> &mut Vec { - self.get_mut() - } -} - -pub trait HasDevices { - fn devices (&self) -> &Vec; - fn devices_mut (&mut self) -> &mut Vec; -} - -#[derive(Debug)] -pub enum Device { - #[cfg(feature = "sampler")] - Sampler(Sampler), - #[cfg(feature = "lv2")] // TODO - Lv2(Lv2), - #[cfg(feature = "vst2")] // TODO - Vst2, - #[cfg(feature = "vst3")] // TODO - Vst3, - #[cfg(feature = "clap")] // TODO - Clap, - #[cfg(feature = "sf2")] // TODO - Sf2, -} - -impl Device { - pub fn name (&self) -> &str { - match self { - Self::Sampler(sampler) => sampler.name.as_ref(), - _ => todo!(), - } - } - pub fn midi_ins (&self) -> &[MidiInput] { - match self { - //Self::Sampler(Sampler { midi_in, .. }) => &[midi_in], - _ => todo!() - } - } - pub fn midi_outs (&self) -> &[MidiOutput] { - match self { - Self::Sampler(_) => &[], - _ => todo!() - } - } - pub fn audio_ins (&self) -> &[AudioInput] { - match self { - Self::Sampler(Sampler { audio_ins, .. }) => audio_ins.as_slice(), - _ => todo!() - } - } - pub fn audio_outs (&self) -> &[AudioOutput] { - match self { - Self::Sampler(Sampler { audio_outs, .. }) => audio_outs.as_slice(), - _ => todo!() - } - } -} - -pub struct DeviceAudio<'a>(pub &'a mut Device); - -audio!(|self: DeviceAudio<'a>, client, scope|{ - use Device::*; - match self.0 { - #[cfg(feature = "sampler")] Sampler(sampler) => sampler.process(client, scope), - #[cfg(feature = "lv2")] Lv2(lv2) => lv2.process(client, scope), - #[cfg(feature = "vst2")] Vst2 => { todo!() }, // TODO - #[cfg(feature = "vst3")] Vst3 => { todo!() }, // TODO - #[cfg(feature = "clap")] Clap => { todo!() }, // TODO - #[cfg(feature = "sf2")] Sf2 => { todo!() }, // TODO - } -}); - -def_command!(DeviceCommand: |device: Device| {}); -//take!(DeviceCommand|state: Arrangement, iter|state.selected_device().as_ref() - //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); diff --git a/device/editor.rs b/device/editor.rs deleted file mode 100644 index 2ec4d37e..00000000 --- a/device/editor.rs +++ /dev/null @@ -1,550 +0,0 @@ -use crate::*; - -#[macro_export] macro_rules! has_editor { - (|$self:ident: $Struct:ident|{ - editor = $e0:expr; - editor_w = $e1:expr; - editor_h = $e2:expr; - is_editing = $e3:expr; - }) => { - impl HasEditor for $Struct { - fn editor (&$self) -> Option<&MidiEditor> { $e0.as_ref() } - fn editor_mut (&mut $self) -> Option<&mut MidiEditor> { $e0.as_mut() } - fn editor_w (&$self) -> usize { $e1 } - fn editor_h (&$self) -> usize { $e2 } - fn is_editing (&$self) -> bool { $e3 } - } - }; -} -impl>> HasEditor for T {} -pub trait HasEditor: Has> { - fn editor (&self) -> Option<&MidiEditor> { self.get().as_ref() } - fn editor_mut (&mut self) -> Option<&mut MidiEditor> { self.get_mut().as_mut() } - fn is_editing (&self) -> bool { self.editor().is_some() } - fn editor_w (&self) -> usize { self.editor().map(|e|e.size.w()).unwrap_or(0) } - fn editor_h (&self) -> usize { self.editor().map(|e|e.size.h()).unwrap_or(0) } -} - -/// Contains state for viewing and editing a clip -pub struct MidiEditor { - /// Size of editor on screen - pub size: Measure, - /// View mode and state of editor - pub mode: PianoHorizontal, -} - -has!(Measure: |self: MidiEditor|self.size); -impl Default for MidiEditor { fn default () -> Self { Self { size: Measure::new(), mode: PianoHorizontal::new(None), } } } -impl std::fmt::Debug for MidiEditor { - fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("MidiEditor").field("mode", &self.mode).finish() - } -} -from!(|clip: &Arc>|MidiEditor = { let model = Self::from(Some(clip.clone())); model.redraw(); model }); -from!(|clip: Option>>|MidiEditor = { - let mut model = Self::default(); - *model.clip_mut() = clip; - model.redraw(); - model -}); - -impl MidiEditor { - /// Put note at current position - pub fn put_note (&mut self, advance: bool) { - let mut redraw = false; - if let Some(clip) = self.clip() { - let mut clip = clip.write().unwrap(); - let note_start = self.get_time_pos(); - let note_pos = self.get_note_pos(); - let note_len = self.get_note_len(); - let note_end = note_start + (note_len.saturating_sub(1)); - let key: u7 = u7::from(note_pos as u8); - let vel: u7 = 100.into(); - let length = clip.length; - let note_end = note_end % length; - let note_on = MidiMessage::NoteOn { key, vel }; - if !clip.notes[note_start].iter().any(|msg|*msg == note_on) { - clip.notes[note_start].push(note_on); - } - let note_off = MidiMessage::NoteOff { key, vel }; - if !clip.notes[note_end].iter().any(|msg|*msg == note_off) { - clip.notes[note_end].push(note_off); - } - if advance { - self.set_time_pos((note_end + 1) % clip.length); - } - redraw = true; - } - if redraw { - self.mode.redraw(); - } - } -} -impl TimeRange for MidiEditor { - fn time_len (&self) -> &AtomicUsize { self.mode.time_len() } - fn time_zoom (&self) -> &AtomicUsize { self.mode.time_zoom() } - fn time_lock (&self) -> &AtomicBool { self.mode.time_lock() } - fn time_start (&self) -> &AtomicUsize { self.mode.time_start() } - fn time_axis (&self) -> &AtomicUsize { self.mode.time_axis() } -} -impl NoteRange for MidiEditor { - fn note_lo (&self) -> &AtomicUsize { self.mode.note_lo() } - fn note_axis (&self) -> &AtomicUsize { self.mode.note_axis() } -} -impl NotePoint for MidiEditor { - fn note_len (&self) -> &AtomicUsize { self.mode.note_len() } - fn note_pos (&self) -> &AtomicUsize { self.mode.note_pos() } -} -impl TimePoint for MidiEditor { - fn time_pos (&self) -> &AtomicUsize { self.mode.time_pos() } -} -impl MidiViewer for MidiEditor { - fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { self.mode.buffer_size(clip) } - fn redraw (&self) { self.mode.redraw() } - fn clip (&self) -> &Option>> { self.mode.clip() } - fn clip_mut (&mut self) -> &mut Option>> { self.mode.clip_mut() } - fn set_clip (&mut self, p: Option<&Arc>>) { self.mode.set_clip(p) } -} - -def_command!(MidiEditCommand: |editor: MidiEditor| { - Show { clip: Option>> } => { - editor.set_clip(clip.as_ref()); editor.redraw(); Ok(None) }, - DeleteNote => { - editor.redraw(); todo!() }, - AppendNote { advance: bool } => { - editor.put_note(*advance); editor.redraw(); Ok(None) }, - SetNotePos { pos: usize } => { - editor.set_note_pos((*pos).min(127)); editor.redraw(); Ok(None) }, - SetNoteLen { len: usize } => { - editor.set_note_len(*len); editor.redraw(); Ok(None) }, - SetNoteScroll { scroll: usize } => { - editor.set_note_lo((*scroll).min(127)); editor.redraw(); Ok(None) }, - SetTimePos { pos: usize } => { - editor.set_time_pos(*pos); editor.redraw(); Ok(None) }, - SetTimeScroll { scroll: usize } => { - editor.set_time_start(*scroll); editor.redraw(); Ok(None) }, - SetTimeZoom { zoom: usize } => { - editor.set_time_zoom(*zoom); editor.redraw(); Ok(None) }, - SetTimeLock { lock: bool } => { - editor.set_time_lock(*lock); editor.redraw(); Ok(None) }, - // TODO: 1-9 seek markers that by default start every 8th of the clip -}); - -impl MidiEditor { - fn _todo_opt_clip_stub (&self) -> Option>> { todo!() } - fn clip_length (&self) -> usize { self.clip().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1) } - fn note_length (&self) -> usize { self.get_note_len() } - fn note_pos (&self) -> usize { self.get_note_pos() } - fn note_pos_next (&self) -> usize { self.get_note_pos() + 1 } - fn note_pos_next_octave (&self) -> usize { self.get_note_pos() + 12 } - fn note_pos_prev (&self) -> usize { self.get_note_pos().saturating_sub(1) } - fn note_pos_prev_octave (&self) -> usize { self.get_note_pos().saturating_sub(12) } - fn note_len (&self) -> usize { self.get_note_len() } - fn note_len_next (&self) -> usize { self.get_note_len() + 1 } - fn note_len_prev (&self) -> usize { self.get_note_len().saturating_sub(1) } - fn note_range (&self) -> usize { self.get_note_axis() } - fn note_range_next (&self) -> usize { self.get_note_axis() + 1 } - fn note_range_prev (&self) -> usize { self.get_note_axis().saturating_sub(1) } - fn time_zoom (&self) -> usize { self.get_time_zoom() } - fn time_zoom_next (&self) -> usize { self.get_time_zoom() + 1 } - fn time_zoom_next_fine (&self) -> usize { self.get_time_zoom() + 1 } - fn time_zoom_prev (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } - fn time_zoom_prev_fine (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } - fn time_lock (&self) -> bool { self.get_time_lock() } - fn time_lock_toggled (&self) -> bool { !self.get_time_lock() } - fn time_pos (&self) -> usize { self.get_time_pos() } - fn time_pos_next (&self) -> usize { (self.get_time_pos() + self.get_note_len()) % self.clip_length() } - fn time_pos_next_fine (&self) -> usize { (self.get_time_pos() + 1) % self.clip_length() } - fn time_pos_prev (&self) -> usize { - let step = self.get_note_len(); - self.get_time_pos().overflowing_sub(step) - .0.min(self.clip_length().saturating_sub(step)) - } - fn time_pos_prev_fine (&self) -> usize { - self.get_time_pos().overflowing_sub(1) - .0.min(self.clip_length().saturating_sub(1)) - } -} - -impl Draw for MidiEditor { fn draw (&self, to: &mut TuiOut) { self.content().draw(to) } } -impl Layout for MidiEditor { fn layout (&self, to: [u16;4]) -> [u16;4] { self.content().layout(to) } } -impl HasContent for MidiEditor { - fn content (&self) -> impl Content { self.autoscroll(); /*self.autozoom();*/ self.size.of(&self.mode) } -} - -impl MidiEditor { - pub fn clip_status (&self) -> impl Content + '_ { - let (_color, name, length, looped) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { - (clip.color, clip.name.clone(), clip.length, clip.looped) - } else { (ItemTheme::G[64], String::new().into(), 0, false) }; - Fixed::X(20, col!( - Fill::X(Align::w(Bsp::e( - button_2("f2", "name ", false), - Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{name} "))))))), - Fill::X(Align::w(Bsp::e( - button_2("l", "ength ", false), - Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{length} "))))))), - Fill::X(Align::w(Bsp::e( - button_2("r", "epeat ", false), - Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{looped} "))))))), - )) - } - pub fn edit_status (&self) -> impl Content + '_ { - let (_color, length) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { - (clip.color, clip.length) - } else { (ItemTheme::G[64], 0) }; - let time_pos = self.get_time_pos(); - let time_zoom = self.get_time_zoom(); - let time_lock = if self.get_time_lock() { "[lock]" } else { " " }; - let note_pos = self.get_note_pos(); - let note_name = format!("{:4}", Note::pitch_to_name(note_pos)); - let note_pos = format!("{:>3}", note_pos); - let note_len = format!("{:>4}", self.get_note_len()); - Fixed::X(20, col!( - Fill::X(Align::w(Bsp::e( - button_2("t", "ime ", false), - Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), - format!("{length} /{time_zoom} +{time_pos} "))))))), - Fill::X(Align::w(Bsp::e( - button_2("z", "lock ", false), - Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), - format!("{time_lock}"))))))), - Fill::X(Align::w(Bsp::e( - button_2("x", "note ", false), - Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), - format!("{note_name} {note_pos} {note_len}"))))))), - )) - } -} - -/// A clip, rendered as a horizontal piano roll. -#[derive(Clone)] -pub struct PianoHorizontal { - pub clip: Option>>, - /// Buffer where the whole clip is rerendered on change - pub buffer: Arc>, - /// Size of actual notes area - pub size: Measure, - /// The display window - pub range: MidiRangeModel, - /// The note cursor - pub point: MidiPointModel, - /// The highlight color palette - pub color: ItemTheme, - /// Width of the keyboard - pub keys_width: u16, -} - -has!(Measure:|self:PianoHorizontal|self.size); - -impl PianoHorizontal { - pub fn new (clip: Option<&Arc>>) -> Self { - let size = Measure::new(); - let mut range = MidiRangeModel::from((12, true)); - range.time_axis = size.x.clone(); - range.note_axis = size.y.clone(); - let piano = Self { - keys_width: 5, - size, - range, - buffer: RwLock::new(Default::default()).into(), - point: MidiPointModel::default(), - clip: clip.cloned(), - color: clip.as_ref().map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]), - }; - piano.redraw(); - piano - } -} - -pub(crate) fn note_y_iter (note_lo: usize, note_hi: usize, y0: u16) - -> impl Iterator -{ - (note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n)) -} - -impl Draw for PianoHorizontal { fn draw (&self, to: &mut TuiOut) { self.content().draw(to) } } -impl Layout for PianoHorizontal { fn layout (&self, to: [u16;4]) -> [u16;4] { self.content().layout(to) } } -impl HasContent for PianoHorizontal { - fn content (&self) -> impl Content { - Bsp::s( - Bsp::e(Fixed::X(5, format!("{}x{}", self.size.w(), self.size.h())), self.timeline()), - Bsp::e(self.keys(), self.size.of(Bsp::b(Fill::XY(self.notes()), Fill::XY(self.cursor())))), - ) - } -} - -impl PianoHorizontal { - /// Draw the piano roll background. - /// - /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ - fn draw_bg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize, note_len: usize, note_point: usize, time_point: usize) { - for (y, note) in (0..=127).rev().enumerate() { - for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) { - let cell = buf.get_mut(x, y).unwrap(); - if note == (127-note_point) || time == time_point { - cell.set_bg(Rgb(0,0,0)); - } else { - cell.set_bg(clip.color.darkest.rgb); - } - if time % 384 == 0 { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('│'); - } else if time % 96 == 0 { - cell.set_fg(clip.color.dark.rgb); - cell.set_char('╎'); - } else if time % note_len == 0 { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('┊'); - } else if (127 - note) % 12 == 0 { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('='); - } else if (127 - note) % 6 == 0 { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('—'); - } else { - cell.set_fg(clip.color.darker.rgb); - cell.set_char('·'); - } - } - } - } - /// Draw the piano roll foreground. - /// - /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ - fn draw_fg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize) { - let style = Style::default().fg(clip.color.base.rgb);//.bg(Rgb(0, 0, 0)); - let mut notes_on = [false;128]; - for (x, time_start) in (0..clip.length).step_by(zoom).enumerate() { - for (_y, note) in (0..=127).rev().enumerate() { - if let Some(cell) = buf.get_mut(x, note) { - if notes_on[note] { - cell.set_char('▂'); - cell.set_style(style); - } - } - } - let time_end = time_start + zoom; - for time in time_start..time_end.min(clip.length) { - for event in clip.notes[time].iter() { - match event { - MidiMessage::NoteOn { key, .. } => { - let note = key.as_int() as usize; - if let Some(cell) = buf.get_mut(x, note) { - cell.set_char('█'); - cell.set_style(style); - } - notes_on[note] = true - }, - MidiMessage::NoteOff { key, .. } => { - notes_on[key.as_int() as usize] = false - }, - _ => {} - } - } - } - - } - } - fn notes (&self) -> impl Content { - let time_start = self.get_time_start(); - let note_lo = self.get_note_lo(); - let note_hi = self.get_note_hi(); - let buffer = self.buffer.clone(); - Thunk::new(move|to: &mut TuiOut|{ - let source = buffer.read().unwrap(); - let [x0, y0, w, _h] = to.area().xywh(); - //if h as usize != note_axis { - //panic!("area height mismatch: {h} <> {note_axis}"); - //} - for (area_x, screen_x) in (x0..x0+w).enumerate() { - for (area_y, screen_y, _note) in note_y_iter(note_lo, note_hi, y0) { - let source_x = time_start + area_x; - let source_y = note_hi - area_y; - // TODO: enable loop rollover: - //let source_x = (time_start + area_x) % source.width.max(1); - //let source_y = (note_hi - area_y) % source.height.max(1); - let is_in_x = source_x < source.width; - let is_in_y = source_y < source.height; - if is_in_x && is_in_y { - if let Some(source_cell) = source.get(source_x, source_y) { - if let Some(cell) = to.buffer.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) { - *cell = source_cell.clone(); - } - } - } - } - } - }) - } - fn cursor (&self) -> impl Content { - let note_hi = self.get_note_hi(); - let note_lo = self.get_note_lo(); - let note_pos = self.get_note_pos(); - let note_len = self.get_note_len(); - let time_pos = self.get_time_pos(); - let time_start = self.get_time_start(); - let time_zoom = self.get_time_zoom(); - let style = Some(Style::default().fg(self.color.lightest.rgb)); - Thunk::new(move|to: &mut TuiOut|{ - let [x0, y0, w, _] = to.area().xywh(); - for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { - if note == note_pos { - for x in 0..w { - let screen_x = x0 + x; - let time_1 = time_start + x as usize * time_zoom; - let time_2 = time_1 + time_zoom; - if time_1 <= time_pos && time_pos < time_2 { - to.blit(&"█", screen_x, screen_y, style); - let tail = note_len as u16 / time_zoom as u16; - for x_tail in (screen_x + 1)..(screen_x + tail) { - to.blit(&"▂", x_tail, screen_y, style); - } - break - } - } - break - } - } - }) - } - fn keys (&self) -> impl Content { - let state = self; - let color = state.color; - let note_lo = state.get_note_lo(); - let note_hi = state.get_note_hi(); - let note_pos = state.get_note_pos(); - let key_style = Some(Style::default().fg(Rgb(192, 192, 192)).bg(Rgb(0, 0, 0))); - let off_style = Some(Style::default().fg(Tui::g(255))); - let on_style = Some(Style::default().fg(Rgb(255,0,0)).bg(color.base.rgb).bold()); - Fill::Y(Fixed::X(self.keys_width, Thunk::new(move|to: &mut TuiOut|{ - let [x, y0, _w, _h] = to.area().xywh(); - for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { - to.blit(&to_key(note), x, screen_y, key_style); - if note > 127 { - continue - } - if note == note_pos { - to.blit(&format!("{:<5}", Note::pitch_to_name(note)), x, screen_y, on_style) - } else { - to.blit(&Note::pitch_to_name(note), x, screen_y, off_style) - }; - } - }))) - } - fn timeline (&self) -> impl Content + '_ { - Fill::X(Fixed::Y(1, Thunk::new(move|to: &mut TuiOut|{ - let [x, y, w, _h] = to.area(); - let style = Some(Style::default().dim()); - let length = self.clip.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); - for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) { - let t = area_x as usize * self.time_zoom().get(); - if t < length { - to.blit(&"|", screen_x, y, style); - } - } - }))) - } -} - -impl TimeRange for PianoHorizontal { - fn time_len (&self) -> &AtomicUsize { self.range.time_len() } - fn time_zoom (&self) -> &AtomicUsize { self.range.time_zoom() } - fn time_lock (&self) -> &AtomicBool { self.range.time_lock() } - fn time_start (&self) -> &AtomicUsize { self.range.time_start() } - fn time_axis (&self) -> &AtomicUsize { self.range.time_axis() } -} - -impl NoteRange for PianoHorizontal { - fn note_lo (&self) -> &AtomicUsize { self.range.note_lo() } - fn note_axis (&self) -> &AtomicUsize { self.range.note_axis() } -} - -impl NotePoint for PianoHorizontal { - fn note_len (&self) -> &AtomicUsize { self.point.note_len() } - fn note_pos (&self) -> &AtomicUsize { self.point.note_pos() } -} - -impl TimePoint for PianoHorizontal { - fn time_pos (&self) -> &AtomicUsize { self.point.time_pos() } -} - -impl MidiViewer for PianoHorizontal { - fn clip (&self) -> &Option>> { &self.clip } - fn clip_mut (&mut self) -> &mut Option>> { &mut self.clip } - /// Determine the required space to render the clip. - fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { (clip.length / self.range.time_zoom().get(), 128) } - fn redraw (&self) { - *self.buffer.write().unwrap() = if let Some(clip) = self.clip.as_ref() { - let clip = clip.read().unwrap(); - let buf_size = self.buffer_size(&clip); - let mut buffer = BigBuffer::from(buf_size); - let time_zoom = self.get_time_zoom(); - self.time_len().set(clip.length); - PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom,self.get_note_len(), self.get_note_pos(), self.get_time_pos()); - PianoHorizontal::draw_fg(&mut buffer, &clip, time_zoom); - buffer - } else { - Default::default() - } - } - fn set_clip (&mut self, clip: Option<&Arc>>) { - *self.clip_mut() = clip.cloned(); - self.color = clip.map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]); - self.redraw(); - } -} - -impl std::fmt::Debug for PianoHorizontal { - fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - let buffer = self.buffer.read().unwrap(); - f.debug_struct("PianoHorizontal") - .field("time_zoom", &self.range.time_zoom) - .field("buffer", &format!("{}x{}", buffer.width, buffer.height)) - .finish() - } -} - -fn to_key (note: usize) -> &'static str { - match note % 12 { - 11 | 9 | 7 | 5 | 4 | 2 | 0 => "████▌", - 10 | 8 | 6 | 3 | 1 => " ", - _ => unreachable!(), - } -} - -pub struct OctaveVertical { on: [bool; 12], colors: [Color; 3] } -impl Default for OctaveVertical { - fn default () -> Self { - Self { on: [false; 12], colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)] } - } -} -impl OctaveVertical { - fn color (&self, pitch: usize) -> Color { - let pitch = pitch % 12; - self.colors[if self.on[pitch] { 2 } else { match pitch { 0 | 2 | 4 | 5 | 6 | 8 | 10 => 0, _ => 1 } }] - } -} -impl HasContent for OctaveVertical { - fn content (&self) -> impl Content + '_ { - row!( - Tui::fg_bg(self.color(0), self.color(1), "▙"), - Tui::fg_bg(self.color(2), self.color(3), "▙"), - Tui::fg_bg(self.color(4), self.color(5), "▌"), - Tui::fg_bg(self.color(6), self.color(7), "▟"), - Tui::fg_bg(self.color(8), self.color(9), "▟"), - Tui::fg_bg(self.color(10), self.color(11), "▟"), - ) - } -} - // Update sequencer playhead indicator - //self.now().set(0.); - //if let Some((ref started_at, Some(ref playing))) = self.sequencer.play_clip { - //let clip = clip.read().unwrap(); - //if *playing.read().unwrap() == *clip { - //let pulse = self.current().pulse.get(); - //let start = started_at.pulse.get(); - //let now = (pulse - start) % clip.length as f64; - //self.now().set(now); - //} - //} diff --git a/device/lv2.rs b/device/lv2.rs deleted file mode 100644 index 2330ce38..00000000 --- a/device/lv2.rs +++ /dev/null @@ -1,310 +0,0 @@ -use crate::*; - -/// A LV2 plugin. -#[derive(Debug)] -pub struct Lv2 { - /// JACK client handle (needs to not be dropped for standalone mode to work). - pub jack: Jack<'static>, - pub name: Arc, - pub path: Option>, - pub selected: usize, - pub mapping: bool, - pub midi_ins: Vec>, - pub midi_outs: Vec>, - pub audio_ins: Vec>, - pub audio_outs: Vec>, - - pub lv2_world: livi::World, - pub lv2_instance: livi::Instance, - pub lv2_plugin: livi::Plugin, - pub lv2_features: Arc, - pub lv2_port_list: Vec, - pub lv2_input_buffer: Vec, - pub lv2_ui_thread: Option>, -} - -impl Lv2 { - - pub fn new ( - jack: &Jack<'static>, - name: &str, - uri: &str, - ) -> Usually { - let lv2_world = livi::World::with_load_bundle(&uri); - let lv2_features = lv2_world.build_features(livi::FeaturesBuilder { - min_block_length: 1, - max_block_length: 65536, - }); - let lv2_plugin = lv2_world.iter_plugins().nth(0) - .unwrap_or_else(||panic!("plugin not found: {uri}")); - Ok(Self { - jack: jack.clone(), - name: name.into(), - path: Some(String::from(uri).into()), - selected: 0, - mapping: false, - midi_ins: vec![], - midi_outs: vec![], - audio_ins: vec![], - audio_outs: vec![], - lv2_instance: unsafe { - lv2_plugin - .instantiate(lv2_features.clone(), 48000.0) - .expect(&format!("instantiate failed: {uri}")) - }, - lv2_port_list: lv2_plugin.ports().collect::>(), - lv2_input_buffer: Vec::with_capacity(Self::INPUT_BUFFER), - lv2_ui_thread: None, - lv2_world, - lv2_features, - lv2_plugin, - }) - } - - const INPUT_BUFFER: usize = 1024; - -} - - - //fn jack_from_lv2 (name: &str, plugin: &::livi::Plugin) -> Usually { - //let counts = plugin.port_counts(); - //let mut jack = Jack::new(name)?; - //for i in 0..counts.atom_sequence_inputs { - //jack = jack.midi_in(&format!("midi-in-{i}")) - //} - //for i in 0..counts.atom_sequence_outputs { - //jack = jack.midi_out(&format!("midi-out-{i}")); - //} - //for i in 0..counts.audio_inputs { - //jack = jack.audio_in(&format!("audio-in-{i}")); - //} - //for i in 0..counts.audio_outputs { - //jack = jack.audio_out(&format!("audio-out-{i}")); - //} - //Ok(jack) - //} - -audio!(|self: Lv2, _client, scope|{ - let Self { - midi_ins, - midi_outs, - audio_ins, - audio_outs, - lv2_features, - lv2_instance, - lv2_input_buffer, - .. - } = self; - let urid = lv2_features.midi_urid(); - lv2_input_buffer.clear(); - for port in midi_ins.iter() { - let mut atom = ::livi::event::LV2AtomSequence::new( - &lv2_features, - scope.n_frames() as usize - ); - for event in port.iter(scope) { - match event.bytes.len() { - 3 => atom.push_midi_event::<3>( - event.time as i64, - urid, - &event.bytes[0..3] - ).unwrap(), - _ => {} - } - } - lv2_input_buffer.push(atom); - } - let mut outputs = vec![]; - for _ in midi_outs.iter() { - outputs.push(::livi::event::LV2AtomSequence::new( - lv2_features, - scope.n_frames() as usize - )); - } - let ports = ::livi::EmptyPortConnections::new() - .with_atom_sequence_inputs(lv2_input_buffer.iter()) - .with_atom_sequence_outputs(outputs.iter_mut()) - .with_audio_inputs(audio_ins.iter().map(|o|o.as_slice(scope))) - .with_audio_outputs(audio_outs.iter_mut().map(|o|o.as_mut_slice(scope))); - unsafe { - lv2_instance.run(scope.n_frames() as usize, ports).unwrap() - }; - Control::Continue -}); - -impl Draw for Lv2 { - fn draw (&self, to: &mut TuiOut) { - let area = to.area(); - let [x, y, _, height] = area; - let mut width = 20u16; - let start = self.selected.saturating_sub((height as usize / 2).saturating_sub(1)); - let end = start + height as usize - 2; - //draw_box(buf, Rect { x, y, width, height }); - for i in start..end { - if let Some(port) = self.lv2_port_list.get(i) { - let value = if let Some(value) = self.lv2_instance.control_input(port.index) { - value - } else { - port.default_value - }; - //let label = &format!("C·· M·· {:25} = {value:.03}", port.name); - let label = &format!("{:25} = {value:.03}", port.name); - width = width.max(label.len() as u16 + 4); - let style = if i == self.selected { - Some(Style::default().green()) - } else { - None - } ; - to.blit(&label, x + 2, y + 1 + i as u16 - start as u16, style); - } else { - break - } - } - draw_header(self, to, x, y, width); - } -} - -fn draw_header (state: &Lv2, to: &mut TuiOut, x: u16, y: u16, w: u16) { - let style = Style::default().gray(); - let label1 = format!(" {}", state.name); - to.blit(&label1, x + 1, y, Some(style.white().bold())); - if let Some(ref path) = state.path { - let label2 = format!("{}…", &path[..((w as usize - 10).min(path.len()))]); - to.blit(&label2, x + 2 + label1.len() as u16, y, Some(style.not_dim())); - } - //Ok(Rect { x, y, width: w, height: 1 }) -} - -//handle!(TuiIn: |self:Plugin, from|{ - //match from.event() { - //kpat!(KeyCode::Up) => { - //self.selected = self.selected.saturating_sub(1); - //Ok(Some(true)) - //}, - //kpat!(KeyCode::Down) => { - //self.selected = (self.selected + 1).min(match &self.plugin { - //Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1, - //_ => unimplemented!() - //}); - //Ok(Some(true)) - //}, - //kpat!(KeyCode::PageUp) => { - //self.selected = self.selected.saturating_sub(8); - //Ok(Some(true)) - //}, - //kpat!(KeyCode::PageDown) => { - //self.selected = (self.selected + 10).min(match &self.plugin { - //Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1, - //_ => unimplemented!() - //}); - //Ok(Some(true)) - //}, - //kpat!(KeyCode::Char(',')) => { - //match self.plugin.as_mut() { - //Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => { - //let index = port_list[self.selected].index; - //if let Some(value) = instance.control_input(index) { - //instance.set_control_input(index, value - 0.01); - //} - //}, - //_ => {} - //} - //Ok(Some(true)) - //}, - //kpat!(KeyCode::Char('.')) => { - //match self.plugin.as_mut() { - //Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => { - //let index = port_list[self.selected].index; - //if let Some(value) = instance.control_input(index) { - //instance.set_control_input(index, value + 0.01); - //} - //}, - //_ => {} - //} - //Ok(Some(true)) - //}, - //kpat!(KeyCode::Char('g')) => { - //match self.plugin { - ////Some(PluginKind::LV2(ref mut plugin)) => { - ////plugin.ui_thread = Some(run_lv2_ui(LV2PluginUI::new()?)?); - ////}, - //Some(_) => unreachable!(), - //None => {} - //} - //Ok(Some(true)) - //}, - //_ => Ok(None) - //} -//}); - -//from_atom!("plugin/lv2" => |jack: &Jack, args| -> Plugin { - //let mut name = String::new(); - //let mut path = String::new(); - //atom!(atom in args { - //Atom::Map(map) => { - //if let Some(Atom::Str(n)) = map.get(&Atom::Key(":name")) { - //name = String::from(*n); - //} - //if let Some(Atom::Str(p)) = map.get(&Atom::Key(":path")) { - //path = String::from(*p); - //} - //}, - //_ => panic!("unexpected in lv2 '{name}'"), - //}); - //Plugin::new_lv2(jack, &name, &path) -//}); - -//pub struct LV2PluginUI { - //write: (), - //controller: (), - //widget: (), - //features: (), - //transfer: (), -//} - - -#[cfg(feature = "lv2_gui")] -pub fn run_lv2_ui (mut ui: LV2PluginUI) -> Usually> { - Ok(spawn(move||{ - let event_loop = EventLoop::builder().with_x11().with_any_thread(true).build().unwrap(); - event_loop.set_control_flow(ControlFlow::Wait); - event_loop.run_app(&mut ui).unwrap() - })) -} - -#[cfg(feature = "lv2_gui")] -/// A LV2 plugin's X11 UI. -pub struct LV2PluginUI { - pub window: Option -} - -#[cfg(feature = "lv2_gui")] -impl LV2PluginUI { - pub fn new () -> Usually { - Ok(Self { window: None }) - } -} - -#[cfg(feature = "lv2_gui")] -impl ApplicationHandler for LV2PluginUI { - fn resumed (&mut self, event_loop: &ActiveEventLoop) { - self.window = Some(event_loop.create_window(Window::default_attributes()).unwrap()); - } - fn window_event (&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) { - match event { - WindowEvent::CloseRequested => { - self.window.as_ref().unwrap().set_visible(false); - event_loop.exit(); - }, - WindowEvent::RedrawRequested => { - self.window.as_ref().unwrap().request_redraw(); - } - _ => (), - } - } -} - -#[cfg(feature = "lv2_gui")] -fn lv2_ui_instantiate (kind: &str) { - //let host = Suil -} diff --git a/device/meter.rs b/device/meter.rs deleted file mode 100644 index 8a7d9a50..00000000 --- a/device/meter.rs +++ /dev/null @@ -1,85 +0,0 @@ -use crate::*; - -#[derive(Debug, Default)] -pub enum MeteringMode { - #[default] - Rms, - Log10, -} - -#[derive(Debug, Default, Clone)] -pub struct Log10Meter(pub f32); -impl Layout for Log10Meter {} -impl Draw for Log10Meter { - fn draw (&self, to: &mut TuiOut) { - let [x, y, w, h] = to.area(); - let signal = 100.0 - f32::max(0.0, f32::min(100.0, self.0.abs())); - let v = (signal * h as f32 / 100.0).ceil() as u16; - let y2 = y + h; - //to.blit(&format!("\r{v} {} {signal}", self.0), x * 20, y, None); - for y in y..(y + v) { - for x in x..(x + w) { - to.blit(&"▌", x, y2 - y, Some(Style::default().green())); - } - } - } -} - -pub fn to_log10 (samples: &[f32]) -> f32 { - let total: f32 = samples.iter().map(|x|x.abs()).sum(); - let count = samples.len() as f32; - 10. * (total / count).log10() -} - -#[derive(Debug, Default, Clone)] -pub struct RmsMeter(pub f32); -impl Layout for RmsMeter {} -impl Draw for RmsMeter { - fn draw (&self, to: &mut TuiOut) { - let [x, y, w, h] = to.area(); - let signal = f32::max(0.0, f32::min(100.0, self.0.abs())); - let v = (signal * h as f32).ceil() as u16; - let y2 = y + h; - //to.blit(&format!("\r{v} {} {signal}", self.0), x * 30, y, Some(Style::default())); - for y in y..(y + v) { - for x in x..(x + w) { - to.blit(&"▌", x, y2.saturating_sub(y), Some(Style::default().green())); - } - } - } -} - -pub fn to_rms (samples: &[f32]) -> f32 { - let sum = samples.iter() - .map(|s|*s) - .reduce(|sum, sample|sum + sample.abs()) - .unwrap_or(0.0); - (sum / samples.len() as f32).sqrt() -} - -pub fn view_meter <'a> (label: &'a str, value: f32) -> impl Content + 'a { - col!( - FieldH(ItemTheme::G[128], label, format!("{:>+9.3}", value)), - Fixed::XY(if value >= 0.0 { 13 } - else if value >= -1.0 { 12 } - else if value >= -2.0 { 11 } - else if value >= -3.0 { 10 } - else if value >= -4.0 { 9 } - else if value >= -6.0 { 8 } - else if value >= -9.0 { 7 } - else if value >= -12.0 { 6 } - else if value >= -15.0 { 5 } - else if value >= -20.0 { 4 } - else if value >= -25.0 { 3 } - else if value >= -30.0 { 2 } - else if value >= -40.0 { 1 } - else { 0 }, 1, Tui::bg(if value >= 0.0 { Red } - else if value >= -3.0 { Yellow } - else { Green }, ()))) -} - -pub fn view_meters (values: &[f32;2]) -> impl Content + use<'_> { - let left = format!("L/{:>+9.3}", values[0]); - let right = format!("R/{:>+9.3}", values[1]); - Bsp::s(left, right) -} diff --git a/device/mixer.rs b/device/mixer.rs deleted file mode 100644 index 99b2d74f..00000000 --- a/device/mixer.rs +++ /dev/null @@ -1,43 +0,0 @@ -#[allow(unused)] use crate::*; - -#[derive(Debug, Default)] -pub enum MixingMode { - #[default] - Summing, - Average, -} - -pub fn mix_summing ( - buffer: &mut [Vec], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>, -) -> bool { - let channels = buffer.len(); - for index in 0..frames { - if let Some(frame) = next() { - for (channel, sample) in frame.iter().enumerate() { - let channel = channel % channels; - buffer[channel][index] += sample * gain; - } - } else { - return false - } - } - true -} - -pub fn mix_average ( - buffer: &mut [Vec], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>, -) -> bool { - let channels = buffer.len(); - for index in 0..frames { - if let Some(frame) = next() { - for (channel, sample) in frame.iter().enumerate() { - let channel = channel % channels; - let value = buffer[channel][index]; - buffer[channel][index] = (value + sample * gain) / 2.0; - } - } else { - return false - } - } - true -} diff --git a/device/pool.rs b/device/pool.rs deleted file mode 100644 index 42bef43c..00000000 --- a/device/pool.rs +++ /dev/null @@ -1,411 +0,0 @@ -use crate::*; -#[derive(Debug)] -pub struct Pool { - pub visible: bool, - /// Selected clip - pub clip: AtomicUsize, - /// Mode switch - pub mode: Option, - /// Collection of clips - #[cfg(feature = "clip")] pub clips: Arc>>>>, - /// Embedded file browse - #[cfg(feature = "browse")] pub browse: Option, -} -impl Default for Pool { - fn default () -> Self { - //use PoolMode::*; - Self { - visible: true, - clip: 0.into(), - mode: None, - clips: Arc::from(RwLock::from(vec![])), - browse: None, - } - } -} -impl Pool { - pub fn clip_index (&self) -> usize { - self.clip.load(Relaxed) - } - pub fn set_clip_index (&self, value: usize) { - self.clip.store(value, Relaxed); - } - pub fn mode (&self) -> &Option { - &self.mode - } - pub fn mode_mut (&mut self) -> &mut Option { - &mut self.mode - } - pub fn begin_clip_length (&mut self) { - let length = self.clips()[self.clip_index()].read().unwrap().length; - *self.mode_mut() = Some(PoolMode::Length( - self.clip_index(), - length, - ClipLengthFocus::Bar - )); - } - pub fn begin_clip_rename (&mut self) { - let name = self.clips()[self.clip_index()].read().unwrap().name.clone(); - *self.mode_mut() = Some(PoolMode::Rename( - self.clip_index(), - name - )); - } - pub fn begin_import (&mut self) -> Usually<()> { - *self.mode_mut() = Some(PoolMode::Import( - self.clip_index(), - Browse::new(None)? - )); - Ok(()) - } - pub fn begin_export (&mut self) -> Usually<()> { - *self.mode_mut() = Some(PoolMode::Export( - self.clip_index(), - Browse::new(None)? - )); - Ok(()) - } - pub fn new_clip (&self) -> MidiClip { - MidiClip::new("Clip", true, 4 * PPQ, None, Some(ItemTheme::random())) - } - pub fn cloned_clip (&self) -> MidiClip { - let index = self.clip_index(); - let mut clip = self.clips()[index].read().unwrap().duplicate(); - clip.color = ItemTheme::random_near(clip.color, 0.25); - clip - } - pub fn add_new_clip (&self) -> (usize, Arc>) { - let clip = Arc::new(RwLock::new(self.new_clip())); - let index = { - let mut clips = self.clips.write().unwrap(); - clips.push(clip.clone()); - clips.len().saturating_sub(1) - }; - self.clip.store(index, Relaxed); - (index, clip) - } - pub fn delete_clip (&mut self, clip: &MidiClip) -> bool { - let index = self.clips.read().unwrap().iter().position(|x|*x.read().unwrap()==*clip); - if let Some(index) = index { - self.clips.write().unwrap().remove(index); - return true - } - false - } -} -/// Modes for clip pool -#[derive(Debug, Clone)] -pub enum PoolMode { - /// Renaming a pattern - Rename(usize, Arc), - /// Editing the length of a pattern - Length(usize, usize, ClipLengthFocus), - /// Load clip from disk - Import(usize, Browse), - /// Save clip to disk - Export(usize, Browse), -} -/// Focused field of `ClipLength` -#[derive(Copy, Clone, Debug)] -pub enum ClipLengthFocus { - /// Editing the number of bars - Bar, - /// Editing the number of beats - Beat, - /// Editing the number of ticks - Tick, -} -impl ClipLengthFocus { - pub fn next (&mut self) { - use ClipLengthFocus::*; - *self = match self { Bar => Beat, Beat => Tick, Tick => Bar, } - } - pub fn prev (&mut self) { - use ClipLengthFocus::*; - *self = match self { Bar => Tick, Beat => Bar, Tick => Beat, } - } -} -/// Displays and edits clip length. -#[derive(Clone)] -pub struct ClipLength { - /// Pulses per beat (quaver) - ppq: usize, - /// Beats per bar - bpb: usize, - /// Length of clip in pulses - pulses: usize, - /// Selected subdivision - pub focus: Option, -} -impl ClipLength { - pub fn _new (pulses: usize, focus: Option) -> Self { - Self { ppq: PPQ, bpb: 4, pulses, focus } - } - pub fn bars (&self) -> usize { - self.pulses / (self.bpb * self.ppq) - } - pub fn beats (&self) -> usize { - (self.pulses % (self.bpb * self.ppq)) / self.ppq - } - pub fn ticks (&self) -> usize { - self.pulses % self.ppq - } - pub fn bars_string (&self) -> Arc { - format!("{}", self.bars()).into() - } - pub fn beats_string (&self) -> Arc { - format!("{}", self.beats()).into() - } - pub fn ticks_string (&self) -> Arc { - format!("{:>02}", self.ticks()).into() - } -} -pub type ClipPool = Vec>>; -pub trait HasClips { - fn clips <'a> (&'a self) -> std::sync::RwLockReadGuard<'a, ClipPool>; - fn clips_mut <'a> (&'a self) -> std::sync::RwLockWriteGuard<'a, ClipPool>; - fn add_clip (&self) -> (usize, Arc>) { - let clip = Arc::new(RwLock::new(MidiClip::new("Clip", true, 384, None, None))); - self.clips_mut().push(clip.clone()); - (self.clips().len() - 1, clip) - } -} -#[macro_export] macro_rules! has_clips { - (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? { - fn clips <'a> (&'a $self) -> std::sync::RwLockReadGuard<'a, ClipPool> { - $cb.read().unwrap() - } - fn clips_mut <'a> (&'a $self) -> std::sync::RwLockWriteGuard<'a, ClipPool> { - $cb.write().unwrap() - } - } - } -} -has_clips!(|self: Pool|self.clips); -has_clip!(|self: Pool|self.clips().get(self.clip_index()).map(|c|c.clone())); -from!(|clip:&Arc>|Pool = { - let model = Self::default(); model.clips.write().unwrap().push(clip.clone()); model.clip.store(1, Relaxed); model -}); -impl Pool { - fn _todo_usize_ (&self) -> usize { todo!() } - fn _todo_bool_ (&self) -> bool { todo!() } - fn _todo_clip_ (&self) -> MidiClip { todo!() } - fn _todo_path_ (&self) -> PathBuf { todo!() } - fn _todo_color_ (&self) -> ItemColor { todo!() } - fn _todo_str_ (&self) -> Arc { todo!() } - fn clip_new (&self) -> MidiClip { self.new_clip() } - fn clip_cloned (&self) -> MidiClip { self.cloned_clip() } - fn clip_index_current (&self) -> usize { 0 } - fn clip_index_after (&self) -> usize { 0 } - fn clip_index_previous (&self) -> usize { 0 } - fn clip_index_next (&self) -> usize { 0 } - fn color_random (&self) -> ItemColor { ItemColor::random() } -} -def_command!(PoolCommand: |pool: Pool| { - // Toggle visibility of pool - Show { visible: bool } => { pool.visible = *visible; Ok(Some(Self::Show { visible: !visible })) }, - // Select a clip from the clip pool - Select { index: usize } => { pool.set_clip_index(*index); Ok(None) }, - // Update the contents of the clip pool - Clip { command: PoolClipCommand } => Ok(command.execute(pool)?.map(|command|Self::Clip{command})), - // Rename a clip - Rename { command: RenameCommand } => Ok(command.delegate(pool, |command|Self::Rename{command})?), - // Change the length of a clip - Length { command: CropCommand } => Ok(command.delegate(pool, |command|Self::Length{command})?), - // Import from file - Import { command: BrowseCommand } => Ok(if let Some(browse) = pool.browse.as_mut() { - command.delegate(browse, |command|Self::Import{command})? - } else { - None - }), - // Export to file - Export { command: BrowseCommand } => Ok(if let Some(browse) = pool.browse.as_mut() { - command.delegate(browse, |command|Self::Export{command})? - } else { - None - }), -}); -def_command!(PoolClipCommand: |pool: Pool| { - Delete { index: usize } => { - let index = *index; - let clip = pool.clips_mut().remove(index).read().unwrap().clone(); - Ok(Some(Self::Add { index, clip })) - }, - Swap { index: usize, other: usize } => { - let index = *index; - let other = *other; - pool.clips_mut().swap(index, other); - Ok(Some(Self::Swap { index, other })) - }, - Export { index: usize, path: PathBuf } => { - todo!("export clip to midi file"); - }, - Add { index: usize, clip: MidiClip } => { - let index = *index; - let mut index = index; - let clip = Arc::new(RwLock::new(clip.clone())); - let mut clips = pool.clips_mut(); - if index >= clips.len() { - index = clips.len(); - clips.push(clip) - } else { - clips.insert(index, clip); - } - Ok(Some(Self::Delete { index })) - }, - Import { index: usize, path: PathBuf } => { - let index = *index; - let bytes = std::fs::read(&path)?; - let smf = Smf::parse(bytes.as_slice())?; - let mut t = 0u32; - let mut events = vec![]; - for track in smf.tracks.iter() { - for event in track.iter() { - t += event.delta.as_int(); - if let TrackEventKind::Midi { channel, message } = event.kind { - events.push((t, channel.as_int(), message)); - } - } - } - let mut clip = MidiClip::new("imported", true, t as usize + 1, None, None); - for event in events.iter() { - clip.notes[event.0 as usize].push(event.2); - } - Ok(Self::Add { index, clip }.execute(pool)?) - }, - SetName { index: usize, name: Arc } => { - let index = *index; - let clip = &mut pool.clips_mut()[index]; - let old_name = clip.read().unwrap().name.clone(); - clip.write().unwrap().name = name.clone(); - Ok(Some(Self::SetName { index, name: old_name })) - }, - SetLength { index: usize, length: usize } => { - let index = *index; - let clip = &mut pool.clips_mut()[index]; - let old_len = clip.read().unwrap().length; - clip.write().unwrap().length = *length; - Ok(Some(Self::SetLength { index, length: old_len })) - }, - SetColor { index: usize, color: ItemColor } => { - let index = *index; - let mut color = ItemTheme::from(*color); - std::mem::swap(&mut color, &mut pool.clips()[index].write().unwrap().color); - Ok(Some(Self::SetColor { index, color: color.base })) - }, -}); - -def_command!(RenameCommand: |pool: Pool| { - Begin => unreachable!(), - Cancel => { - if let Some(PoolMode::Rename(clip, ref mut old_name)) = pool.mode_mut().clone() { - pool.clips()[clip].write().unwrap().name = old_name.clone().into(); - } - Ok(None) - }, - Confirm => { - if let Some(PoolMode::Rename(_clip, ref mut old_name)) = pool.mode_mut().clone() { - let old_name = old_name.clone(); *pool.mode_mut() = None; return Ok(Some(Self::Set { value: old_name })) - } - Ok(None) - }, - Set { value: Arc } => { - if let Some(PoolMode::Rename(clip, ref mut _old_name)) = pool.mode_mut().clone() { - pool.clips()[clip].write().unwrap().name = value.clone(); - } - Ok(None) - }, -}); -def_command!(CropCommand: |pool: Pool| { - Begin => unreachable!(), - Cancel => { if let Some(PoolMode::Length(..)) = pool.mode_mut().clone() { *pool.mode_mut() = None; } Ok(None) }, - Set { length: usize } => { - if let Some(PoolMode::Length(clip, ref mut length, ref mut _focus)) - = pool.mode_mut().clone() - { - let old_length; - { - let clip = pool.clips()[clip].clone();//.write().unwrap(); - old_length = Some(clip.read().unwrap().length); - clip.write().unwrap().length = *length; - } - *pool.mode_mut() = None; - return Ok(old_length.map(|length|Self::Set { length })) - } - Ok(None) - }, - Next => { - if let Some(PoolMode::Length(_clip, ref mut _length, ref mut focus)) = pool.mode_mut().clone() { focus.next() }; Ok(None) - }, - Prev => { - if let Some(PoolMode::Length(_clip, ref mut _length, ref mut focus)) = pool.mode_mut().clone() { focus.prev() }; Ok(None) - }, - Inc => { - if let Some(PoolMode::Length(_clip, ref mut length, ref mut focus)) = pool.mode_mut().clone() { - match focus { - ClipLengthFocus::Bar => { *length += 4 * PPQ }, - ClipLengthFocus::Beat => { *length += PPQ }, - ClipLengthFocus::Tick => { *length += 1 }, - } - } - Ok(None) - }, - Dec => { - if let Some(PoolMode::Length(_clip, ref mut length, ref mut focus)) = pool.mode_mut().clone() { - match focus { - ClipLengthFocus::Bar => { *length = length.saturating_sub(4 * PPQ) }, - ClipLengthFocus::Beat => { *length = length.saturating_sub(PPQ) }, - ClipLengthFocus::Tick => { *length = length.saturating_sub(1) }, - } - } - Ok(None) - } -}); -pub struct PoolView<'a>(pub &'a Pool); -impl<'a> HasContent for PoolView<'a> { - fn content (&self) -> impl Content { - let Self(pool) = self; - //let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||Tui::g(32).into()); - //let on_bg = |x|x;//Bsp::b(Repeat(" "), Tui::bg(color.darkest.rgb, x)); - //let border = |x|x;//Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)).enclose(x); - //let height = pool.clips.read().unwrap().len() as u16; - Fixed::X(20, Fill::Y(Align::n(Map::new( - ||pool.clips().clone().into_iter(), - move|clip: Arc>, i: usize|{ - let item_height = 1; - let item_offset = i as u16 * item_height; - let selected = i == pool.clip_index(); - let MidiClip { ref name, color, length, .. } = *clip.read().unwrap(); - let bg = if selected { color.light.rgb } else { color.base.rgb }; - let fg = color.lightest.rgb; - let name = if false { format!(" {i:>3}") } else { format!(" {i:>3} {name}") }; - let length = if false { String::default() } else { format!("{length} ") }; - Fixed::Y(1, map_south(item_offset, item_height, Tui::bg(bg, lay!( - Fill::X(Align::w(Tui::fg(fg, Tui::bold(selected, name)))), - Fill::X(Align::e(Tui::fg(fg, Tui::bold(selected, length)))), - Fill::X(Align::w(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "▶"))))), - Fill::X(Align::e(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "◀"))))), - )))) - })))) - } -} -impl HasContent for ClipLength { - fn content (&self) -> impl Content + '_ { - use ClipLengthFocus::*; - let bars = ||self.bars_string(); - let beats = ||self.beats_string(); - let ticks = ||self.ticks_string(); - match self.focus { - None => row!(" ", bars(), ".", beats(), ".", ticks()), - Some(Bar) => row!("[", bars(), "]", beats(), ".", ticks()), - Some(Beat) => row!(" ", bars(), "[", beats(), "]", ticks()), - Some(Tick) => row!(" ", bars(), ".", beats(), "[", ticks()), - } - } -} -//take!(BrowseCommand |state: Pool, iter|Ok(state.browse.as_ref() - //.map(|p|Take::take(p, iter)) - //.transpose()? - //.flatten())); - diff --git a/device/port.rs b/device/port.rs deleted file mode 100644 index 0485f7c8..00000000 --- a/device/port.rs +++ /dev/null @@ -1,577 +0,0 @@ -use crate::*; - -def_sizes_iter!(InputsSizes => MidiInput); -def_sizes_iter!(OutputsSizes => MidiOutput); -def_sizes_iter!(PortsSizes => Arc, [Connect]); - -pub(crate) use ConnectName::*; -pub(crate) use ConnectScope::*; -pub(crate) use ConnectStatus::*; - -#[derive(Debug)] pub struct AudioInput { - /// Handle to JACK client, for receiving reconnect events. - jack: Jack<'static>, - /// Port name - name: Arc, - /// Port handle. - port: Port, - /// List of ports to connect to. - pub connections: Vec, -} - -#[derive(Debug)] pub struct AudioOutput { - /// Handle to JACK client, for receiving reconnect events. - jack: Jack<'static>, - /// Port name - name: Arc, - /// Port handle. - port: Port, - /// List of ports to connect to. - pub connections: Vec, -} - -#[derive(Debug)] pub struct MidiInput { - /// Handle to JACK client, for receiving reconnect events. - jack: Jack<'static>, - /// Port name - name: Arc, - /// Port handle. - port: Port, - /// List of currently held notes. - held: Arc>, - /// List of ports to connect to. - pub connections: Vec, -} - -#[derive(Debug)] pub struct MidiOutput { - /// Handle to JACK client, for receiving reconnect events. - jack: Jack<'static>, - /// Port name - name: Arc, - /// Port handle. - port: Port, - /// List of ports to connect to. - pub connections: Vec, - /// List of currently held notes. - held: Arc>, - /// Buffer - note_buffer: Vec, - /// Buffer - output_buffer: Vec>>, -} - -pub trait RegisterPorts: HasJack<'static> { - /// Register a MIDI input port. - fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; - /// Register a MIDI output port. - fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; - /// Register an audio input port. - fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; - /// Register an audio output port. - fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; -} - -impl> RegisterPorts for J { - fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - MidiInput::new(self.jack(), name, connect) - } - fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - MidiOutput::new(self.jack(), name, connect) - } - fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - AudioInput::new(self.jack(), name, connect) - } - fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - AudioOutput::new(self.jack(), name, connect) - } -} -//take!(MidiInputCommand |state: Arrangement, iter|state.selected_midi_in().as_ref() - //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); -//take!(MidiOutputCommand |state: Arrangement, iter|state.selected_midi_out().as_ref() - //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); - -pub trait JackPort: HasJack<'static> { - type Port: PortSpec + Default; - type Pair: PortSpec + Default; - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized; - fn register (jack: &Jack<'static>, name: &impl AsRef) -> Usually> { - jack.with_client(|c|c.register_port::(name.as_ref(), Default::default())) - .map_err(|e|e.into()) - } - fn port_name (&self) -> &Arc; - fn connections (&self) -> &[Connect]; - fn port (&self) -> &Port; - fn port_mut (&mut self) -> &mut Port; - fn into_port (self) -> Port where Self: Sized; - fn close (self) -> Usually<()> where Self: Sized { - let jack = self.jack().clone(); - Ok(jack.with_client(|c|c.unregister_port(self.into_port()))?) - } - fn ports (&self, re_name: Option<&str>, re_type: Option<&str>, flags: PortFlags) -> Vec { - self.with_client(|c|c.ports(re_name, re_type, flags)) - } - fn port_by_id (&self, id: u32) -> Option> { - self.with_client(|c|c.port_by_id(id)) - } - fn port_by_name (&self, name: impl AsRef) -> Option> { - self.with_client(|c|c.port_by_name(name.as_ref())) - } - fn connect_to_matching <'k> (&'k self) -> Usually<()> { - for connect in self.connections().iter() { - //panic!("{connect:?}"); - let status = match &connect.name { - Exact(name) => self.connect_exact(name), - RegExp(re) => self.connect_regexp(re, connect.scope), - }?; - *connect.status.write().unwrap() = status; - } - Ok(()) - } - fn connect_exact <'k> (&'k self, name: &str) -> - Usually, Arc, ConnectStatus)>> - { - self.with_client(move|c|{ - let mut status = vec![]; - for port in c.ports(None, None, PortFlags::empty()).iter() { - if port.as_str() == &*name { - if let Some(port) = c.port_by_name(port.as_str()) { - let port_status = self.connect_to_unowned(&port)?; - let name = port.name()?.into(); - status.push((port, name, port_status)); - if port_status == Connected { - break - } - } - } - } - Ok(status) - }) - } - fn connect_regexp <'k> ( - &'k self, re: &str, scope: ConnectScope - ) -> Usually, Arc, ConnectStatus)>> { - self.with_client(move|c|{ - let mut status = vec![]; - let ports = c.ports(Some(&re), None, PortFlags::empty()); - for port in ports.iter() { - if let Some(port) = c.port_by_name(port.as_str()) { - let port_status = self.connect_to_unowned(&port)?; - let name = port.name()?.into(); - status.push((port, name, port_status)); - if port_status == Connected && scope == One { - break - } - } - } - Ok(status) - }) - } - /** Connect to a matching port by name. */ - fn connect_to_name (&self, name: impl AsRef) -> Usually { - self.with_client(|c|if let Some(ref port) = c.port_by_name(name.as_ref()) { - self.connect_to_unowned(port) - } else { - Ok(Missing) - }) - } - /** Connect to a matching port by reference. */ - fn connect_to_unowned (&self, port: &Port) -> Usually { - self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { - Connected - } else if let Ok(_) = c.connect_ports(port, self.port()) { - Connected - } else { - Mismatch - })) - } - /** Connect to an owned matching port by reference. */ - fn connect_to_owned (&self, port: &Port) -> Usually { - self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { - Connected - } else if let Ok(_) = c.connect_ports(port, self.port()) { - Connected - } else { - Mismatch - })) - } -} - -#[derive(Clone, Debug, PartialEq)] -pub enum ConnectName { - /** Exact match */ - Exact(Arc), - /** Match regular expression */ - RegExp(Arc), -} - -#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectScope { - One, - All -} - -#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectStatus { - Missing, - Disconnected, - Connected, - Mismatch, -} - -#[derive(Clone, Debug)] pub struct Connect { - pub name: ConnectName, - pub scope: ConnectScope, - pub status: Arc, Arc, ConnectStatus)>>>, - pub info: Arc, -} - -impl Connect { - pub fn collect (exact: &[impl AsRef], re: &[impl AsRef], re_all: &[impl AsRef]) - -> Vec - { - let mut connections = vec![]; - for port in exact.iter() { connections.push(Self::exact(port)) } - for port in re.iter() { connections.push(Self::regexp(port)) } - for port in re_all.iter() { connections.push(Self::regexp_all(port)) } - connections - } - /// Connect to this exact port - pub fn exact (name: impl AsRef) -> Self { - let info = format!("=:{}", name.as_ref()).into(); - let name = Exact(name.as_ref().into()); - Self { name, scope: One, status: Arc::new(RwLock::new(vec![])), info } - } - pub fn regexp (name: impl AsRef) -> Self { - let info = format!("~:{}", name.as_ref()).into(); - let name = RegExp(name.as_ref().into()); - Self { name, scope: One, status: Arc::new(RwLock::new(vec![])), info } - } - pub fn regexp_all (name: impl AsRef) -> Self { - let info = format!("+:{}", name.as_ref()).into(); - let name = RegExp(name.as_ref().into()); - Self { name, scope: All, status: Arc::new(RwLock::new(vec![])), info } - } - pub fn info (&self) -> Arc { - let status = { - let status = self.status.read().unwrap(); - let mut ok = 0; - for (_, _, state) in status.iter() { - if *state == Connected { - ok += 1 - } - } - format!("{ok}/{}", status.len()) - }; - let scope = match self.scope { - One => " ", All => "*", - }; - let name = match &self.name { - Exact(name) => format!("= {name}"), RegExp(name) => format!("~ {name}"), - }; - format!(" ({}) {} {}", status, scope, name).into() - } -} - -def_command!(MidiInputCommand: |port: MidiInput| { - Close => todo!(), - Connect { midi_out: Arc } => todo!(), -}); -def_command!(MidiOutputCommand: |port: MidiOutput| { - Close => todo!(), - Connect { midi_in: Arc } => todo!(), -}); -def_command!(AudioInputCommand: |port: AudioInput| { - Close => todo!(), - Connect { audio_out: Arc } => todo!(), -}); -def_command!(AudioOutputCommand: |port: AudioOutput| { - Close => todo!(), - Connect { audio_in: Arc } => todo!(), -}); - -impl HasJack<'static> for MidiInput { fn jack (&self) -> &Jack<'static> { &self.jack } } - -impl JackPort for MidiInput { - type Port = MidiIn; - type Pair = MidiOut; - fn port_name (&self) -> &Arc { - &self.name - } - fn port (&self) -> &Port { - &self.port - } - fn port_mut (&mut self) -> &mut Port { - &mut self.port - } - fn into_port (self) -> Port { - self.port - } - fn connections (&self) -> &[Connect] { - self.connections.as_slice() - } - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized - { - let port = Self { - port: Self::register(jack, name)?, - jack: jack.clone(), - name: name.as_ref().into(), - connections: connect.to_vec(), - held: Arc::new(RwLock::new([false;128])) - }; - port.connect_to_matching()?; - Ok(port) - } -} - -impl MidiInput { - pub fn parsed <'a> (&'a self, scope: &'a ProcessScope) -> impl Iterator, &'a [u8])> { - parse_midi_input(self.port().iter(scope)) - } -} - -impl>> HasMidiIns for T { - fn midi_ins (&self) -> &Vec { - self.get() - } - fn midi_ins_mut (&mut self) -> &mut Vec { - self.get_mut() - } -} - -/// Trait for thing that may receive MIDI. -pub trait HasMidiIns { - fn midi_ins (&self) -> &Vec; - fn midi_ins_mut (&mut self) -> &mut Vec; - /// Collect MIDI input from app ports (TODO preallocate large buffers) - fn midi_input_collect <'a> (&'a self, scope: &'a ProcessScope) -> CollectedMidiInput<'a> { - self.midi_ins().iter() - .map(|port|port.port().iter(scope) - .map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes))) - .collect::>()) - .collect::>() - } - fn midi_ins_with_sizes <'a> (&'a self) -> - impl Iterator, &'a [Connect], usize, usize)> + Send + Sync + 'a - { - let mut y = 0; - self.midi_ins().iter().enumerate().map(move|(i, input)|{ - let height = 1 + input.connections().len(); - let data = (i, input.port_name(), input.connections(), y, y + height); - y += height; - data - }) - } -} - -pub type CollectedMidiInput<'a> = Vec, MidiError>)>>; - -impl> AddMidiIn for T { - fn midi_in_add (&mut self) -> Usually<()> { - let index = self.midi_ins().len(); - let port = MidiInput::new(self.jack(), &format!("M/{index}"), &[])?; - self.midi_ins_mut().push(port); - Ok(()) - } -} - -/// May create new MIDI input ports. -pub trait AddMidiIn { - fn midi_in_add (&mut self) -> Usually<()>; -} - -impl HasJack<'static> for MidiOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } - -impl JackPort for MidiOutput { - type Port = MidiOut; - type Pair = MidiIn; - fn port_name (&self) -> &Arc { - &self.name - } - fn port (&self) -> &Port { - &self.port - } - fn port_mut (&mut self) -> &mut Port { - &mut self.port - } - fn into_port (self) -> Port { - self.port - } - fn connections (&self) -> &[Connect] { - self.connections.as_slice() - } - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized - { - let port = Self::register(jack, name)?; - let jack = jack.clone(); - let name = name.as_ref().into(); - let connections = connect.to_vec(); - let port = Self { - jack, - port, - name, - connections, - held: Arc::new([false;128].into()), - note_buffer: vec![0;8], - output_buffer: vec![vec![];65536], - }; - port.connect_to_matching()?; - Ok(port) - } -} - -impl MidiOutput { - /// Clear the section of the output buffer that we will be using, - /// emitting "all notes off" at start of buffer if requested. - pub fn buffer_clear (&mut self, scope: &ProcessScope, reset: bool) { - let n_frames = (scope.n_frames() as usize).min(self.output_buffer.len()); - for frame in &mut self.output_buffer[0..n_frames] { - frame.clear(); - } - if reset { - all_notes_off(&mut self.output_buffer); - } - } - /// Write a note to the output buffer - pub fn buffer_write <'a> ( - &'a mut self, - sample: usize, - event: LiveEvent, - ) { - self.note_buffer.fill(0); - event.write(&mut self.note_buffer).expect("failed to serialize MIDI event"); - self.output_buffer[sample].push(self.note_buffer.clone()); - // Update the list of currently held notes. - if let LiveEvent::Midi { ref message, .. } = event { - update_keys(&mut*self.held.write().unwrap(), message); - } - } - /// Write a chunk of MIDI data from the output buffer to the output port. - pub fn buffer_emit (&mut self, scope: &ProcessScope) { - let samples = scope.n_frames() as usize; - let mut writer = self.port.writer(scope); - for (time, events) in self.output_buffer.iter().enumerate().take(samples) { - for bytes in events.iter() { - writer.write(&RawMidi { time: time as u32, bytes }).unwrap_or_else(|_|{ - panic!("Failed to write MIDI data: {bytes:?}"); - }); - } - } - } -} - - -impl>> HasMidiOuts for T { - fn midi_outs (&self) -> &Vec { - self.get() - } - fn midi_outs_mut (&mut self) -> &mut Vec { - self.get_mut() - } -} - - -/// Trait for thing that may output MIDI. -pub trait HasMidiOuts { - fn midi_outs (&self) -> &Vec; - fn midi_outs_mut (&mut self) -> &mut Vec; - fn midi_outs_with_sizes <'a> (&'a self) -> - impl Iterator, &'a [Connect], usize, usize)> + Send + Sync + 'a - { - let mut y = 0; - self.midi_outs().iter().enumerate().map(move|(i, output)|{ - let height = 1 + output.connections().len(); - let data = (i, output.port_name(), output.connections(), y, y + height); - y += height; - data - }) - } - fn midi_outs_emit (&mut self, scope: &ProcessScope) { - for port in self.midi_outs_mut().iter_mut() { - port.buffer_emit(scope) - } - } -} - -/// Trail for thing that may gain new MIDI ports. -impl> AddMidiOut for T { - fn midi_out_add (&mut self) -> Usually<()> { - let index = self.midi_outs().len(); - let port = MidiOutput::new(self.jack(), &format!("{index}/M"), &[])?; - self.midi_outs_mut().push(port); - Ok(()) - } -} - -/// May create new MIDI output ports. -pub trait AddMidiOut { - fn midi_out_add (&mut self) -> Usually<()>; -} - -impl HasJack<'static> for AudioInput { fn jack (&self) -> &Jack<'static> { &self.jack } } - -impl JackPort for AudioInput { - type Port = AudioIn; - type Pair = AudioOut; - fn port_name (&self) -> &Arc { - &self.name - } - fn port (&self) -> &Port { - &self.port - } - fn port_mut (&mut self) -> &mut Port { - &mut self.port - } - fn into_port (self) -> Port { - self.port - } - fn connections (&self) -> &[Connect] { - self.connections.as_slice() - } - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized - { - let port = Self { - port: Self::register(jack, name)?, - jack: jack.clone(), - name: name.as_ref().into(), - connections: connect.to_vec() - }; - port.connect_to_matching()?; - Ok(port) - } -} - -impl HasJack<'static> for AudioOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } - -impl JackPort for AudioOutput { - type Port = AudioOut; - type Pair = AudioIn; - fn port_name (&self) -> &Arc { - &self.name - } - fn port (&self) -> &Port { - &self.port - } - fn port_mut (&mut self) -> &mut Port { - &mut self.port - } - fn into_port (self) -> Port { - self.port - } - fn connections (&self) -> &[Connect] { - self.connections.as_slice() - } - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized - { - let port = Self { - port: Self::register(jack, name)?, - jack: jack.clone(), - name: name.as_ref().into(), - connections: connect.to_vec() - }; - port.connect_to_matching()?; - Ok(port) - } -} diff --git a/device/sampler.rs b/device/sampler.rs deleted file mode 100644 index 77aea1b2..00000000 --- a/device/sampler.rs +++ /dev/null @@ -1,1116 +0,0 @@ -//! ``` -//! let sample = Sample::new("test", 0, 0, vec![]); -//! ``` - -use crate::*; - -/// The sampler device plays sounds in response to MIDI notes. -#[derive(Debug)] -pub struct Sampler { - /// Name of sampler. - pub name: Arc, - /// Device color. - pub color: ItemTheme, - /// Audio input ports. Samples get recorded here. - #[cfg(feature = "port")] pub audio_ins: Vec, - /// Audio input meters. - #[cfg(feature = "meter")] pub input_meters: Vec, - /// Sample currently being recorded. - pub recording: Option<(usize, Option>>)>, - /// Recording buffer. - pub buffer: Vec>, - /// Samples mapped to MIDI notes. - pub mapped: [Option>>;128], - /// Samples that are not mapped to MIDI notes. - pub unmapped: Vec>>, - /// Sample currently being edited. - pub editing: Option>>, - /// MIDI input port. Triggers sample playback. - #[cfg(feature = "port")] pub midi_in: MidiInput, - /// Collection of currently playing instances of samples. - pub voices: Arc>>, - /// Audio output ports. Voices get played here. - #[cfg(feature = "port")] pub audio_outs: Vec, - /// Audio output meters. - #[cfg(feature = "meter")] pub output_meters: Vec, - /// How to mix the voices. - pub mixing_mode: MixingMode, - /// How to meter the inputs and outputs. - pub metering_mode: MeteringMode, - /// Fixed gain applied to all output. - pub output_gain: f32, - /// Currently active modal, if any. - pub mode: Option, - /// Size of rendered sampler. - pub size: Measure, - /// Lowest note displayed. - pub note_lo: AtomicUsize, - /// Currently selected note. - pub note_pt: AtomicUsize, - /// Selected note as row/col. - pub cursor: (AtomicUsize, AtomicUsize), -} - -impl Sampler { - pub fn new ( - jack: &Jack<'static>, - name: impl AsRef, - #[cfg(feature = "port")] midi_from: &[Connect], - #[cfg(feature = "port")] audio_from: &[&[Connect];2], - #[cfg(feature = "port")] audio_to: &[&[Connect];2], - ) -> Usually { - let name = name.as_ref(); - Ok(Self { - name: name.into(), - #[cfg(feature = "port")] midi_in: MidiInput::new(jack, &format!("M/{name}"), midi_from)?, - #[cfg(feature = "port")] audio_ins: vec![ - AudioInput::new(jack, &format!("L/{name}"), audio_from[0])?, - AudioInput::new(jack, &format!("R/{name}"), audio_from[1])?, - ], - #[cfg(feature = "port")] audio_outs: vec![ - AudioOutput::new(jack, &format!("{name}/L"), audio_to[0])?, - AudioOutput::new(jack, &format!("{name}/R"), audio_to[1])?, - ], - input_meters: vec![0.0;2], - output_meters: vec![0.0;2], - mapped: [const { None };128], - unmapped: vec![], - voices: Arc::new(RwLock::new(vec![])), - buffer: vec![vec![0.0;16384];2], - output_gain: 1., - recording: None, - mode: None, - editing: None, - size: Default::default(), - note_lo: 0.into(), - note_pt: 0.into(), - cursor: (0.into(), 0.into()), - color: Default::default(), - mixing_mode: Default::default(), - metering_mode: Default::default(), - }) - } - /// Value of cursor - pub fn cursor (&self) -> (usize, usize) { - (self.cursor.0.load(Relaxed), self.cursor.1.load(Relaxed)) - } -} - -impl NoteRange for Sampler { - fn note_lo (&self) -> &AtomicUsize { - &self.note_lo - } - fn note_axis (&self) -> &AtomicUsize { - &self.size.y - } -} - -impl NotePoint for Sampler { - fn note_len (&self) -> &AtomicUsize { - unreachable!(); - } - fn get_note_len (&self) -> usize { - 0 - } - fn set_note_len (&self, x: usize) -> usize { - 0 /*TODO?*/ - } - fn note_pos (&self) -> &AtomicUsize { - &self.note_pt - } - fn get_note_pos (&self) -> usize { - self.note_pt.load(Relaxed) - } - fn set_note_pos (&self, x: usize) -> usize { - let old = self.note_pt.swap(x, Relaxed); - self.cursor.0.store(x % 8, Relaxed); - self.cursor.1.store(x / 8, Relaxed); - old - } -} - -/// A sound sample. -#[derive(Default, Debug)] -pub struct Sample { - pub name: Arc, - pub start: usize, - pub end: usize, - pub channels: Vec>, - pub rate: Option, - pub gain: f32, - pub color: ItemTheme, -} - -impl Sample { - pub fn new (name: impl AsRef, start: usize, end: usize, channels: Vec>) -> Self { - Self { - name: name.as_ref().into(), - start, - end, - channels, - rate: None, - gain: 1.0, - color: ItemTheme::random(), - } - } - pub fn play (sample: &Arc>, after: usize, velocity: &u7) -> Voice { - Voice { - sample: sample.clone(), - after, - position: sample.read().unwrap().start, - velocity: velocity.as_int() as f32 / 127.0, - } - } -} - -/// A currently playing instance of a sample. -#[derive(Default, Debug, Clone)] -pub struct Voice { - pub sample: Arc>, - pub after: usize, - pub position: usize, - pub velocity: f32, -} - -#[derive(Debug)] -pub enum SamplerMode { - // Load sample from path - Import(usize, Browse), -} - -impl Sample { - - /// Read WAV from file - pub fn read_data (src: &str) -> Usually<(usize, Vec>)> { - let mut channels: Vec> = vec![]; - for channel in wavers::Wav::from_path(src)?.channels() { - channels.push(channel); - } - let mut end = 0; - let mut data: Vec> = vec![]; - for samples in channels.iter() { - let channel = Vec::from(samples.as_ref()); - end = end.max(channel.len()); - data.push(channel); - } - Ok((end, data)) - } - - pub fn from_file (path: &PathBuf) -> Usually { - let name = path.file_name().unwrap().to_string_lossy().into(); - let mut sample = Self { name, ..Default::default() }; - // Use file extension if present - let mut hint = Hint::new(); - if let Some(ext) = path.extension() { - hint.with_extension(&ext.to_string_lossy()); - } - let probed = symphonia::default::get_probe().format( - &hint, - MediaSourceStream::new( - Box::new(File::open(path)?), - Default::default(), - ), - &Default::default(), - &Default::default() - )?; - let mut format = probed.format; - let params = &format.tracks().iter() - .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) - .expect("no tracks found") - .codec_params; - let mut decoder = get_codecs().make(params, &Default::default())?; - loop { - match format.next_packet() { - Ok(packet) => sample.decode_packet(&mut decoder, packet)?, - Err(symphonia::core::errors::Error::IoError(_)) => break decoder.last_decoded(), - Err(err) => return Err(err.into()), - }; - }; - sample.end = sample.channels.iter().fold(0, |l, c|l + c.len()); - Ok(sample) - } - - fn decode_packet ( - &mut self, decoder: &mut Box, packet: Packet - ) -> Usually<()> { - // Decode a packet - let decoded = decoder - .decode(&packet) - .map_err(|e|Box::::from(e))?; - // Determine sample rate - let spec = *decoded.spec(); - if let Some(rate) = self.rate { - if rate != spec.rate as usize { - panic!("sample rate changed"); - } - } else { - self.rate = Some(spec.rate as usize); - } - // Determine channel count - while self.channels.len() < spec.channels.count() { - self.channels.push(vec![]); - } - // Load sample - let mut samples = SampleBuffer::new( - decoded.frames() as u64, - spec - ); - if samples.capacity() > 0 { - samples.copy_interleaved_ref(decoded); - for frame in samples.samples().chunks(spec.channels.count()) { - for (chan, frame) in frame.iter().enumerate() { - self.channels[chan].push(*frame) - } - } - } - Ok(()) - } - -} - -pub type MidiSample = (Option, Arc>); - -impl Sampler { - - /// Create [Voice]s from [Sample]s in response to MIDI input. - pub fn process_midi_in (&mut self, scope: &ProcessScope) { - let Sampler { midi_in, mapped, voices, .. } = self; - for RawMidi { time, bytes } in midi_in.port().iter(scope) { - if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() { - match message { - MidiMessage::NoteOn { ref key, ref vel } => { - if let Some(ref sample) = mapped[key.as_int() as usize] { - voices.write().unwrap().push(Sample::play(sample, time as usize, vel)); - } - }, - MidiMessage::Controller { controller: _, value: _ } => { - // TODO - } - _ => {} - } - } - } - } - -} - -impl Sample { - pub fn handle_cc (&mut self, controller: u7, value: u7) { - let percentage = value.as_int() as f64 / 127.; - match controller.as_int() { - 20 => { - self.start = (percentage * self.end as f64) as usize; - }, - 21 => { - let length = self.channels[0].len(); - self.end = length.min( - self.start + (percentage * (length as f64 - self.start as f64)) as usize - ); - }, - 22 => { /*attack*/ }, - 23 => { /*decay*/ }, - 24 => { - self.gain = percentage as f32 * 2.0; - }, - 26 => { /* pan */ } - 25 => { /* pitch */ } - _ => {} - } - } -} - -// TODO: -//for port in midi_in.iter() { - //for event in port.iter() { - //match event { - //(time, Ok(LiveEvent::Midi {message, ..})) => match message { - //MidiMessage::NoteOn {ref key, ..} if let Some(editor) = self.editor.as_ref() => { - //editor.set_note_pos(key.as_int() as usize); - //}, - //MidiMessage::Controller {controller, value} if let (Some(editor), Some(sampler)) = ( - //self.editor.as_ref(), - //self.sampler.as_ref(), - //) => { - //// TODO: give sampler its own cursor - //if let Some(sample) = &sampler.mapped[editor.note_pos()] { - //sample.write().unwrap().handle_cc(*controller, *value) - //} - //} - //_ =>{} - //}, - //_ =>{} - //} - //} -//} - -audio!(|self: Sampler, _client, scope|{ - self.process_midi_in(scope); - self.process_audio_out(scope); - self.process_audio_in(scope); - Control::Continue -}); - -impl Sampler { - - pub fn process_audio_in (&mut self, scope: &ProcessScope) { - self.reset_input_meters(); - if self.recording.is_some() { - self.record_into(scope); - } else { - self.update_input_meters(scope); - } - } - - /// Make sure that input meter count corresponds to input channel count - fn reset_input_meters (&mut self) { - let channels = self.audio_ins.len(); - if self.input_meters.len() != channels { - self.input_meters = vec![f32::MIN;channels]; - } - } - - /// Record from inputs to sample - fn record_into (&mut self, scope: &ProcessScope) { - if let Some(ref sample) = self.recording.as_ref().expect("no recording sample").1 { - let mut sample = sample.write().unwrap(); - if sample.channels.len() != self.audio_ins.len() { - panic!("channel count mismatch"); - } - let samples_with_meters = self.audio_ins.iter() - .zip(self.input_meters.iter_mut()) - .zip(sample.channels.iter_mut()); - let mut length = 0; - for ((input, meter), channel) in samples_with_meters { - let slice = input.port().as_slice(scope); - length = length.max(slice.len()); - *meter = to_rms(slice); - channel.extend_from_slice(slice); - } - sample.end += length; - } else { - panic!("tried to record into the void") - } - } - - /// Update input meters - fn update_input_meters (&mut self, scope: &ProcessScope) { - for (input, meter) in self.audio_ins.iter().zip(self.input_meters.iter_mut()) { - let slice = input.port().as_slice(scope); - *meter = to_rms(slice); - } - } - - /// Make sure that output meter count corresponds to input channel count - fn reset_output_meters (&mut self) { - let channels = self.audio_outs.len(); - if self.output_meters.len() != channels { - self.output_meters = vec![f32::MIN;channels]; - } - } - - /// Mix all currently playing samples into the output. - pub fn process_audio_out (&mut self, scope: &ProcessScope) { - self.clear_output_buffer(); - self.populate_output_buffer(scope.n_frames() as usize); - self.write_output_buffer(scope); - } - - /// Zero the output buffer. - fn clear_output_buffer (&mut self) { - for buffer in self.buffer.iter_mut() { - buffer.fill(0.0); - } - } - - /// Write playing voices to output buffer - fn populate_output_buffer (&mut self, frames: usize) { - let Sampler { buffer, voices, output_gain, mixing_mode, .. } = self; - let channel_count = buffer.len(); - match mixing_mode { - MixingMode::Summing => voices.write().unwrap().retain_mut(|voice|{ - mix_summing(buffer.as_mut_slice(), *output_gain, frames, ||voice.next()) - }), - MixingMode::Average => voices.write().unwrap().retain_mut(|voice|{ - mix_average(buffer.as_mut_slice(), *output_gain, frames, ||voice.next()) - }), - } - } - - /// Write output buffer to output ports. - fn write_output_buffer (&mut self, scope: &ProcessScope) { - let Sampler { audio_outs, buffer, .. } = self; - for (i, port) in audio_outs.iter_mut().enumerate() { - let buffer = &buffer[i]; - for (i, value) in port.port_mut().as_mut_slice(scope).iter_mut().enumerate() { - *value = *buffer.get(i).unwrap_or(&0.0); - } - } - } - -} - -impl Iterator for Voice { - type Item = [f32;2]; - fn next (&mut self) -> Option { - if self.after > 0 { - self.after -= 1; - return Some([0.0, 0.0]) - } - let sample = self.sample.read().unwrap(); - if self.position < sample.end { - let position = self.position; - self.position += 1; - return sample.channels[0].get(position).map(|_amplitude|[ - sample.channels[0][position] * self.velocity * sample.gain, - sample.channels[0][position] * self.velocity * sample.gain, - ]) - } - None - } -} - -def_command!(SamplerCommand: |sampler: Sampler| { - RecordToggle { slot: usize } => { - let slot = *slot; - let recording = sampler.recording.as_ref().map(|x|x.0); - let _ = Self::RecordFinish.execute(sampler)?; - // autoslice: continue recording at next slot - if recording != Some(slot) { - Self::RecordBegin { slot }.execute(sampler) - } else { - Ok(None) - } - }, - RecordBegin { slot: usize } => { - let slot = *slot; - sampler.recording = Some(( - slot, - Some(Arc::new(RwLock::new(Sample::new( - "Sample", 0, 0, vec![vec![];sampler.audio_ins.len()] - )))) - )); - Ok(None) - }, - RecordFinish => { - let _prev_sample = sampler.recording.as_mut().map(|(index, sample)|{ - std::mem::swap(sample, &mut sampler.mapped[*index]); - sample - }); // TODO: undo - Ok(None) - }, - RecordCancel => { - sampler.recording = None; - Ok(None) - }, - PlaySample { slot: usize } => { - let slot = *slot; - if let Some(ref sample) = sampler.mapped[slot] { - sampler.voices.write().unwrap().push(Sample::play(sample, 0, &u7::from(128))); - } - Ok(None) - }, - StopSample { slot: usize } => { - let slot = *slot; - todo!(); - Ok(None) - }, -}); - -def_command!(FileBrowserCommand: |sampler: Sampler|{ - //("begin" [] Some(Self::Begin)) - //("cancel" [] Some(Self::Cancel)) - //("confirm" [] Some(Self::Confirm)) - //("select" [i: usize] Some(Self::Select(i.expect("no index")))) - //("chdir" [p: PathBuf] Some(Self::Chdir(p.expect("no path")))) - //("filter" [f: Arc] Some(Self::Filter(f.expect("no filter"))))) -}); - -impl Sampler { - fn sample_selected (&self) -> usize { - (self.get_note_pos() as u8).into() - } - fn sample_selected_pitch (&self) -> u7 { - (self.get_note_pos() as u8).into() - } -} - -pub struct AddSampleModal { - exited: bool, - dir: PathBuf, - subdirs: Vec, - files: Vec, - cursor: usize, - offset: usize, - sample: Arc>, - voices: Arc>>, - _search: Option, -} - -impl AddSampleModal { - fn exited (&self) -> bool { - self.exited - } - fn exit (&mut self) { - self.exited = true - } -} - -impl AddSampleModal { - pub fn new ( - sample: &Arc>, - voices: &Arc>> - ) -> Usually { - let dir = std::env::current_dir()?; - let (subdirs, files) = scan(&dir)?; - Ok(Self { - exited: false, - dir, - subdirs, - files, - cursor: 0, - offset: 0, - sample: sample.clone(), - voices: voices.clone(), - _search: None - }) - } - fn rescan (&mut self) -> Usually<()> { - scan(&self.dir).map(|(subdirs, files)|{ - self.subdirs = subdirs; - self.files = files; - }) - } - fn prev (&mut self) { - self.cursor = self.cursor.saturating_sub(1); - } - fn next (&mut self) { - self.cursor = self.cursor + 1; - } - fn try_preview (&mut self) -> Usually<()> { - if let Some(path) = self.cursor_file() { - if let Ok(sample) = Sample::from_file(&path) { - *self.sample.write().unwrap() = sample; - self.voices.write().unwrap().push( - Sample::play(&self.sample, 0, &u7::from(100u8)) - ); - } - //load_sample(&path)?; - //let src = std::fs::File::open(&path)?; - //let mss = MediaSourceStream::new(Box::new(src), Default::default()); - //let mut hint = Hint::new(); - //if let Some(ext) = path.extension() { - //hint.with_extension(&ext.to_string_lossy()); - //} - //let meta_opts: MetadataOptions = Default::default(); - //let fmt_opts: FormatOptions = Default::default(); - //if let Ok(mut probed) = symphonia::default::get_probe() - //.format(&hint, mss, &fmt_opts, &meta_opts) - //{ - //panic!("{:?}", probed.format.metadata()); - //}; - } - Ok(()) - } - fn cursor_dir (&self) -> Option { - if self.cursor < self.subdirs.len() { - Some(self.dir.join(&self.subdirs[self.cursor])) - } else { - None - } - } - fn cursor_file (&self) -> Option { - if self.cursor < self.subdirs.len() { - return None - } - let index = self.cursor.saturating_sub(self.subdirs.len()); - if index < self.files.len() { - Some(self.dir.join(&self.files[index])) - } else { - None - } - } - fn pick (&mut self) -> Usually { - if self.cursor == 0 { - if let Some(parent) = self.dir.parent() { - self.dir = parent.into(); - self.rescan()?; - self.cursor = 0; - return Ok(false) - } - } - if let Some(dir) = self.cursor_dir() { - self.dir = dir; - self.rescan()?; - self.cursor = 0; - return Ok(false) - } - if let Some(path) = self.cursor_file() { - let (end, channels) = read_sample_data(&path.to_string_lossy())?; - let mut sample = self.sample.write().unwrap(); - sample.name = path.file_name().unwrap().to_string_lossy().into(); - sample.end = end; - sample.channels = channels; - return Ok(true) - } - return Ok(false) - } -} - -fn read_sample_data (_: &str) -> Usually<(usize, Vec>)> { - todo!(); -} - -fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { - let (mut subdirs, mut files) = std::fs::read_dir(dir)? - .fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{ - let entry = entry.expect("failed to read drectory entry"); - let meta = entry.metadata().expect("failed to read entry metadata"); - if meta.is_file() { - files.push(entry.file_name()); - } else if meta.is_dir() { - subdirs.push(entry.file_name()); - } - (subdirs, files) - }); - subdirs.sort(); - files.sort(); - Ok((subdirs, files)) -} - -impl Draw for AddSampleModal { - fn draw (&self, _to: &mut TuiOut) { - todo!() - //let area = to.area(); - //to.make_dim(); - //let area = center_box( - //area, - //64.max(area.w().saturating_sub(8)), - //20.max(area.w().saturating_sub(8)), - //); - //to.fill_fg(area, Color::Reset); - //to.fill_bg(area, Nord::bg_lo(true, true)); - //to.fill_char(area, ' '); - //to.blit(&format!("{}", &self.dir.to_string_lossy()), area.x()+2, area.y()+1, Some(Style::default().bold()))?; - //to.blit(&"Select sample:", area.x()+2, area.y()+2, Some(Style::default().bold()))?; - //for (i, (is_dir, name)) in self.subdirs.iter() - //.map(|path|(true, path)) - //.chain(self.files.iter().map(|path|(false, path))) - //.enumerate() - //.skip(self.offset) - //{ - //if i >= area.h() as usize - 4 { - //break - //} - //let t = if is_dir { "" } else { "" }; - //let line = format!("{t} {}", name.to_string_lossy()); - //let line = &line[..line.len().min(area.w() as usize - 4)]; - //to.blit(&line, area.x() + 2, area.y() + 3 + i as u16, Some(if i == self.cursor { - //Style::default().green() - //} else { - //Style::default().white() - //}))?; - //} - //Lozenge(Style::default()).draw(to) - } -} - -impl Sampler { - - pub fn view_grid (&self) -> impl Content + use<'_> { - //let cells_x = 8u16; - //let cells_y = 8u16; - //let cell_width = 10u16; - //let cell_height = 2u16; - //let width = cells_x * cell_width; - //let height = cells_y * cell_height; - //let cols = Map::east( - //cell_width, - //move||0..cells_x, - //move|x, _|Map::south( - //cell_height, - //move||0..cells_y, - //move|y, _|self.view_grid_cell("........", x, y, cell_width, cell_height) - //) - //); - //cols - //Thunk::new(|to: &mut TuiOut|{ - //}) - "TODO" - } - - pub fn view_grid_cell <'a> ( - &'a self, name: &'a str, x: u16, y: u16, w: u16, h: u16 - ) -> impl Content + use<'a> { - let cursor = self.cursor(); - let hi_fg = Color::Rgb(64, 64, 64); - let hi_bg = if y == 0 { Color::Reset } else { Color::Rgb(64, 64, 64) /*prev*/ }; - let tx_fg = if let Some((index, _)) = self.recording - && index % 8 == x as usize - && index / 8 == y as usize - { - Color::Rgb(255, 64, 0) - } else { - Color::Rgb(255, 255, 255) - }; - let tx_bg = if x as usize == cursor.0 && y as usize == cursor.1 { - Color::Rgb(96, 96, 96) - } else { - Color::Rgb(64, 64, 64) - }; - let lo_fg = Color::Rgb(64, 64, 64); - let lo_bg = if y == 7 { Color::Reset } else { tx_bg }; - Fixed::XY(w, h, Bsp::s( - Fixed::Y(1, Tui::fg_bg(hi_fg, hi_bg, RepeatH(Phat::<()>::LO))), - Bsp::n( - Fixed::Y(1, Tui::fg_bg(lo_fg, lo_bg, RepeatH(Phat::<()>::HI))), - Fill::X(Fixed::Y(1, Tui::fg_bg(tx_fg, tx_bg, name))), - ), - )) - } - - const _EMPTY: &[(f64, f64)] = &[(0., 0.), (1., 1.), (2., 2.), (0., 2.), (2., 0.)]; - - pub fn view_list <'a, T: NotePoint + NoteRange> ( - &'a self, compact: bool, editor: &T - ) -> impl Content + 'a { - let note_lo = editor.get_note_lo(); - let note_pt = editor.get_note_pos(); - let note_hi = editor.get_note_hi(); - Fixed::X(if compact { 4 } else { 12 }, Map::south( - 1, - move||(note_lo..=note_hi).rev(), - move|note, _index| { - //let offset = |a|Push::y(i as u16, Align::n(Fixed::Y(1, Fill::X(a)))); - let mut bg = if note == note_pt { Tui::g(64) } else { Color::Reset }; - let mut fg = Tui::g(160); - if let Some(mapped) = &self.mapped[note] { - let sample = mapped.read().unwrap(); - fg = if note == note_pt { - sample.color.lightest.rgb - } else { - Tui::g(224) - }; - bg = if note == note_pt { - sample.color.light.rgb - } else { - sample.color.base.rgb - }; - } - if let Some((index, _)) = self.recording { - if note == index { - bg = if note == note_pt { Color::Rgb(96,24,0) } else { Color::Rgb(64,16,0) }; - fg = Color::Rgb(224,64,32) - } - } - Tui::fg_bg(fg, bg, format!("{note:3} {}", self.view_list_item(note, compact))) - })) - } - - pub fn view_list_item (&self, note: usize, compact: bool) -> String { - if compact { - String::default() - } else { - draw_list_item(&self.mapped[note]) - } - } - - pub fn view_sample (&self, note_pt: usize) -> impl Content + use<'_> { - Outer(true, Style::default().fg(Tui::g(96))) - .enclose(Fill::XY(draw_viewer(if let Some((_, Some(sample))) = &self.recording { - Some(sample) - } else if let Some(sample) = &self.mapped[note_pt] { - Some(sample) - } else { - None - }))) - } - - pub fn view_sample_info (&self, note_pt: usize) -> impl Content + use<'_> { - Fill::X(Fixed::Y(1, draw_info(if let Some((_, Some(sample))) = &self.recording { - Some(sample) - } else if let Some(sample) = &self.mapped[note_pt] { - Some(sample) - } else { - None - }))) - } - - pub fn view_sample_status (&self, note_pt: usize) -> impl Content + use<'_> { - Fixed::X(20, draw_info_v(if let Some((_, Some(sample))) = &self.recording { - Some(sample) - } else if let Some(sample) = &self.mapped[note_pt] { - Some(sample) - } else { - None - })) - } - - pub fn view_status (&self, index: usize) -> impl Content { - draw_status(self.mapped[index].as_ref()) - } - - pub fn view_meters_input (&self) -> impl Content + use<'_> { - draw_meters(&self.input_meters) - } - - pub fn view_meters_output (&self) -> impl Content + use<'_> { - draw_meters(&self.output_meters) - } -} - -fn draw_meters (meters: &[f32]) -> impl Content + use<'_> { - Tui::bg(Black, Fixed::X(2, Map::east(1, ||meters.iter(), |value, _index|{ - Fill::Y(RmsMeter(*value)) - }))) -} - -fn draw_list_item (sample: &Option>>) -> String { - if let Some(sample) = sample { - let sample = sample.read().unwrap(); - format!("{:8}", sample.name) - //format!("{:8} {:3} {:6}-{:6}/{:6}", - //sample.name, - //sample.gain, - //sample.start, - //sample.end, - //sample.channels[0].len() - //) - } else { - String::from("........") - } -} - -fn draw_viewer (sample: Option<&Arc>>) -> impl Content + use<'_> { - let min_db = -64.0; - Thunk::new(move|to: &mut TuiOut|{ - let [x, y, width, height] = to.area(); - let area = Rect { x, y, width, height }; - if let Some(sample) = &sample { - let sample = sample.read().unwrap(); - let start = sample.start as f64; - let end = sample.end as f64; - let length = end - start; - let step = length / width as f64; - let mut t = start; - let mut lines = vec![]; - while t < end { - let chunk = &sample.channels[0][t as usize..((t + step) as usize).min(sample.end)]; - let total: f32 = chunk.iter().map(|x|x.abs()).sum(); - let count = chunk.len() as f32; - let meter = 10. * (total / count).log10(); - let x = t as f64; - let y = meter as f64; - lines.push(Line::new(x, min_db, x, y, Color::Green)); - t += step / 2.; - } - Canvas::default() - .x_bounds([sample.start as f64, sample.end as f64]) - .y_bounds([min_db, 0.]) - .paint(|ctx| { - for line in lines.iter() { - ctx.draw(line); - } - //FIXME: proportions - //let text = "press record to finish sampling"; - //ctx.print( - //(width - text.len() as u16) as f64 / 2.0, - //height as f64 / 2.0, - //text.red() - //); - }).render(area, &mut to.buffer); - } else { - Canvas::default() - .x_bounds([0.0, width as f64]) - .y_bounds([0.0, height as f64]) - .paint(|_ctx| { - //let text = "press record to begin sampling"; - //ctx.print( - //(width - text.len() as u16) as f64 / 2.0, - //height as f64 / 2.0, - //text.red() - //); - }) - .render(area, &mut to.buffer); - } - }) -} - -fn draw_info (sample: Option<&Arc>>) -> impl Content + use<'_> { - When::new(sample.is_some(), Thunk::new(move|to: &mut TuiOut|{ - let sample = sample.unwrap().read().unwrap(); - let theme = sample.color; - to.place(&row!( - FieldH(theme, "Name", format!("{:<10}", sample.name.clone())), - FieldH(theme, "Length", format!("{:<8}", sample.channels[0].len())), - FieldH(theme, "Start", format!("{:<8}", sample.start)), - FieldH(theme, "End", format!("{:<8}", sample.end)), - FieldH(theme, "Trans", "0"), - FieldH(theme, "Gain", format!("{}", sample.gain)), - )) - })) -} - -fn draw_info_v (sample: Option<&Arc>>) -> impl Content + use<'_> { - Either::new(sample.is_some(), Thunk::new(move|to: &mut TuiOut|{ - let sample = sample.unwrap().read().unwrap(); - let theme = sample.color; - to.place(&Fixed::X(20, col!( - Fill::X(Align::w(FieldH(theme, "Name ", format!("{:<10}", sample.name.clone())))), - Fill::X(Align::w(FieldH(theme, "Length", format!("{:<8}", sample.channels[0].len())))), - Fill::X(Align::w(FieldH(theme, "Start ", format!("{:<8}", sample.start)))), - Fill::X(Align::w(FieldH(theme, "End ", format!("{:<8}", sample.end)))), - Fill::X(Align::w(FieldH(theme, "Trans ", "0"))), - Fill::X(Align::w(FieldH(theme, "Gain ", format!("{}", sample.gain)))), - ))) - }), Thunk::new(|to: &mut TuiOut|to.place(&Tui::fg(Red, col!( - Tui::bold(true, "× No sample."), - "[r] record", - "[Shift-F9] import", - ))))) -} - -fn draw_status (sample: Option<&Arc>>) -> impl Content { - Tui::bold(true, Tui::fg(Tui::g(224), sample - .map(|sample|{ - let sample = sample.read().unwrap(); - format!("Sample {}-{}", sample.start, sample.end) - }) - .unwrap_or_else(||"No sample".to_string()))) -} - -fn draw_sample ( - to: &mut TuiOut, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool -) -> Usually { - let style = if focus { Style::default().green() } else { Style::default() }; - if focus { - to.blit(&"🬴", x+1, y, Some(style.bold())); - } - let label1 = format!("{:3} {:12}", - note.map(|n|n.to_string()).unwrap_or(String::default()), - sample.name); - let label2 = format!("{:>6} {:>6} +0.0", - sample.start, - sample.end); - to.blit(&label1, x+2, y, Some(style.bold())); - to.blit(&label2, x+3+label1.len()as u16, y, Some(style)); - Ok(label1.len() + label2.len() + 4) -} - //fn file_browser_filter (&self) -> Arc { - //todo!() - //} - //fn file_browser_path (&self) -> PathBuf { - //todo!(); - //} - ///// Immutable reference to sample at cursor. - //fn sample_selected (&self) -> Option>> { - //for (i, sample) in self.mapped.iter().enumerate() { - //if i == self.cursor().0 { - //return sample.as_ref() - //} - //} - //for (i, sample) in self.unmapped.iter().enumerate() { - //if i + self.mapped.len() == self.cursor().0 { - //return Some(sample) - //} - //} - //None - //} - //fn sample_gain (&self) -> f32 { - //todo!() - //} - //fn sample_above () -> usize { - //self.note_pos().min(119) + 8 - //} - //fn sample_below () -> usize { - //self.note_pos().max(8) - 8 - //} - //fn sample_to_left () -> usize { - //self.note_pos().min(126) + 1 - //} - //fn sample_to_right () -> usize { - //self.note_pos().max(1) - 1 - //} - //fn selected_pitch () -> u7 { - //(self.note_pos() as u8).into() // TODO - //} - - //select (&self, state: &mut Sampler, i: usize) -> Option { - //Self::Select(state.set_note_pos(i)) - //} - ///// Assign sample to slot - //set (&self, slot: u7, sample: Option>>) -> Option { - //let i = slot.as_int() as usize; - //let old = self.mapped[i].clone(); - //self.mapped[i] = sample; - //Some(Self::Set(old)) - //} - //set_start (&self, state: &mut Sampler, slot: u7, frame: usize) -> Option { - //todo!() - //} - //set_gain (&self, state: &mut Sampler, slot: u7, g: f32) -> Option { - //todo!() - //} - //note_on (&self, state: &mut Sampler, slot: u7, v: u7) -> Option { - //todo!() - //} - //note_off (&self, state: &mut Sampler, slot: u7) -> Option { - //todo!() - //} - //set_sample (&self, state: &mut Sampler, slot: u7, s: Option>>) -> Option { - //Some(Self::SetSample(p, state.set_sample(p, s))) - //} - //import (&self, state: &mut Sampler, c: FileBrowserCommand) -> Option { - //match c { - //FileBrowserCommand::Begin => { - ////let voices = &state.state.voices; - ////let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); - //state.mode = Some(SamplerMode::Import(0, FileBrowser::new(None)?)); - //None - //}, - //_ => { - //println!("\n\rtodo: import: filebrowser: {c:?}"); - //None - //} - //} - //} - ////(Select [i: usize] Some(Self::Select(state.set_note_pos(i)))) - ////(RecordBegin [p: u7] cmd!(state.begin_recording(p.as_int() as usize))) - ////(RecordCancel [] cmd!(state.cancel_recording())) - ////(RecordFinish [] cmd!(state.finish_recording())) - ////(SetStart [p: u7, frame: usize] cmd_todo!("\n\rtodo: {self:?}")) - ////(SetGain [p: u7, gain: f32] cmd_todo!("\n\rtodo: {self:?}")) - ////(NoteOn [p: u7, velocity: u7] cmd_todo!("\n\rtodo: {self:?}")) - ////(NoteOff [p: u7] cmd_todo!("\n\rtodo: {self:?}")) - ////(SetSample [p: u7, s: Option>>] Some(Self::SetSample(p, state.set_sample(p, s)))) - ////(Import [c: FileBrowserCommand] match c { - ////FileBrowserCommand::Begin => { - //////let voices = &state.state.voices; - //////let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); - ////state.mode = Some(SamplerMode::Import(0, FileBrowser::new(None)?)); - ////None - ////}, - ////_ => { - ////println!("\n\rtodo: import: filebrowser: {c:?}"); - ////None - ////} - ////}))); - ////("import" [,..a] - ////FileBrowserCommand::try_from_expr(state, a).map(Self::Import)) - ////("select" [i: usize] - ////Some(Self::Select(i.expect("no index")))) - ////("record/begin" [i: u7] - ////Some(Self::RecordBegin(i.expect("no index")))) - ////("record/cancel" [] - ////Some(Self::RecordCancel)) - ////("record/finish" [] - ////Some(Self::RecordFinish)) - ////("set/sample" [i: u7, s: Option>>] - ////Some(Self::SetSample(i.expect("no index"), s.expect("no sampler")))) - ////("set/start" [i: u7, s: usize] - ////Some(Self::SetStart(i.expect("no index"), s.expect("no start")))) - ////("set/gain" [i: u7, g: f32] - ////Some(Self::SetGain(i.expect("no index"), g.expect("no gain")))) - ////("note/on" [p: u7, v: u7] - ////Some(Self::NoteOn(p.expect("no slot"), v.expect("no velocity")))) - ////("note/off" [p: u7] - ////Some(Self::NoteOff(p.expect("no slot")))))); diff --git a/device/scene.rs b/device/scene.rs deleted file mode 100644 index 271daa5d..00000000 --- a/device/scene.rs +++ /dev/null @@ -1,191 +0,0 @@ -use crate::*; - -def_sizes_iter!(ScenesSizes => Scene); - -impl> + Send + Sync> HasScenes for T {} - -pub trait HasScenes: Has> + Send + Sync { - fn scenes (&self) -> &Vec { - Has::>::get(self) - } - fn scenes_mut (&mut self) -> &mut Vec { - Has::>::get_mut(self) - } - /// 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) - } -} - -pub trait HasSceneScroll: HasScenes { - fn scene_scroll (&self) -> usize; -} -impl HasSceneScroll for Arrangement { - fn scene_scroll (&self) -> usize { self.scene_scroll } -} - -pub type SceneWith<'a, T> = (usize, &'a Scene, usize, usize, T); - -impl AddScene for T {} - -pub trait AddScene: HasScenes + HasTracks { - /// Add multiple scenes - fn scenes_add (&mut self, n: usize) -> Usually<()> { - 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)> - { - 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 Scene { - fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } - fn _todo_usize_stub_ (&self) -> usize { todo!() } - fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } - fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } -} - -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}), -}); - -impl> + Send + Sync> HasScene for T {} - -pub trait HasScene: Has> + Send + Sync { - fn scene (&self) -> Option<&Scene> { - Has::>::get(self).as_ref() - } - fn scene_mut (&mut self) -> &mut Option { - Has::>::get_mut(self) - } -} - -pub trait ScenesView: - HasEditor + - HasSelection + - HasSceneScroll + - HasClipsSize + - Send + - Sync -{ - /// Default scene height. - const H_SCENE: usize = 2; - /// Default editor height. - const H_EDITOR: usize = 15; - fn h_scenes (&self) -> u16; - fn w_side (&self) -> u16; - fn w_mid (&self) -> u16; - fn scenes_with_sizes (&self) -> impl ScenesSizes<'_> { - let mut y = 0; - self.scenes().iter().enumerate().skip(self.scene_scroll()).map_while(move|(s, scene)|{ - let height = if self.selection().scene() == Some(s) && self.editor().is_some() { - 8 - } else { - Self::H_SCENE - }; - if y + height <= self.clips_size().h() { - let data = (s, scene, y, y + height); - y += height; - Some(data) - } else { - None - } - }) - } - fn view_scenes_names (&self) -> impl Content { - Fixed::X(20, Thunk::new(|to: &mut TuiOut|for (index, scene, ..) in self.scenes_with_sizes() { - to.place(&self.view_scene_name(index, scene)); - })) - } - fn view_scene_name <'a> (&'a self, index: usize, scene: &'a Scene) -> impl Content + 'a { - let h = if self.selection().scene() == Some(index) && let Some(_editor) = self.editor() { - 7 - } else { - Self::H_SCENE as u16 - }; - let bg = if self.selection().scene() == Some(index) { - scene.color.light.rgb - } else { - scene.color.base.rgb - }; - let a = Fill::X(Align::w(Bsp::e(format!("·s{index:02} "), - Tui::fg(Tui::g(255), Tui::bold(true, &scene.name))))); - let b = When::new(self.selection().scene() == Some(index) && self.is_editing(), - Fill::XY(Align::nw(Bsp::s( - self.editor().as_ref().map(|e|e.clip_status()), - self.editor().as_ref().map(|e|e.edit_status()))))); - Fixed::XY(20, h, Tui::bg(bg, Align::nw(Bsp::s(a, b)))) - } -} - -#[derive(Debug, Default)] -pub struct Scene { - /// Name of scene - 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 } - } -} - //scene_scroll: Fill::Y(Fixed::X(1, ScrollbarV { - //offset: arrangement.scene_scroll, - //length: h_scenes_area as usize, - //total: h_scenes as usize, - //})), -//take!(SceneCommand |state: Arrangement, iter|state.selected_scene().as_ref() - //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); diff --git a/device/select.rs b/device/select.rs deleted file mode 100644 index 4868692b..00000000 --- a/device/select.rs +++ /dev/null @@ -1,140 +0,0 @@ -use crate::*; - -impl> HasSelection for T {} -pub trait HasSelection: Has { - fn selection (&self) -> &Selection { self.get() } - fn selection_mut (&mut self) -> &mut Selection { self.get_mut() } -} - -/// 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 }, -} - -#[cfg(feature = "track")] -impl Selection { - pub fn track (&self) -> Option { - use Selection::*; - if let Track(track)|TrackClip{track,..}|TrackInput{track,..}|TrackOutput{track,..}|TrackDevice{track,..} = self { - Some(*track) - } else { - None - } - } - pub fn select_track (&self, track_count: usize) -> Self { - use Selection::*; - match self { - Mix => Track(0), - Scene(_) => Mix, - Track(t) => Track((t + 1) % track_count), - TrackClip { track, .. } => Track(*track), - _ => todo!(), - } - } - pub fn select_track_next (&self, len: usize) -> Self { - use Selection::*; - match self { - Mix => Track(0), - Scene(s) => TrackClip { track: 0, scene: *s }, - Track(t) => if t + 1 < len { Track(t + 1) } else { Mix }, - TrackClip {track, scene} => if track + 1 < len { TrackClip { track: track + 1, scene: *scene } } else { Scene(*scene) }, - _ => todo!() - } - } - pub fn select_track_prev (&self) -> Self { - use Selection::*; - match self { - Mix => Mix, - Scene(s) => Scene(*s), - Track(0) => Mix, - Track(t) => Track(t - 1), - TrackClip { track: 0, scene } => Scene(*scene), - TrackClip { track: t, scene } => TrackClip { track: t - 1, scene: *scene }, - _ => todo!() - } - } -} - -#[cfg(feature = "scene")] -impl Selection { - pub fn scene (&self) -> Option { - use Selection::*; - match self { Scene(scene) | TrackClip { scene, .. } => Some(*scene), _ => None } - } - pub fn select_scene (&self, scene_count: usize) -> Self { - use Selection::*; - match self { - Mix | Track(_) => Scene(0), - Scene(s) => Scene((s + 1) % scene_count), - TrackClip { scene, .. } => Track(*scene), - _ => todo!(), - } - } - pub fn select_scene_next (&self, len: usize) -> Self { - use Selection::*; - match self { - Mix => Scene(0), - Track(t) => TrackClip { track: *t, scene: 0 }, - Scene(s) => if s + 1 < len { Scene(s + 1) } else { Mix }, - TrackClip { track, scene } => if scene + 1 < len { TrackClip { track: *track, scene: scene + 1 } } else { Track(*track) }, - _ => todo!() - } - } - pub fn select_scene_prev (&self) -> Self { - use Selection::*; - match self { - Mix | Scene(0) => Mix, - Scene(s) => Scene(s - 1), - Track(t) => Track(*t), - TrackClip { track, scene: 0 } => Track(*track), - TrackClip { track, scene } => TrackClip { track: *track, scene: scene - 1 }, - _ => todo!() - } - } -} - -impl Selection { - pub fn describe ( - &self, - #[cfg(feature = "track")] tracks: &[Track], - #[cfg(feature = "scene")] scenes: &[Scene], - ) -> Arc { - use Selection::*; - format!("{}", match self { - Mix => "Everything".to_string(), - #[cfg(feature = "scene")] Scene(s) => - scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name)).unwrap_or_else(||"S??".into()), - #[cfg(feature = "track")] Track(t) => - tracks.get(*t).map(|track|format!("T{t}: {}", &track.name)).unwrap_or_else(||"T??".into()), - TrackClip { track, scene } => match (tracks.get(*track), scenes.get(*scene)) { - (Some(_), Some(s)) => match s.clip(*track) { - Some(clip) => format!("T{track} S{scene} C{}", &clip.read().unwrap().name), - None => format!("T{track} S{scene}: Empty") - }, - _ => format!("T{track} S{scene}: Empty"), - }, - _ => todo!() - }).into() - } -} diff --git a/device/sequencer.rs b/device/sequencer.rs deleted file mode 100644 index 46c93cd3..00000000 --- a/device/sequencer.rs +++ /dev/null @@ -1,542 +0,0 @@ -//! MIDI sequencer -//! ``` -//! use crate::*; -//! -//! let clip = MidiClip::default(); -//! println!("Empty clip: {clip:?}"); -//! -//! let clip = MidiClip::stop_all(); -//! println!("Panic clip: {clip:?}"); -//! -//! let mut clip = MidiClip::new("clip", true, 1, None, None); -//! clip.set_length(96); -//! clip.toggle_loop(); -//! clip.record_event(12, MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }); -//! assert!(clip.contains_note_on(36.into(), 6, 18)); -//! assert_eq!(&clip.notes, &clip.duplicate().notes); -//! -//! let clip = std::sync::Arc::new(clip); -//! assert_eq!(clip.clone(), clip); -//! -//! let sequencer = Sequencer::default(); -//! println!("{sequencer:?}"); -//! ``` - -use crate::*; - -impl> HasSequencer for T { - fn sequencer (&self) -> &Sequencer { - self.get() - } - fn sequencer_mut (&mut self) -> &mut Sequencer { - self.get_mut() - } -} - -pub trait HasSequencer { - fn sequencer (&self) -> &Sequencer; - fn sequencer_mut (&mut self) -> &mut Sequencer; -} - -/// Contains state for playing a clip -pub struct Sequencer { - /// State of clock and playhead - #[cfg(feature = "clock")] pub clock: Clock, - /// Start time and clip being played - #[cfg(feature = "clip")] pub play_clip: Option<(Moment, Option>>)>, - /// Start time and next clip - #[cfg(feature = "clip")] pub next_clip: Option<(Moment, Option>>)>, - /// Record from MIDI ports to current sequence. - #[cfg(feature = "port")] pub midi_ins: Vec, - /// Play from current sequence to MIDI ports - #[cfg(feature = "port")] pub midi_outs: Vec, - /// Play input through output. - pub monitoring: bool, - /// Write input to sequence. - pub recording: bool, - /// Overdub input to sequence. - pub overdub: bool, - /// Send all notes off - pub reset: bool, // TODO?: after Some(nframes) - /// Notes currently held at input - pub notes_in: Arc>, - /// Notes currently held at output - pub notes_out: Arc>, - /// MIDI output buffer - pub note_buf: Vec, - /// MIDI output buffer - pub midi_buf: Vec>>, -} - -impl Default for Sequencer { - fn default () -> Self { - Self { - #[cfg(feature = "clock")] clock: Clock::default(), - #[cfg(feature = "clip")] play_clip: None, - #[cfg(feature = "clip")] next_clip: None, - #[cfg(feature = "port")] midi_ins: vec![], - #[cfg(feature = "port")] midi_outs: vec![], - - recording: false, - monitoring: true, - overdub: false, - notes_in: RwLock::new([false;128]).into(), - notes_out: RwLock::new([false;128]).into(), - note_buf: vec![0;8], - midi_buf: vec![], - reset: true, - } - } -} - -impl Sequencer { - pub fn new ( - name: impl AsRef, - jack: &Jack<'static>, - #[cfg(feature = "clock")] clock: Option<&Clock>, - #[cfg(feature = "clip")] clip: Option<&Arc>>, - #[cfg(feature = "port")] midi_from: &[Connect], - #[cfg(feature = "port")] midi_to: &[Connect], - ) -> Usually { - let _name = name.as_ref(); - #[cfg(feature = "clock")] let clock = clock.cloned().unwrap_or_default(); - Ok(Self { - reset: true, - notes_in: RwLock::new([false;128]).into(), - notes_out: RwLock::new([false;128]).into(), - #[cfg(feature = "port")] midi_ins: vec![MidiInput::new(jack, &format!("M/{}", name.as_ref()), midi_from)?,], - #[cfg(feature = "port")] midi_outs: vec![MidiOutput::new(jack, &format!("{}/M", name.as_ref()), midi_to)?, ], - #[cfg(feature = "clip")] play_clip: clip.map(|clip|(Moment::zero(&clock.timebase), Some(clip.clone()))), - #[cfg(feature = "clock")] clock, - ..Default::default() - }) - } -} - -impl std::fmt::Debug for Sequencer { - fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("Sequencer") - .field("clock", &self.clock) - .field("play_clip", &self.play_clip) - .field("next_clip", &self.next_clip) - .finish() - } -} - -#[cfg(feature = "clock")] has!(Clock: |self:Sequencer|self.clock); -#[cfg(feature = "port")] has!(Vec: |self:Sequencer|self.midi_ins); -#[cfg(feature = "port")] has!(Vec: |self:Sequencer|self.midi_outs); - -impl MidiMonitor for Sequencer { - fn notes_in (&self) -> &Arc> { - &self.notes_in - } - fn monitoring (&self) -> bool { - self.monitoring - } - fn monitoring_mut (&mut self) -> &mut bool { - &mut self.monitoring - } -} - -impl MidiRecord for Sequencer { - fn recording (&self) -> bool { - self.recording - } - fn recording_mut (&mut self) -> &mut bool { - &mut self.recording - } - fn overdub (&self) -> bool { - self.overdub - } - fn overdub_mut (&mut self) -> &mut bool { - &mut self.overdub - } -} - -#[cfg(feature="clip")] impl HasPlayClip for Sequencer { - fn reset (&self) -> bool { - self.reset - } - fn reset_mut (&mut self) -> &mut bool { - &mut self.reset - } - fn play_clip (&self) -> &Option<(Moment, Option>>)> { - &self.play_clip - } - fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)> { - &mut self.play_clip - } - fn next_clip (&self) -> &Option<(Moment, Option>>)> { - &self.next_clip - } - fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)> { - &mut self.next_clip - } -} - -/// JACK process callback for a sequencer's clip sequencer/recorder. -impl Audio for Sequencer { - fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { - if self.clock().is_rolling() { - self.process_rolling(scope) - } else { - self.process_stopped(scope) - } - } -} - -impl Sequencer { - fn process_rolling (&mut self, scope: &ProcessScope) -> Control { - self.process_clear(scope, false); - // Write chunk of clip to output, handle switchover - if self.process_playback(scope) { - self.process_switchover(scope); - } - // Monitor input to output - self.process_monitoring(scope); - // Record and/or monitor input - self.process_recording(scope); - // Emit contents of MIDI buffers to JACK MIDI output ports. - self.midi_outs_emit(scope); - Control::Continue - } - fn process_stopped (&mut self, scope: &ProcessScope) -> Control { - if self.monitoring() && self.midi_ins().len() > 0 && self.midi_outs().len() > 0 { - self.process_monitoring(scope) - } - Control::Continue - } - fn process_monitoring (&mut self, scope: &ProcessScope) { - let notes_in = self.notes_in().clone(); // For highlighting keys and note repeat - let monitoring = self.monitoring(); - for input in self.midi_ins.iter() { - for (sample, event, bytes) in input.parsed(scope) { - if let LiveEvent::Midi { message, .. } = event { - if monitoring { - self.midi_buf[sample].push(bytes.to_vec()); - } - // FIXME: don't lock on every event! - update_keys(&mut notes_in.write().unwrap(), &message); - } - } - } - } - /// Clear the section of the output buffer that we will be using, - /// emitting "all notes off" at start of buffer if requested. - fn process_clear (&mut self, scope: &ProcessScope, reset: bool) { - let n_frames = (scope.n_frames() as usize).min(self.midi_buf_mut().len()); - for frame in &mut self.midi_buf_mut()[0..n_frames] { - frame.clear(); - } - if reset { - all_notes_off(self.midi_buf_mut()); - } - for port in self.midi_outs_mut().iter_mut() { - // Clear output buffer(s) - port.buffer_clear(scope, false); - } - } - fn process_recording (&mut self, scope: &ProcessScope) { - if self.monitoring() { - self.monitor(scope); - } - if let Some((started, ref clip)) = self.play_clip.clone() { - self.record_clip(scope, started, clip); - } - if let Some((_start_at, _clip)) = &self.next_clip() { - self.record_next(); - } - } - fn process_playback (&mut self, scope: &ProcessScope) -> bool { - // If a clip is playing, write a chunk of MIDI events from it to the output buffer. - // If no clip is playing, prepare for switchover immediately. - if let Some((started, clip)) = &self.play_clip { - // Length of clip, to repeat or stop on end. - let length = clip.as_ref().map_or(0, |p|p.read().unwrap().length); - // Index of first sample to populate. - let offset = self.clock().get_sample_offset(scope, &started); - // Write MIDI events from clip at sample offsets corresponding to pulses. - for (sample, pulse) in self.clock().get_pulses(scope, offset) { - // If a next clip is enqueued, and we're past the end of the current one, - // break the loop here (FIXME count pulse correctly) - let past_end = if clip.is_some() { pulse >= length } else { true }; - // Is it time for switchover? - if self.next_clip().is_some() && past_end { - return true - } - // If there's a currently playing clip, output notes from it to buffer: - if let Some(clip) = clip { - // Source clip from which the MIDI events will be taken. - let clip = clip.read().unwrap(); - // Clip with zero length is not processed - if clip.length > 0 { - // Current pulse index in source clip - let pulse = pulse % clip.length; - // Output each MIDI event from clip at appropriate frames of output buffer: - for message in clip.notes[pulse].iter() { - for port in self.midi_outs.iter_mut() { - port.buffer_write(sample, LiveEvent::Midi { - channel: 0.into(), /* TODO */ - message: *message - }); - } - } - } - } - } - false - } else { - true - } - } - /// Handle switchover from current to next playing clip. - fn process_switchover (&mut self, scope: &ProcessScope) { - let midi_buf = self.midi_buf_mut(); - let sample0 = scope.last_frame_time() as usize; - //let samples = scope.n_frames() as usize; - if let Some((start_at, clip)) = &self.next_clip() { - let start = start_at.sample.get() as usize; - let sample = self.clock().started.read().unwrap() - .as_ref().unwrap().sample.get() as usize; - // If it's time to switch to the next clip: - if start <= sample0.saturating_sub(sample) { - // Samples elapsed since clip was supposed to start - let _skipped = sample0 - start; - // Switch over to enqueued clip - let started = Moment::from_sample(self.clock().timebase(), start as f64); - // Launch enqueued clip - *self.play_clip_mut() = Some((started, clip.clone())); - // Unset enqueuement (TODO: where to implement looping?) - *self.next_clip_mut() = None; - // Fill in remaining ticks of chunk from next clip. - self.process_playback(scope); - } - } - } -} - -pub trait HasMidiBuffers { - fn note_buf_mut (&mut self) -> &mut Vec; - fn midi_buf_mut (&mut self) -> &mut Vec>>; -} - -impl HasMidiBuffers for Sequencer { - fn note_buf_mut (&mut self) -> &mut Vec { - &mut self.note_buf - } - fn midi_buf_mut (&mut self) -> &mut Vec>> { - &mut self.midi_buf - } -} - -pub trait MidiMonitor: HasMidiIns + HasMidiBuffers { - fn notes_in (&self) -> &Arc>; - fn monitoring (&self) -> bool; - fn monitoring_mut (&mut self) -> &mut bool; - fn toggle_monitor (&mut self) { - *self.monitoring_mut() = !self.monitoring(); - } - fn monitor (&mut self, scope: &ProcessScope) { - } -} - -pub trait MidiRecord: MidiMonitor + HasClock + HasPlayClip { - fn recording (&self) -> bool; - fn recording_mut (&mut self) -> &mut bool; - fn toggle_record (&mut self) { - *self.recording_mut() = !self.recording(); - } - - fn overdub (&self) -> bool; - fn overdub_mut (&mut self) -> &mut bool; - fn toggle_overdub (&mut self) { - *self.overdub_mut() = !self.overdub(); - } - - fn record_clip ( - &mut self, - scope: &ProcessScope, - started: Moment, - clip: &Option>>, - ) { - if let Some(clip) = clip { - let sample0 = scope.last_frame_time() as usize; - let start = started.sample.get() as usize; - let _recording = self.recording(); - let timebase = self.clock().timebase().clone(); - let quant = self.clock().quant.get(); - let mut clip = clip.write().unwrap(); - let length = clip.length; - for input in self.midi_ins_mut().iter() { - for (sample, event, _bytes) in parse_midi_input(input.port().iter(scope)) { - if let LiveEvent::Midi { message, .. } = event { - clip.record_event({ - let sample = (sample0 + sample - start) as f64; - let pulse = timebase.samples_to_pulse(sample); - let quantized = (pulse / quant).round() * quant; - quantized as usize % length - }, message); - } - } - } - } - } - - fn record_next (&mut self) { - // TODO switch to next clip and record into it - } -} - -pub trait MidiViewer: HasSize + MidiRange + MidiPoint + Debug + Send + Sync { - fn buffer_size (&self, clip: &MidiClip) -> (usize, usize); - fn redraw (&self); - fn clip (&self) -> &Option>>; - fn clip_mut (&mut self) -> &mut Option>>; - fn set_clip (&mut self, clip: Option<&Arc>>) { - *self.clip_mut() = clip.cloned(); - self.redraw(); - } - /// Make sure cursor is within note range - fn autoscroll (&self) { - let note_pos = self.get_note_pos().min(127); - let note_lo = self.get_note_lo(); - let note_hi = self.get_note_hi(); - if note_pos < note_lo { - self.note_lo().set(note_pos); - } else if note_pos > note_hi { - self.note_lo().set((note_lo + note_pos).saturating_sub(note_hi)); - } - } - /// Make sure time range is within display - fn autozoom (&self) { - if self.time_lock().get() { - let time_len = self.get_time_len(); - let time_axis = self.get_time_axis(); - let time_zoom = self.get_time_zoom(); - loop { - let time_zoom = self.time_zoom().get(); - let time_area = time_axis * time_zoom; - if time_area > time_len { - let next_time_zoom = NoteDuration::prev(time_zoom); - if next_time_zoom <= 1 { - break - } - let next_time_area = time_axis * next_time_zoom; - if next_time_area >= time_len { - self.time_zoom().set(next_time_zoom); - } else { - break - } - } else if time_area < time_len { - let prev_time_zoom = NoteDuration::next(time_zoom); - if prev_time_zoom > 384 { - break - } - let prev_time_area = time_axis * prev_time_zoom; - if prev_time_area <= time_len { - self.time_zoom().set(prev_time_zoom); - } else { - break - } - } - } - if time_zoom != self.time_zoom().get() { - self.redraw() - } - } - //while time_len.div_ceil(time_zoom) > time_axis { - //println!("\r{time_len} {time_zoom} {time_axis}"); - //time_zoom = Note::next(time_zoom); - //} - //self.time_zoom().set(time_zoom); - } -} - -pub trait HasPlayClip: HasClock { - - fn reset (&self) -> bool; - - fn reset_mut (&mut self) -> &mut bool; - - fn play_clip (&self) -> &Option<(Moment, Option>>)>; - - fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)>; - - fn next_clip (&self) -> &Option<(Moment, Option>>)>; - - fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)>; - - fn pulses_since_start (&self) -> Option { - if let Some((started, Some(_))) = self.play_clip().as_ref() { - let elapsed = self.clock().playhead.pulse.get() - started.pulse.get(); - return Some(elapsed) - } - None - } - - fn pulses_since_start_looped (&self) -> Option<(f64, f64)> { - if let Some((started, Some(clip))) = self.play_clip().as_ref() { - let elapsed = self.clock().playhead.pulse.get() - started.pulse.get(); - let length = clip.read().unwrap().length.max(1); // prevent div0 on empty clip - let times = (elapsed as usize / length) as f64; - let elapsed = (elapsed as usize % length) as f64; - return Some((times, elapsed)) - } - None - } - - fn enqueue_next (&mut self, clip: Option<&Arc>>) { - *self.next_clip_mut() = Some((self.clock().next_launch_instant(), clip.cloned())); - *self.reset_mut() = true; - } - - fn play_status (&self) -> impl Content { - let (name, color): (Arc, ItemTheme) = if let Some((_, Some(clip))) = self.play_clip() { - let MidiClip { ref name, color, .. } = *clip.read().unwrap(); - (name.clone(), color) - } else { - ("".into(), Tui::g(64).into()) - }; - let time: String = self.pulses_since_start_looped() - .map(|(times, time)|format!("{:>3}x {:>}", times+1.0, self.clock().timebase.format_beats_1(time))) - .unwrap_or_else(||String::from(" ")).into(); - FieldV(color, "Now:", format!("{} {}", time, name)) - } - - fn next_status (&self) -> impl Content { - let mut time: Arc = String::from("--.-.--").into(); - let mut name: Arc = String::from("").into(); - let mut color = ItemTheme::G[64]; - let clock = self.clock(); - if let Some((t, Some(clip))) = self.next_clip() { - let clip = clip.read().unwrap(); - name = clip.name.clone(); - color = clip.color.clone(); - time = { - let target = t.pulse.get(); - let current = clock.playhead.pulse.get(); - if target > current { - let remaining = target - current; - format!("-{:>}", clock.timebase.format_beats_1(remaining)) - } else { - String::new() - } - }.into() - } else if let Some((t, Some(clip))) = self.play_clip() { - let clip = clip.read().unwrap(); - if clip.looped { - name = clip.name.clone(); - color = clip.color.clone(); - let target = t.pulse.get() + clip.length as f64; - let current = clock.playhead.pulse.get(); - if target > current { - time = format!("-{:>}", clock.timebase.format_beats_0(target - current)).into() - } - } else { - name = "Stop".to_string().into(); - } - }; - FieldV(color, "Next:", format!("{} {}", time, name)) - } -} diff --git a/device/sf2.rs b/device/sf2.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/device/track.rs b/device/track.rs deleted file mode 100644 index c583381c..00000000 --- a/device/track.rs +++ /dev/null @@ -1,365 +0,0 @@ -use crate::*; - -def_sizes_iter!(TracksSizes => Track); - -impl> + Send + Sync> HasTracks for T {} - -pub trait HasTracks: Has> + Send + Sync { - fn tracks (&self) -> &Vec { Has::>::get(self) } - fn tracks_mut (&mut self) -> &mut Vec { Has::>::get_mut(self) } - /// Run audio callbacks for every track and every device - fn process_tracks (&mut self, client: &Client, scope: &ProcessScope) -> Control { - for track in self.tracks_mut().iter_mut() { - if Control::Quit == Audio::process(&mut track.sequencer, client, scope) { - return Control::Quit - } - for device in track.devices.iter_mut() { - if Control::Quit == DeviceAudio(device).process(client, scope) { - return Control::Quit - } - } - } - Control::Continue - } - fn track_longest_name (&self) -> usize { self.tracks().iter().map(|s|s.name.len()).fold(0, usize::max) } - /// Stop all playing clips - fn tracks_stop_all (&mut self) { for track in self.tracks_mut().iter_mut() { track.sequencer.enqueue_next(None); } } - /// Stop all playing clips - fn tracks_launch (&mut self, clips: Option>>>>) { - if let Some(clips) = clips { - for (clip, track) in clips.iter().zip(self.tracks_mut()) { track.sequencer.enqueue_next(clip.as_ref()); } - } else { - for track in self.tracks_mut().iter_mut() { track.sequencer.enqueue_next(None); } - } - } - /// Spacing between tracks. - const TRACK_SPACING: usize = 0; -} - -pub trait HasTrackScroll: HasTracks { - fn track_scroll (&self) -> usize; -} - -impl HasTrackScroll for Arrangement { - fn track_scroll (&self) -> usize { - self.track_scroll - } -} - -pub trait HasTrack { - fn track (&self) -> Option<&Track>; - fn track_mut (&mut self) -> Option<&mut Track>; - #[cfg(feature = "port")] fn view_midi_ins_status <'a> (&'a self, theme: ItemTheme) -> impl Content + 'a { - self.track().map(move|track|view_ports_status(theme, "MIDI ins: ", &track.sequencer.midi_ins)) - } - #[cfg(feature = "port")] fn view_midi_outs_status (&self, theme: ItemTheme) -> impl Content + '_ { - self.track().map(move|track|view_ports_status(theme, "MIDI outs: ", &track.sequencer.midi_outs)) - } - #[cfg(feature = "port")] fn view_audio_ins_status (&self, theme: ItemTheme) -> impl Content { - self.track().map(move|track|view_ports_status(theme, "Audio ins: ", &track.audio_ins())) - } - #[cfg(feature = "port")] fn view_audio_outs_status (&self, theme: ItemTheme) -> impl Content { - self.track().map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs())) - } -} - -impl> HasTrack for T { - fn track (&self) -> Option<&Track> { - self.get() - } - fn track_mut (&mut self) -> Option<&mut Track> { - self.get_mut() - } -} - -#[derive(Debug, Default)] -pub struct Track { - /// Name of track - pub name: Arc, - /// Identifying color of track - pub color: ItemTheme, - /// Preferred width of track column - pub width: usize, - /// MIDI sequencer state - pub sequencer: Sequencer, - /// Device chain - pub devices: Vec, -} - -has!(Clock: |self: Track|self.sequencer.clock); -has!(Sequencer: |self: Track|self.sequencer); -impl Track { - /// Create a new track with only the default [Sequencer]. - pub fn new ( - name: &impl AsRef, - color: Option, - jack: &Jack<'static>, - clock: Option<&Clock>, - clip: Option<&Arc>>, - midi_from: &[Connect], - midi_to: &[Connect], - ) -> Usually { - Ok(Self { - name: name.as_ref().into(), - color: color.unwrap_or_default(), - sequencer: Sequencer::new(format!("{}/sequencer", name.as_ref()), jack, clock, clip, midi_from, midi_to)?, - ..Default::default() - }) - } - fn audio_ins (&self) -> &[AudioInput] { - self.devices.first().map(|x|x.audio_ins()).unwrap_or_default() - } - fn audio_outs (&self) -> &[AudioOutput] { - self.devices.last().map(|x|x.audio_outs()).unwrap_or_default() - } - fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } - fn _todo_usize_stub_ (&self) -> usize { todo!() } - fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } - fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } -} - -impl HasWidth for Track { - const MIN_WIDTH: usize = 9; - fn width_inc (&mut self) { self.width += 1; } - fn width_dec (&mut self) { if self.width > Track::MIN_WIDTH { self.width -= 1; } } -} - -#[cfg(feature = "sampler")] -impl Track { - /// Create a new track connecting the [Sequencer] to a [Sampler]. - pub fn new_with_sampler ( - name: &impl AsRef, - color: Option, - jack: &Jack<'static>, - clock: Option<&Clock>, - clip: Option<&Arc>>, - midi_from: &[Connect], - midi_to: &[Connect], - audio_from: &[&[Connect];2], - audio_to: &[&[Connect];2], - ) -> Usually { - let mut track = Self::new(name, color, jack, clock, clip, midi_from, midi_to)?; - let client_name = jack.with_client(|c|c.name().to_string()); - let port_name = track.sequencer.midi_outs[0].port_name(); - let connect = [Connect::exact(format!("{client_name}:{}", port_name))]; - track.devices.push(Device::Sampler(Sampler::new( - jack, &format!("{}/sampler", name.as_ref()), &connect, audio_from, audio_to - )?)); - Ok(track) - } - pub fn sampler (&self, mut nth: usize) -> Option<&Sampler> { - for device in self.devices.iter() { - match device { - Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; }, - _ => {} - } - } - None - } - pub fn sampler_mut (&mut self, mut nth: usize) -> Option<&mut Sampler> { - for device in self.devices.iter_mut() { - match device { - Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; }, - _ => {} - } - } - None - } -} -def_command!(TrackCommand: |track: Track| { - Stop => { track.sequencer.enqueue_next(None); Ok(None) }, - SetMute { mute: Option } => todo!(), - SetSolo { solo: Option } => todo!(), - SetSize { size: usize } => todo!(), - SetZoom { zoom: usize } => todo!(), - SetName { name: Arc } => - swap_value(&mut track.name, name, |name|Self::SetName { name }), - SetColor { color: ItemTheme } => - swap_value(&mut track.color, color, |color|Self::SetColor { color }), - SetRec { rec: Option } => - toggle_bool(&mut track.sequencer.recording, rec, |rec|Self::SetRec { rec }), - SetMon { mon: Option } => - toggle_bool(&mut track.sequencer.monitoring, mon, |mon|Self::SetMon { mon }), -}); - -impl ClipsView for T {} - -pub trait TracksView: - ScenesView + - HasMidiIns + - HasMidiOuts + - HasSize + - HasTrackScroll + - HasSelection + - HasEditor + - HasClipsSize -{ - fn tracks_width_available (&self) -> u16 { - (self.width() as u16).saturating_sub(40) - } - /// Iterate over tracks with their corresponding sizes. - fn tracks_with_sizes (&self) -> impl TracksSizes<'_> { - let _editor_width = self.editor().map(|e|e.width()); - let _active_track = self.selection().track(); - let mut x = 0; - self.tracks().iter().enumerate().map_while(move |(index, track)|{ - let width = track.width.max(8); - if x + width < self.clips_size().w() { - let data = (index, track, x, x + width); - x += width + Self::TRACK_SPACING; - Some(data) - } else { - None - } - }) - } - fn view_track_names (&self, theme: ItemTheme) -> impl Content { - let track_count = self.tracks().len(); - let scene_count = self.scenes().len(); - let selected = self.selection(); - let button = Bsp::s( - button_3("t", "rack ", format!("{}{track_count}", selected.track() - .map(|track|format!("{track}/")).unwrap_or_default()), false), - button_3("s", "cene ", format!("{}{scene_count}", selected.scene() - .map(|scene|format!("{scene}/")).unwrap_or_default()), false)); - let button_2 = Bsp::s( - button_2("T", "+", false), - button_2("S", "+", false)); - view_track_row_section(theme, button, button_2, Tui::bg(theme.darker.rgb, - Fixed::Y(2, Thunk::new(|to: &mut TuiOut|{ - for (index, track, x1, _x2) in self.tracks_with_sizes() { - to.place(&Push::X(x1 as u16, Fixed::X(track_width(index, track), - Tui::bg(if selected.track() == Some(index) { - track.color.light.rgb - } else { - track.color.base.rgb - }, Bsp::s(Fill::X(Align::nw(Bsp::e( - format!("·t{index:02} "), - Tui::fg(Rgb(255, 255, 255), Tui::bold(true, &track.name)) - ))), ""))) ));}})))) - } - fn view_track_outputs <'a> (&'a self, theme: ItemTheme, _h: u16) -> impl Content { - view_track_row_section(theme, - Bsp::s(Fill::X(Align::w(button_2("o", "utput", false))), - Thunk::new(|to: &mut TuiOut|for port in self.midi_outs().iter() { - to.place(&Fill::X(Align::w(port.port_name()))); - })), - button_2("O", "+", false), - Tui::bg(theme.darker.rgb, Align::w(Thunk::new(|to: &mut TuiOut|{ - for (index, track, _x1, _x2) in self.tracks_with_sizes() { - to.place(&Fixed::X(track_width(index, track), - Align::nw(Fill::Y(Map::south(1, ||track.sequencer.midi_outs.iter(), - |port, index|Tui::fg(Rgb(255, 255, 255), - Fixed::Y(1, Tui::bg(track.color.dark.rgb, Fill::X(Align::w( - format!("·o{index:02} {}", port.port_name())))))))))));}})))) - } - fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Content { - let mut h = 0u16; - for track in self.tracks().iter() { - h = h.max(track.sequencer.midi_ins.len() as u16); - } - let content = Thunk::new(move|to: &mut TuiOut|for (index, track, _x1, _x2) in self.tracks_with_sizes() { - to.place(&Fixed::XY(track_width(index, track), h + 1, - Align::nw(Bsp::s( - Tui::bg(track.color.base.rgb, - Fill::X(Align::w(row!( - Either::new(track.sequencer.monitoring, Tui::fg(Green, "●mon "), "·mon "), - Either::new(track.sequencer.recording, Tui::fg(Red, "●rec "), "·rec "), - Either::new(track.sequencer.overdub, Tui::fg(Yellow, "●dub "), "·dub "), - )))), - Map::south(1, ||track.sequencer.midi_ins.iter(), - |port, index|Tui::fg_bg(Rgb(255, 255, 255), track.color.dark.rgb, - Fill::X(Align::w(format!("·i{index:02} {}", port.port_name()))))))))); - }); - view_track_row_section(theme, button_2("i", "nput", false), button_2("I", "+", false), - Tui::bg(theme.darker.rgb, Align::w(content))) - } -} - -pub(crate) fn track_width (_index: usize, track: &Track) -> u16 { - track.width as u16 -} - -fn view_track_header (theme: ItemTheme, content: impl Content) -> impl Content { - Fixed::X(12, Tui::bg(theme.darker.rgb, Fill::X(Align::e(content)))) -} - -fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, ports: &'a [T]) - -> impl Content + use<'a, T> -{ - let ins = ports.len() as u16; - let frame = Outer(true, Style::default().fg(Tui::g(96))); - let iter = move||ports.iter(); - let names = Map::south(1, iter, move|port, index|Fill::Y(Align::w(format!(" {index} {}", port.port_name())))); - let field = FieldV(theme, title, names); - Fixed::XY(20, 1 + ins, frame.enclose(Fixed::XY(20, 1 + ins, field))) -} - -impl Track { - pub fn per <'a, T: Content + 'a, U: TracksSizes<'a>> ( - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a - ) -> impl Content + 'a { - Map::new(tracks, - move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ - let width = (x2 - x1) as u16; - map_east(x1 as u16, width, Fixed::X(width, Tui::fg_bg( - track.color.lightest.rgb, - track.color.base.rgb, - callback(index, track))))}) - } -} - -pub(crate) fn per_track_top <'a, T: Content + 'a, U: TracksSizes<'a>> ( - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a -) -> impl Content + 'a { - Align::x(Tui::bg(Reset, Map::new(tracks, - move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ - let width = (x2 - x1) as u16; - map_east(x1 as u16, width, Fixed::X(width, Tui::fg_bg( - track.color.lightest.rgb, - track.color.base.rgb, - callback(index, track))))}))) -} - -pub(crate) fn per_track <'a, T: Content + 'a, U: TracksSizes<'a>> ( - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a -) -> impl Content + 'a { - per_track_top(tracks, move|index, track|Fill::Y(Align::y(callback(index, track)))) -} - -pub(crate) fn io_ports <'a, T: PortsSizes<'a>> ( - fg: Color, bg: Color, iter: impl Fn()->T + Send + Sync + 'a -) -> impl Content + 'a { - Map::new(iter, move|( - _index, name, connections, y, y2 - ): (usize, &'a Arc, &'a [Connect], usize, usize), _| - map_south(y as u16, (y2-y) as u16, Bsp::s( - Fill::Y(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(Bsp::e(&" 󰣲 ", name))))), - Map::new(||connections.iter(), move|connect: &'a Connect, index|map_south(index as u16, 1, - Fill::Y(Align::w(Tui::bold(false, Tui::fg_bg(fg, bg, - &connect.info))))))))) -} - -//pub(crate) fn io_conns <'a, T: PortsSizes<'a>> ( - //fg: Color, bg: Color, iter: &mut impl Iterator, &'a [Connect], usize, usize)> -//) -> impl Content + 'a { - //Fill::XY(Thunk::new(move|to: &mut TuiOut|for (_, _, connections, y, y2) in &mut *iter { - //to.place(&map_south(y as u16, (y2-y) as u16, Bsp::s( - //Fill::Y(Tui::bold(true, wrap(bg, fg, Fill::Y(Align::w(&"▞▞▞▞ ▞▞▞▞"))))), - //Thunk::new(|to: &mut TuiOut|for (index, _connection) in connections.iter().enumerate() { - //to.place(&map_south(index as u16, 1, Fill::Y(Align::w(Tui::bold(false, - //wrap(bg, fg, Fill::Y(&""))))))) - //}) - //))) - //})) -//} - //track_scroll: Fill::Y(Fixed::Y(1, ScrollbarH { - //offset: arrangement.track_scroll, - //length: h_tracks_area as usize, - //total: h_scenes as usize, - //})), -//take!(TrackCommand |state: Arrangement, iter|state.selected_track().as_ref() - //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); diff --git a/device/vst3.rs b/device/vst3.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/engine/Cargo.lock b/engine/Cargo.lock new file mode 100644 index 00000000..5bf6354f --- /dev/null +++ b/engine/Cargo.lock @@ -0,0 +1,722 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "better-panic" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fa9e1d11a268684cbd90ed36370d7577afb6c62d912ddff5c15fc34343e5036" +dependencies = [ + "backtrace", + "console", +] + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "compact_str" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "console" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "windows-sys 0.59.0", +] + +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags", + "crossterm_winapi", + "mio", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + +[[package]] +name = "instability" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "898e106451f7335950c9cc64f8ec67b5f65698679ac67ed00619aeef14e1cf75" +dependencies = [ + "darling", + "indoc", + "pretty_assertions", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "miniz_oxide" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ratatui" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +dependencies = [ + "bitflags", + "cassowary", + "compact_str", + "crossterm", + "indoc", + "instability", + "itertools", + "lru", + "paste", + "strum", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.0", +] + +[[package]] +name = "redox_syscall" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustix" +version = "0.38.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c786062daee0d6db1132800e623df74274a0a87322d8e183338e01b3d98d058" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tek_engine" +version = "0.2.0" +dependencies = [ + "better-panic", + "crossterm", + "ratatui", +] + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width 0.1.14", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +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", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[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_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[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_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" diff --git a/engine/Cargo.toml b/engine/Cargo.toml index 23988f80..b72a07cb 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -1,22 +1,9 @@ [package] -name = "tek_engine" -edition = { workspace = true } -version = { workspace = true } - -[lib] -path = "engine.rs" - -[target.'cfg(target_os = "linux")'] -rustflags = ["-C", "link-arg=-fuse-ld=mold"] +name = "tek_engine" +edition = "2021" +version = "0.2.0" [dependencies] -atomic_float = { workspace = true } -jack = { workspace = true } -midly = { workspace = true } -tengri = { workspace = true } - -[dev-dependencies] -proptest = { workspace = true } -proptest-derive = { workspace = true } - -[features] +crossterm = "0.28.1" +ratatui = { version = "0.29.0", features = [ "unstable-widget-ref", "underline-color" ] } +better-panic = "0.3.0" diff --git a/engine/README.md b/engine/README.md new file mode 100644 index 00000000..03d7e53d --- /dev/null +++ b/engine/README.md @@ -0,0 +1,65 @@ +# `tek_engine` + +this crate provides the `Engine` trait, +which defines an application's lifecycle. + +currently, there is one kind of engine implemented, `Tui`. +it uses `ratatui` to present an interactive user interface +in text mode. + +at launch, the `Tui` engine spawns two threads, +a **render thread** and an **input thread**. (the +application may spawn further threads, such as a +**jack thread**.) + +all threads communicate using shared ownership, +`Arc` and `Arc`. the engine and +application instances are expected to be wrapped +in `Arc`; internally, those synchronization +mechanisms may be used liberally. + +## rendering + +the **render thread** continually invokes the +`Content::render` method of the application +to redraw the display. it does this efficiently +by using ratatui's double buffering. + +thus, for a type to be a valid application for engine `E`, +it must implement the trait `Content`, which allows +it to display content to the engine's output. + +the most important thing about the `Content` trait is that +it composes: +* you can implement `Content::content` to build + `Content`s out of other `Content`s +* and/or `Content::area` for custom positioning and sizing, +* and/or `Content::render` for custom rendering + within the given `Content`'s area. + +the manner of output is determined by the +`Engine::Output` type, a mutable pointer to which +is passed to the render method, e.g. in the case of +the `Tui` engine: `fn render(&self, output: &mut TuiOut)` + +you can use `TuiOut::blit` and `TuiOut::place` +to draw at specified coordinates of the display, and/or +directly modify the underlying `ratatui::Buffer` at +`output.buffer` + +rendering is intended to work with read-only access +to the application state. if you really need to update +values during rendering, use interior mutability. + +## input handling + +the **input thread** polls for keyboard events +and passes them onto the application's `Handle::handle` method. + +thus, for a type to be a valid application for engine `E`, +it must implement the trait `Handle`, which allows it +to respond to user input. + +this thread has write access to the application state, +and is responsible for mutating it in response to +user activity. diff --git a/engine/engine.rs b/engine/engine.rs deleted file mode 100644 index f7beba37..00000000 --- a/engine/engine.rs +++ /dev/null @@ -1,155 +0,0 @@ -mod engine_deps; pub use self::engine_deps::*; -mod time; pub use self::time::*; -mod note; pub use self::note::*; -mod jack; pub use self::jack::*; -mod midi; pub use self::midi::*; - -//pub trait MaybeHas: Send + Sync { - //fn get (&self) -> Option<&T>; -//} - -//impl>> MaybeHas for U { - //fn get (&self) -> Option<&T> { - //Has::>::get(self).as_ref() - //} -//} - -pub trait HasN: Send + Sync { - fn get_nth (&self, key: usize) -> &T; - fn get_nth_mut (&mut self, key: usize) -> &mut T; -} - -pub trait Gettable { - /// Returns current value - fn get (&self) -> T; -} - -pub trait Mutable: Gettable { - /// Sets new value, returns old - fn set (&mut self, value: T) -> T; -} - -pub trait InteriorMutable: Gettable { - /// Sets new value, returns old - fn set (&self, value: T) -> T; -} - -impl Gettable for AtomicBool { - fn get (&self) -> bool { self.load(Relaxed) } -} - -impl InteriorMutable for AtomicBool { - fn set (&self, value: bool) -> bool { self.swap(value, Relaxed) } -} - -impl Gettable for AtomicUsize { - fn get (&self) -> usize { self.load(Relaxed) } -} - -impl InteriorMutable for AtomicUsize { - fn set (&self, value: usize) -> usize { self.swap(value, Relaxed) } -} - -#[cfg(test)] #[test] fn test_time () -> Usually<()> { - // TODO! - Ok(()) -} - -#[cfg(test)] #[test] fn test_midi_range () { - let model = MidiRangeModel::from((1, false)); - - let _ = model.get_time_len(); - let _ = model.get_time_zoom(); - let _ = model.get_time_lock(); - let _ = model.get_time_start(); - let _ = model.get_time_axis(); - let _ = model.get_time_end(); - - let _ = model.get_note_lo(); - let _ = model.get_note_axis(); - let _ = model.get_note_hi(); -} - -//macro_rules! impl_port { - //($Name:ident : $Spec:ident -> $Pair:ident |$jack:ident, $name:ident|$port:expr) => { - //#[derive(Debug)] pub struct $Name { - ///// Handle to JACK client, for receiving reconnect events. - //jack: Jack<'static>, - ///// Port name - //name: Arc, - ///// Port handle. - //port: Port<$Spec>, - ///// List of ports to connect to. - //conn: Vec - //} - //impl AsRef> for $Name { - //fn as_ref (&self) -> &Port<$Spec> { &self.port } - //} - //impl $Name { - //pub fn new ($jack: &Jack, name: impl AsRef, connect: &[PortConnect]) - //-> Usually - //{ - //let $name = name.as_ref(); - //let jack = $jack.clone(); - //let port = $port?; - //let name = $name.into(); - //let conn = connect.to_vec(); - //let port = Self { jack, port, name, conn }; - //port.connect_to_matching()?; - //Ok(port) - //} - //pub fn name (&self) -> &Arc { &self.name } - //pub fn port (&self) -> &Port<$Spec> { &self.port } - //pub fn port_mut (&mut self) -> &mut Port<$Spec> { &mut self.port } - //pub fn into_port (self) -> Port<$Spec> { self.port } - //pub fn close (self) -> Usually<()> { - //let Self { jack, port, .. } = self; - //Ok(jack.with_client(|client|client.unregister_port(port))?) - //} - //} - //impl HasJack<'static> for $Name { - //fn jack (&self) -> &'static Jack<'static> { &self.jack } - //} - //impl JackPort<'static> for $Name { - //type Port = $Spec; - //type Pair = $Pair; - //fn port (&self) -> &Port<$Spec> { &self.port } - //} - //impl ConnectTo<'static, &str> for $Name { - //fn connect_to (&self, to: &str) -> Usually { - //self.with_client(|c|if let Some(ref port) = c.port_by_name(to.as_ref()) { - //self.connect_to(port) - //} else { - //Ok(Missing) - //}) - //} - //} - //impl ConnectTo<'static, &Port> for $Name { - //fn connect_to (&self, port: &Port) -> Usually { - //self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(&self.port, port) { - //Connected - //} else if let Ok(_) = c.connect_ports(port, &self.port) { - //Connected - //} else { - //Mismatch - //})) - //} - //} - //impl ConnectTo<'static, &Port<$Pair>> for $Name { - //fn connect_to (&self, port: &Port<$Pair>) -> Usually { - //self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(&self.port, port) { - //Connected - //} else if let Ok(_) = c.connect_ports(port, &self.port) { - //Connected - //} else { - //Mismatch - //})) - //} - //} - //impl ConnectAuto<'static> for $Name { - //fn connections (&self) -> &[PortConnect] { - //&self.conn - //} - //} - //}; -//} diff --git a/engine/engine_deps.rs b/engine/engine_deps.rs deleted file mode 100644 index 1284b0f2..00000000 --- a/engine/engine_deps.rs +++ /dev/null @@ -1,11 +0,0 @@ -pub use ::tengri; -pub(crate) use ::{ - tengri::{Usually, from}, - atomic_float::AtomicF64, - std::{ - cmp::Ord, - fmt::Debug, - ops::{Add, Sub, Mul, Div, Rem}, - sync::{Arc, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}}, - }, -}; diff --git a/engine/jack.rs b/engine/jack.rs deleted file mode 100644 index 9b611752..00000000 --- a/engine/jack.rs +++ /dev/null @@ -1,222 +0,0 @@ -use crate::*; -pub use ::jack::{*, contrib::{*, ClosureProcessHandler}}; -/// Wraps [JackState] and through it [jack::Client] when connected. -#[derive(Clone, Debug, Default)] -pub struct Jack<'j>(Arc>>); -/// Implement [Jack] constructor and methods -impl<'j> Jack<'j> { - /// Register new [Client] and wrap it for shared use. - pub fn new_run + Audio + Send + Sync + 'static> ( - name: &impl AsRef, - init: impl FnOnce(Jack<'j>)->Usually - ) -> Usually>> { - Jack::new(name)?.run(init) - } - pub fn new (name: &impl AsRef) -> Usually { - let client = Client::new(name.as_ref(), ClientOptions::NO_START_SERVER)?.0; - Ok(Jack(Arc::new(RwLock::new(JackState::Inactive(client))))) - } - pub fn run + Audio + Send + Sync + 'static> - (self, init: impl FnOnce(Self)->Usually) -> Usually>> - { - let client_state = self.0.clone(); - let app: Arc> = Arc::new(RwLock::new(init(self)?)); - let mut state = Activating; - std::mem::swap(&mut*client_state.write().unwrap(), &mut state); - if let Inactive(client) = state { - let client = client.activate_async( - // This is the misc notifications handler. It's a struct that wraps a [Box] - // which performs type erasure on a callback that takes [JackEvent], which is - // one of the available misc notifications. - Notifications(Box::new({ - let app = app.clone(); - move|event|(&mut*app.write().unwrap()).handle(event) - }) as BoxedJackEventHandler), - // This is the main processing handler. It's a struct that wraps a [Box] - // which performs type erasure on a callback that takes [Client] and [ProcessScope] - // and passes them down to the `app`'s `process` callback, which in turn - // implements audio and MIDI input and output on a realtime basis. - ClosureProcessHandler::new(Box::new({ - let app = app.clone(); - move|c: &_, s: &_|if let Ok(mut app) = app.write() { - app.process(c, s) - } else { - Control::Quit - } - }) as BoxedAudioHandler), - )?; - *client_state.write().unwrap() = Active(client); - } else { - unreachable!(); - } - Ok(app) - } - /// Run something with the client. - pub fn with_client (&self, op: impl FnOnce(&Client)->T) -> T { - match &*self.0.read().unwrap() { - Inert => panic!("jack client not activated"), - Inactive(client) => op(client), - Activating => panic!("jack client has not finished activation"), - Active(client) => op(client.as_client()), - } - } -} -impl<'j> HasJack<'j> for Jack<'j> { - fn jack (&self) -> &Jack<'j> { - self - } -} -impl<'j> HasJack<'j> for &Jack<'j> { - fn jack (&self) -> &Jack<'j> { - self - } -} -/// This is a connection which may be [Inactive], [Activating], or [Active]. -/// In the [Active] and [Inactive] states, [JackState::client] returns a -/// [jack::Client], which you can use to talk to the JACK API. -#[derive(Debug, Default)] -pub enum JackState<'j> { - /// Unused - #[default] Inert, - /// Before activation. - Inactive(Client), - /// During activation. - Activating, - /// After activation. Must not be dropped for JACK thread to persist. - Active(DynamicAsyncClient<'j>), -} -/// Things that can provide a [jack::Client] reference. -pub trait HasJack<'j>: Send + Sync { - /// Return the internal [jack::Client] handle - /// that lets you call the JACK API. - fn jack (&self) -> &Jack<'j>; - fn with_client (&self, op: impl FnOnce(&Client)->T) -> T { - self.jack().with_client(op) - } - fn port_by_name (&self, name: &str) -> Option> { - self.with_client(|client|client.port_by_name(name)) - } - fn port_by_id (&self, id: u32) -> Option> { - self.with_client(|c|c.port_by_id(id)) - } - fn register_port (&self, name: impl AsRef) -> Usually> { - self.with_client(|client|Ok(client.register_port(name.as_ref(), PS::default())?)) - } - fn sync_lead (&self, enable: bool, callback: impl Fn(TimebaseInfo)->Position) -> Usually<()> { - if enable { - self.with_client(|client|match client.register_timebase_callback(false, callback) { - Ok(_) => Ok(()), - Err(e) => Err(e) - })? - } - Ok(()) - } - fn sync_follow (&self, _enable: bool) -> Usually<()> { - // TODO: sync follow - Ok(()) - } -} -/// Trait for thing that has a JACK process callback. -pub trait Audio { - /// Handle a JACK event. - fn handle (&mut self, _event: JackEvent) {} - /// Projecss a JACK chunk. - fn process (&mut self, _: &Client, _: &ProcessScope) -> Control { - Control::Continue - } - /// The JACK process callback function passed to the server. - fn callback ( - state: &Arc>, client: &Client, scope: &ProcessScope - ) -> Control where Self: Sized { - if let Ok(mut state) = state.write() { - state.process(client, scope) - } else { - Control::Quit - } - } -} -/// Implement [Audio]: provide JACK callbacks. -#[macro_export] macro_rules! audio { - (| - $self1:ident: - $Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?,$c:ident,$s:ident - |$cb:expr$(;|$self2:ident,$e:ident|$cb2:expr)?) => { - impl $(<$($L),*$($T $(: $U)?),*>)? Audio for $Struct $(<$($L),*$($T),*>)? { - #[inline] fn process (&mut $self1, $c: &Client, $s: &ProcessScope) -> Control { $cb } - $(#[inline] fn handle (&mut $self2, $e: JackEvent) { $cb2 })? - } - } -} -/// Event enum for JACK events. -#[derive(Debug, Clone, PartialEq)] pub enum JackEvent { - ThreadInit, - Shutdown(ClientStatus, Arc), - Freewheel(bool), - SampleRate(Frames), - ClientRegistration(Arc, bool), - PortRegistration(PortId, bool), - PortRename(PortId, Arc, Arc), - PortsConnected(PortId, PortId, bool), - GraphReorder, - XRun, -} -/// Generic notification handler that emits [JackEvent] -pub struct Notifications(pub T); - -impl NotificationHandler for Notifications { - fn thread_init(&self, _: &Client) { - self.0(JackEvent::ThreadInit); - } - unsafe fn shutdown(&mut self, status: ClientStatus, reason: &str) { - self.0(JackEvent::Shutdown(status, reason.into())); - } - fn freewheel(&mut self, _: &Client, enabled: bool) { - self.0(JackEvent::Freewheel(enabled)); - } - fn sample_rate(&mut self, _: &Client, frames: Frames) -> Control { - self.0(JackEvent::SampleRate(frames)); - Control::Quit - } - fn client_registration(&mut self, _: &Client, name: &str, reg: bool) { - self.0(JackEvent::ClientRegistration(name.into(), reg)); - } - fn port_registration(&mut self, _: &Client, id: PortId, reg: bool) { - self.0(JackEvent::PortRegistration(id, reg)); - } - fn port_rename(&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control { - self.0(JackEvent::PortRename(id, old.into(), new.into())); - Control::Continue - } - fn ports_connected(&mut self, _: &Client, a: PortId, b: PortId, are: bool) { - self.0(JackEvent::PortsConnected(a, b, are)); - } - fn graph_reorder(&mut self, _: &Client) -> Control { - self.0(JackEvent::GraphReorder); - Control::Continue - } - fn xrun(&mut self, _: &Client) -> Control { - self.0(JackEvent::XRun); - Control::Continue - } -} - -/// This is a running JACK [AsyncClient] with maximum type erasure. -/// It has one [Box] containing a function that handles [JackEvent]s, -/// and another [Box] containing a function that handles realtime IO, -/// and that's all it knows about them. -pub type DynamicAsyncClient<'j> - = AsyncClient, DynamicAudioHandler<'j>>; -/// This is the notification handler wrapper for a boxed realtime callback. -pub type DynamicAudioHandler<'j> = - ClosureProcessHandler<(), BoxedAudioHandler<'j>>; -/// This is a boxed realtime callback. -pub type BoxedAudioHandler<'j> = - Box Control + Send + Sync + 'j>; -/// This is the notification handler wrapper for a boxed [JackEvent] callback. -pub type DynamicNotifications<'j> = - Notifications>; -/// This is a boxed [JackEvent] callback. -pub type BoxedJackEventHandler<'j> = - Box; -use self::JackState::*; - diff --git a/engine/midi.rs b/engine/midi.rs deleted file mode 100644 index 55835404..00000000 --- a/engine/midi.rs +++ /dev/null @@ -1,35 +0,0 @@ -pub use ::midly::{ - Smf, - TrackEventKind, - MidiMessage, - Error as MidiError, - num::*, - live::*, -}; - -/// Return boxed iterator of MIDI events -pub fn parse_midi_input <'a> (input: ::jack::MidiIter<'a>) -> Box, &'a [u8])> + 'a> { - Box::new(input.map(|::jack::RawMidi { time, bytes }|( - time as usize, - LiveEvent::parse(bytes).unwrap(), - bytes - ))) -} - -/// Add "all notes off" to the start of a buffer. -pub fn all_notes_off (output: &mut [Vec>]) { - let mut buf = vec![]; - let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() }; - let evt = LiveEvent::Midi { channel: 0.into(), message: msg }; - evt.write(&mut buf).unwrap(); - output[0].push(buf); -} - -/// Update notes_in array -pub fn update_keys (keys: &mut[bool;128], message: &MidiMessage) { - match message { - MidiMessage::NoteOn { key, .. } => { keys[key.as_int() as usize] = true; } - MidiMessage::NoteOff { key, .. } => { keys[key.as_int() as usize] = false; }, - _ => {} - } -} diff --git a/engine/note.rs b/engine/note.rs deleted file mode 100644 index e7e35888..00000000 --- a/engine/note.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod note_pitch; pub use self::note_pitch::*; -mod note_point; pub use self::note_point::*; -mod note_range; pub use self::note_range::*; diff --git a/engine/note/note_pitch.rs b/engine/note/note_pitch.rs deleted file mode 100644 index 06e455ce..00000000 --- a/engine/note/note_pitch.rs +++ /dev/null @@ -1,23 +0,0 @@ -pub struct Note; - -impl Note { - pub const NAMES: [&str; 128] = [ - "C0", "C#0", "D0", "D#0", "E0", "F0", "F#0", "G0", "G#0", "A0", "A#0", "B0", - "C1", "C#1", "D1", "D#1", "E1", "F1", "F#1", "G1", "G#1", "A1", "A#1", "B1", - "C2", "C#2", "D2", "D#2", "E2", "F2", "F#2", "G2", "G#2", "A2", "A#2", "B2", - "C3", "C#3", "D3", "D#3", "E3", "F3", "F#3", "G3", "G#3", "A3", "A#3", "B3", - "C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4", - "C5", "C#5", "D5", "D#5", "E5", "F5", "F#5", "G5", "G#5", "A5", "A#5", "B5", - "C6", "C#6", "D6", "D#6", "E6", "F6", "F#6", "G6", "G#6", "A6", "A#6", "B6", - "C7", "C#7", "D7", "D#7", "E7", "F7", "F#7", "G7", "G#7", "A7", "A#7", "B7", - "C8", "C#8", "D8", "D#8", "E8", "F8", "F#8", "G8", "G#8", "A8", "A#8", "B8", - "C9", "C#9", "D9", "D#9", "E9", "F9", "F#9", "G9", "G#9", "A9", "A#9", "B9", - "C10", "C#10", "D10", "D#10", "E10", "F10", "F#10", "G10", - ]; - pub fn pitch_to_name (n: usize) -> &'static str { - if n > 127 { - panic!("to_note_name({n}): must be 0-127"); - } - Self::NAMES[n] - } -} diff --git a/engine/note/note_point.rs b/engine/note/note_point.rs deleted file mode 100644 index 310cef87..00000000 --- a/engine/note/note_point.rs +++ /dev/null @@ -1,79 +0,0 @@ -use crate::*; - -#[derive(Debug, Clone)] -pub struct MidiPointModel { - /// Time coordinate of cursor - pub time_pos: Arc, - /// Note coordinate of cursor - pub note_pos: Arc, - /// Length of note that will be inserted, in pulses - pub note_len: Arc, -} - -impl Default for MidiPointModel { - fn default () -> Self { - Self { - time_pos: Arc::new(0.into()), - note_pos: Arc::new(36.into()), - note_len: Arc::new(24.into()), - } - } -} - -impl NotePoint for MidiPointModel { - fn note_len (&self) -> &AtomicUsize { - &self.note_len - } - fn note_pos (&self) -> &AtomicUsize { - &self.note_pos - } -} - -impl TimePoint for MidiPointModel { - fn time_pos (&self) -> &AtomicUsize { - self.time_pos.as_ref() - } -} - -pub trait NotePoint { - fn note_len (&self) -> &AtomicUsize; - /// Get the current length of the note cursor. - fn get_note_len (&self) -> usize { - self.note_len().load(Relaxed) - } - /// Set the length of the note cursor, returning the previous value. - fn set_note_len (&self, x: usize) -> usize { - self.note_len().swap(x, Relaxed) - } - - fn note_pos (&self) -> &AtomicUsize; - /// Get the current pitch of the note cursor. - fn get_note_pos (&self) -> usize { - self.note_pos().load(Relaxed).min(127) - } - /// Set the current pitch fo the note cursor, returning the previous value. - fn set_note_pos (&self, x: usize) -> usize { - self.note_pos().swap(x.min(127), Relaxed) - } -} - -pub trait TimePoint { - fn time_pos (&self) -> &AtomicUsize; - /// Get the current time position of the note cursor. - fn get_time_pos (&self) -> usize { - self.time_pos().load(Relaxed) - } - /// Set the current time position of the note cursor, returning the previous value. - fn set_time_pos (&self, x: usize) -> usize { - self.time_pos().swap(x, Relaxed) - } -} - -pub trait MidiPoint: NotePoint + TimePoint { - /// Get the current end of the note cursor. - fn get_note_end (&self) -> usize { - self.get_time_pos() + self.get_note_len() - } -} - -impl MidiPoint for T {} diff --git a/engine/src/engine.rs b/engine/src/engine.rs new file mode 100644 index 00000000..886f9670 --- /dev/null +++ b/engine/src/engine.rs @@ -0,0 +1,161 @@ +use crate::*; +use std::fmt::{Debug, Display}; +use std::ops::{Add, Sub, Mul, Div}; + +/// Platform backend. +pub trait Engine: Send + Sync + Sized { + /// Input event type + type Input: Input; + /// Result of handling input + type Handled; + /// Render target + type Output: Output; + /// Unit of length + type Unit: Coordinate; + /// Rectangle without offset + type Size: Size + From<[Self::Unit;2]> + Debug + Copy; + /// Rectangle with offset + type Area: Area + From<[Self::Unit;4]> + Debug + Copy; + /// Prepare before run + fn setup (&mut self) -> Usually<()> { Ok(()) } + /// True if done + fn exited (&self) -> bool; + /// Clean up after run + fn teardown (&mut self) -> Usually<()> { Ok(()) } +} + +/// A linear coordinate. +pub trait Coordinate: Send + Sync + Copy + + Add + + Sub + + Mul + + Div + + Ord + PartialEq + Eq + + Debug + Display + Default + + From + Into + + Into + + Into +{ + #[inline] fn minus (self, other: Self) -> Self { + if self >= other { + self - other + } else { + 0.into() + } + } + #[inline] fn zero () -> Self { + 0.into() + } +} + +pub trait Size { + fn x (&self) -> N; + fn y (&self) -> N; + #[inline] fn w (&self) -> N { self.x() } + #[inline] fn h (&self) -> N { self.y() } + #[inline] fn wh (&self) -> [N;2] { [self.x(), self.y()] } + #[inline] fn clip_w (&self, w: N) -> [N;2] { [self.w().min(w), self.h()] } + #[inline] fn clip_h (&self, h: N) -> [N;2] { [self.w(), self.h().min(h)] } + #[inline] fn expect_min (&self, w: N, h: N) -> Usually<&Self> { + if self.w() < w || self.h() < h { + Err(format!("min {w}x{h}").into()) + } else { + Ok(self) + } + } + #[inline] fn zero () -> [N;2] { + [N::zero(), N::zero()] + } +} + +impl Size for (N, N) { + #[inline] fn x (&self) -> N { self.0 } + #[inline] fn y (&self) -> N { self.1 } +} + +impl Size for [N;2] { + #[inline] fn x (&self) -> N { self[0] } + #[inline] fn y (&self) -> N { self[1] } +} + +pub trait Area { + fn x (&self) -> N; + fn y (&self) -> N; + fn w (&self) -> N; + fn h (&self) -> N; + #[inline] fn expect_min (&self, w: N, h: N) -> Usually<&Self> { + if self.w() < w || self.h() < h { + Err(format!("min {w}x{h}").into()) + } else { + Ok(self) + } + } + #[inline] fn xy (&self) -> [N;2] { + [self.x(), self.y()] + } + #[inline] fn wh (&self) -> [N;2] { + [self.w(), self.h()] + } + #[inline] fn xywh (&self) -> [N;4] { + [self.x(), self.y(), self.w(), self.h()] + } + #[inline] fn clip_h (&self, h: N) -> [N;4] { + [self.x(), self.y(), self.w(), self.h().min(h)] + } + #[inline] fn clip_w (&self, w: N) -> [N;4] { + [self.x(), self.y(), self.w().min(w), self.h()] + } + #[inline] fn clip (&self, wh: impl Size) -> [N;4] { + [self.x(), self.y(), wh.w(), wh.h()] + } + #[inline] fn set_w (&self, w: N) -> [N;4] { + [self.x(), self.y(), w, self.h()] + } + #[inline] fn set_h (&self, h: N) -> [N;4] { + [self.x(), self.y(), self.w(), h] + } + #[inline] fn x2 (&self) -> N { + self.x() + self.w() + } + #[inline] fn y2 (&self) -> N { + self.y() + self.h() + } + #[inline] fn lrtb (&self) -> [N;4] { + [self.x(), self.x2(), self.y(), self.y2()] + } + #[inline] fn center (&self) -> [N;2] { + [self.x() + self.w()/2.into(), self.y() + self.h()/2.into()] + } + #[inline] fn center_x (&self, n: N) -> [N;4] { + let [x, y, w, h] = self.xywh(); + [(x + w / 2.into()).minus(n / 2.into()), y + h / 2.into(), n, 1.into()] + } + #[inline] fn center_y (&self, m: N) -> [N;4] { + let [x, y, w, h] = self.xywh(); + [x + w / 2.into(), (y + h / 2.into()).minus(m / 2.into()), 1.into(), m] + } + #[inline] fn center_xy (&self, [n, m]: [N;2]) -> [N;4] { + let [x, y, w, h] = self.xywh(); + [(x + w / 2.into()).minus(n / 2.into()), (y + h / 2.into()).minus(m / 2.into()), n, m] + } + #[inline] fn centered (&self) -> [N;2] { + [self.x().minus(self.w()/2.into()), self.y().minus(self.h()/2.into())] + } + fn zero () -> [N;4] { + [N::zero(), N::zero(), N::zero(), N::zero()] + } +} + +impl Area for (N, N, N, N) { + #[inline] fn x (&self) -> N { self.0 } + #[inline] fn y (&self) -> N { self.1 } + #[inline] fn w (&self) -> N { self.2 } + #[inline] fn h (&self) -> N { self.3 } +} + +impl Area for [N;4] { + #[inline] fn x (&self) -> N { self[0] } + #[inline] fn y (&self) -> N { self[1] } + #[inline] fn w (&self) -> N { self[2] } + #[inline] fn h (&self) -> N { self[3] } +} diff --git a/engine/src/input.rs b/engine/src/input.rs new file mode 100644 index 00000000..6a77db37 --- /dev/null +++ b/engine/src/input.rs @@ -0,0 +1,69 @@ +use crate::*; +use std::sync::{Arc, Mutex, RwLock}; + +/// Handle input +pub trait Handle: Send + Sync { + fn handle (&mut self, context: &E::Input) -> Perhaps; +} + +/// Current input state +pub trait Input { + /// Type of input event + type Event; + /// Currently handled event + fn event (&self) -> &Self::Event; + /// Whether component should exit + fn is_done (&self) -> bool; + /// Mark component as done + fn done (&self); +} + +#[macro_export] macro_rules! handle { + (<$E:ty>|$self:ident:$Struct:ty,$input:ident|$handler:expr) => { + impl Handle<$E> for $Struct { + fn handle (&mut $self, $input: &<$E as Engine>::Input) -> Perhaps<<$E as Engine>::Handled> { + $handler + } + } + } +} + +impl> Handle for &mut H { + fn handle (&mut self, context: &E::Input) -> Perhaps { + (*self).handle(context) + } +} + +impl> Handle for Option { + fn handle (&mut self, context: &E::Input) -> Perhaps { + if let Some(ref mut handle) = self { + handle.handle(context) + } else { + Ok(None) + } + } +} + +impl Handle for Mutex where H: Handle { + fn handle (&mut self, context: &E::Input) -> Perhaps { + self.get_mut().unwrap().handle(context) + } +} + +impl Handle for Arc> where H: Handle { + fn handle (&mut self, context: &E::Input) -> Perhaps { + self.lock().unwrap().handle(context) + } +} + +impl Handle for RwLock where H: Handle { + fn handle (&mut self, context: &E::Input) -> Perhaps { + self.write().unwrap().handle(context) + } +} + +impl Handle for Arc> where H: Handle { + fn handle (&mut self, context: &E::Input) -> Perhaps { + self.write().unwrap().handle(context) + } +} diff --git a/engine/src/lib.rs b/engine/src/lib.rs new file mode 100644 index 00000000..9a89169b --- /dev/null +++ b/engine/src/lib.rs @@ -0,0 +1,85 @@ +//mod component; pub use self::component::*; +mod engine; pub use self::engine::*; +mod input; pub use self::input::*; +mod output; pub use self::output::*; + +pub mod tui; + +pub use std::error::Error; + +/// Standard result type. +pub type Usually = Result>; + +/// Standard optional result type. +pub type Perhaps = Result, Box>; + +#[cfg(test)] #[test] fn test_dimensions () { + assert_eq!(Area::center(&[10u16, 10, 20, 20]), [20, 20]); +} + +#[cfg(test)] #[test] fn test_stub_engine () -> Usually<()> { + struct TestEngine(bool); + struct TestInput(bool); + struct TestOutput([u16;4]); + enum TestEvent { Test1 } + impl Engine for TestEngine { + type Input = TestInput; + type Handled = (); + type Output = TestOutput; + type Unit = u16; + type Size = [u16;2]; + type Area = [u16;4]; + fn exited (&self) -> bool { + self.0 + } + } + impl Input for TestInput { + type Event = TestEvent; + fn event (&self) -> &Self::Event { + &TestEvent::Test1 + } + fn is_done (&self) -> bool { + self.0 + } + fn done (&self) {} + } + impl Output for TestOutput { + fn area (&self) -> [u16;4] { + self.0 + } + fn area_mut (&mut self) -> &mut [u16;4] { + &mut self.0 + } + fn place (&mut self, _: [u16;4], _: &impl Content) { + () + } + } + impl Content for String { + fn render (&self, to: &mut TestOutput) { + to.area_mut().set_w(self.len() as u16); + } + } + Ok(()) +} + +#[cfg(test)] #[test] fn test_tui_engine () -> Usually<()> { + use crate::tui::*; + use std::sync::{Arc, RwLock}; + struct TestComponent(String); + impl Content for TestComponent { + fn content (&self) -> Option> { + Some(self.0.as_str()) + } + } + impl Handle for TestComponent { + fn handle (&mut self, from: &TuiIn) -> Perhaps { + Ok(None) + } + } + let engine = Tui::new()?; + engine.read().unwrap().exited.store(true, std::sync::atomic::Ordering::Relaxed); + let state = TestComponent("hello world".into()); + let state = std::sync::Arc::new(std::sync::RwLock::new(state)); + engine.run(&state)?; + Ok(()) +} diff --git a/engine/src/output.rs b/engine/src/output.rs new file mode 100644 index 00000000..2433140d --- /dev/null +++ b/engine/src/output.rs @@ -0,0 +1,118 @@ +use crate::*; +use std::marker::PhantomData; +//use std::sync::{Arc, Mutex, RwLock}; + +/// Rendering target +pub trait Output { + /// Current output area + fn area (&self) -> E::Area; + /// Mutable pointer to area + fn area_mut (&mut self) -> &mut E::Area; + /// Render widget in area + fn place (&mut self, area: E::Area, content: &impl Content); + + #[inline] fn x (&self) -> E::Unit { self.area().x() } + #[inline] fn y (&self) -> E::Unit { self.area().y() } + #[inline] fn w (&self) -> E::Unit { self.area().w() } + #[inline] fn h (&self) -> E::Unit { self.area().h() } + #[inline] fn wh (&self) -> E::Size { self.area().wh().into() } +} + +pub trait Content: Send + Sync { + fn content (&self) -> impl Content { + () + } + fn layout (&self, area: E::Area) -> E::Area { + self.content().layout(area) + } + fn render (&self, output: &mut E::Output) { + output.place(self.layout(output.area()), &self.content()) + } +} + +/// The platonic ideal unit of [Content]: total emptiness at dead center. +impl Content for () { + fn layout (&self, area: E::Area) -> E::Area { + let [x, y] = area.center(); + [x, y, 0.into(), 0.into()].into() + } + fn render (&self, _: &mut E::Output) {} +} + +impl> Content for &T { + fn content (&self) -> impl Content { + (*self).content() + } + fn layout (&self, area: E::Area) -> E::Area { + (*self).layout(area) + } + fn render (&self, output: &mut E::Output) { + (*self).render(output) + } +} + +impl> Content for Option { + fn content (&self) -> impl Content { + self.as_ref() + .map(|content|content.content()) + } + fn layout (&self, area: E::Area) -> E::Area { + self.as_ref() + .map(|content|content.layout(area)) + .unwrap_or([0.into(), 0.into(), 0.into(), 0.into(),].into()) + } + fn render (&self, output: &mut E::Output) { + self.as_ref() + .map(|content|content.render(output)); + } +} + +pub struct Thunk, F: Fn()->T + Send + Sync>(F, PhantomData); + +impl, F: Fn()->T + Send + Sync> Thunk { + pub fn new (thunk: F) -> Self { + Self(thunk, Default::default()) + } +} +impl, F: Fn()->T + Send + Sync> Content for Thunk { + fn content (&self) -> impl Content { + (self.0)() + } +} + +#[macro_export] macro_rules! render { + (($self:ident:$Struct:ty) => $content:expr) => { + impl Content for $Struct { + fn content (&$self) -> impl Content { Some($content) } + } + }; + (|$self:ident:$Struct:ident $(< + $($L:lifetime),* $($T:ident $(:$Trait:path)?),* + >)?, $to:ident | $render:expr) => { + impl <$($($L),*)? E: Engine, $($T$(:$Trait)?),*> Content + for $Struct $(<$($L),* $($T),*>>)? { + fn render (&$self, $to: &mut E::Output) { $render } + } + }; + ($Engine:ty: + ($self:ident:$Struct:ident $(<$( + $($L:lifetime)? $($T:ident)? $(:$Trait:path)? + ),+>)?) => $content:expr + ) => { + impl $(<$($($L)? $($T)? $(:$Trait)?),+>)? Content<$Engine> + for $Struct $(<$($($L)? $($T)?),+>)? { + fn content (&$self) -> impl Content<$Engine> { $content } + } + }; + + ($Engine:ty: + |$self:ident : $Struct:ident $(<$( + $($L:lifetime)? $($T:ident)? $(:$Trait:path)? + ),+>)?, $to:ident| $render:expr + ) => { + impl $(<$($($L)? $($T)? $(:$Trait)?),+>)? Content<$Engine> + for $Struct $(<$($($L)? $($T)?),+>)? { + fn render (&$self, $to: &mut <$Engine as Engine>::Output) { $render } + } + }; +} diff --git a/engine/src/tui.rs b/engine/src/tui.rs new file mode 100644 index 00000000..7158e9e6 --- /dev/null +++ b/engine/src/tui.rs @@ -0,0 +1,164 @@ +mod tui_output; pub use self::tui_output::*; +mod tui_input; pub use self::tui_input::*; + +use crate::*; +use std::sync::{Arc, RwLock, atomic::{AtomicBool, Ordering::*}}; +use std::io::{stdout, Stdout}; +use std::time::Duration; +use std::thread::{spawn, JoinHandle}; + +pub use ::better_panic; +pub(crate) use better_panic::{Settings, Verbosity}; + +pub use ::crossterm; +pub(crate) use crossterm::{ + ExecutableCommand, + terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode}, + event::{KeyCode, KeyModifiers, KeyEvent, KeyEventKind, KeyEventState}, +}; + +pub use ::ratatui; +pub(crate) use ratatui::{ + prelude::{Color, Style, Buffer}, + style::Modifier, + backend::{Backend, CrosstermBackend, ClearType}, + layout::{Size, Rect}, + buffer::Cell +}; + +impl Coordinate for u16 {} + +pub struct Tui { + pub exited: Arc, + pub buffer: Buffer, + pub backend: CrosstermBackend, + pub area: [u16;4], // FIXME auto resize +} + +impl Engine for Tui { + type Unit = u16; + type Size = [Self::Unit;2]; + type Area = [Self::Unit;4]; + type Input = TuiIn; + type Handled = bool; + type Output = TuiOut; + fn exited (&self) -> bool { + self.exited.fetch_and(true, Relaxed) + } + fn setup (&mut self) -> Usually<()> { + let better_panic_handler = Settings::auto().verbosity(Verbosity::Full).create_panic_handler(); + std::panic::set_hook(Box::new(move |info: &std::panic::PanicHookInfo|{ + stdout().execute(LeaveAlternateScreen).unwrap(); + CrosstermBackend::new(stdout()).show_cursor().unwrap(); + disable_raw_mode().unwrap(); + better_panic_handler(info); + })); + stdout().execute(EnterAlternateScreen)?; + self.backend.hide_cursor()?; + enable_raw_mode().map_err(Into::into) + } + fn teardown (&mut self) -> Usually<()> { + stdout().execute(LeaveAlternateScreen)?; + self.backend.show_cursor()?; + disable_raw_mode().map_err(Into::into) + } +} + +impl Tui { + /// Construct a new TUI engine and wrap it for shared ownership. + pub fn new () -> Usually>> { + let backend = CrosstermBackend::new(stdout()); + let Size { width, height } = backend.size()?; + Ok(Arc::new(RwLock::new(Self { + exited: Arc::new(AtomicBool::new(false)), + buffer: Buffer::empty(Rect { x: 0, y: 0, width, height }), + area: [0, 0, width, height], + backend, + }))) + } + /// Update the display buffer. + fn flip (&mut self, mut buffer: Buffer, size: ratatui::prelude::Rect) -> Buffer { + if self.buffer.area != size { + self.backend.clear_region(ClearType::All).unwrap(); + self.buffer.resize(size); + self.buffer.reset(); + } + let updates = self.buffer.diff(&buffer); + self.backend.draw(updates.into_iter()).expect("failed to render"); + self.backend.flush().expect("failed to flush output buffer"); + std::mem::swap(&mut self.buffer, &mut buffer); + buffer.reset(); + buffer + } +} + +pub trait TuiRun + Handle + Sized + 'static> { + /// Run an app in the main loop. + fn run (&self, state: &Arc>) -> Usually<()>; + /// Spawn the input thread. + fn run_input (&self, state: &Arc>, poll: Duration) -> JoinHandle<()>; + /// Spawn the output thread. + fn run_output (&self, state: &Arc>, sleep: Duration) -> JoinHandle<()>; +} + +impl + Handle + Sized + 'static> TuiRun for Arc> { + fn run (&self, state: &Arc>) -> Usually<()> { + let _input_thread = self.run_input(state, Duration::from_millis(100)); + self.write().unwrap().setup()?; + let render_thread = self.run_output(state, Duration::from_millis(10)); + render_thread.join().expect("main thread failed"); + self.write().unwrap().teardown()?; + Ok(()) + } + fn run_input (&self, state: &Arc>, poll: Duration) -> JoinHandle<()> { + let exited = self.read().unwrap().exited.clone(); + let state = state.clone(); + spawn(move || loop { + if exited.fetch_and(true, Relaxed) { + break + } + if ::crossterm::event::poll(poll).is_ok() { + let event = ::crossterm::event::read().unwrap(); + match event { + kpat!(Ctrl-KeyCode::Char('c')) => { + exited.store(true, Relaxed); + }, + _ => { + let exited = exited.clone(); + if let Err(e) = state.write().unwrap().handle(&TuiIn { event, exited }) { + panic!("{e}") + } + } + } + } + }) + } + fn run_output (&self, state: &Arc>, sleep: Duration) -> JoinHandle<()> { + let exited = self.read().unwrap().exited.clone(); + let engine = self.clone(); + let state = state.clone(); + let Size { width, height } = engine.read().unwrap().backend.size().expect("get size failed"); + let mut buffer = Buffer::empty(Rect { x: 0, y: 0, width, height }); + spawn(move || loop { + if exited.fetch_and(true, Relaxed) { + break + } + let Size { width, height } = engine.read().unwrap().backend.size() + .expect("get size failed"); + if let Ok(state) = state.try_read() { + let size = Rect { x: 0, y: 0, width, height }; + if buffer.area != size { + engine.write().unwrap().backend.clear_region(ClearType::All) + .expect("clear failed"); + buffer.resize(size); + buffer.reset(); + } + let mut output = TuiOut { buffer, area: [0, 0, width, height] }; + state.render(&mut output); + buffer = engine.write().unwrap().flip(output.buffer, size); + } + std::thread::sleep(sleep); + }) + } +} + diff --git a/engine/src/tui/tui_input.rs b/engine/src/tui/tui_input.rs new file mode 100644 index 00000000..a15feeb3 --- /dev/null +++ b/engine/src/tui/tui_input.rs @@ -0,0 +1,113 @@ +use crate::{*, tui::*}; +pub use crossterm::event::Event; + +#[derive(Debug, Clone)] +pub struct TuiIn { + pub(crate) exited: Arc, + pub(crate) event: Event, +} + +impl Input for TuiIn { + type Event = Event; + fn event (&self) -> &Event { + &self.event + } + fn is_done (&self) -> bool { + self.exited.fetch_and(true, Relaxed) + } + fn done (&self) { + self.exited.store(true, Relaxed); + } +} + +/// Define a key +pub const fn key (code: KeyCode) -> Event { + let modifiers = KeyModifiers::NONE; + let kind = KeyEventKind::Press; + let state = KeyEventState::NONE; + Event::Key(KeyEvent { code, modifiers, kind, state }) +} + +/// Add Ctrl modifier to key +pub const fn ctrl (event: Event) -> Event { + match event { + Event::Key(mut event) => { + event.modifiers = event.modifiers.union(KeyModifiers::CONTROL) + }, + _ => {} + } + event +} + +/// Add Alt modifier to key +pub const fn alt (event: Event) -> Event { + match event { + Event::Key(mut event) => { + event.modifiers = event.modifiers.union(KeyModifiers::ALT) + }, + _ => {} + } + event +} + +/// Add Shift modifier to key +pub const fn shift (event: Event) -> Event { + match event { + Event::Key(mut event) => { + event.modifiers = event.modifiers.union(KeyModifiers::SHIFT) + }, + _ => {} + } + event +} + +#[macro_export] macro_rules! kpat { + (Ctrl-Alt-$code:pat) => { kpat!($code, KeyModifiers::CONTROL | KeyModifiers::ALT) }; + (Ctrl-$code:pat) => { kpat!($code, KeyModifiers::CONTROL) }; + (Alt-$code:pat) => { kpat!($code, KeyModifiers::ALT) }; + (Shift-$code:pat) => { kpat!($code, KeyModifiers::SHIFT) }; + ($code:pat) => { + crossterm::event::Event::Key(KeyEvent { + code: $code, + modifiers: KeyModifiers::NONE, + kind: KeyEventKind::Press, + state: KeyEventState::NONE + }) + }; + ($code:pat, $modifiers: pat) => { + crossterm::event::Event::Key(KeyEvent { + code: $code, + modifiers: $modifiers, + kind: KeyEventKind::Press, + state: KeyEventState::NONE + }) + }; +} + +#[macro_export] macro_rules! kexp { + (Ctrl-Alt-$code:ident) => { key_event_expr!($code, KeyModifiers::from_bits(0b0000_0110).unwrap()) }; + (Ctrl-$code:ident) => { key_event_expr!($code, KeyModifiers::CONTROL) }; + (Alt-$code:ident) => { key_event_expr!($code, KeyModifiers::ALT) }; + (Shift-$code:ident) => { key_event_expr!($code, KeyModifiers::SHIFT) }; + ($code:ident) => { key_event_expr!($code) }; + ($code:expr) => { key_event_expr!($code) }; +} + +#[macro_export] macro_rules! key_event_expr { + ($code:expr, $modifiers: expr) => { + crossterm::event::Event::Key(KeyEvent { + code: $code, + modifiers: $modifiers, + kind: KeyEventKind::Press, + state: KeyEventState::NONE + }) + }; + ($code:expr) => { + crossterm::event::Event::Key(KeyEvent { + code: $code, + modifiers: KeyModifiers::NONE, + kind: KeyEventKind::Press, + state: KeyEventState::NONE + }) + }; +} diff --git a/engine/src/tui/tui_output.rs b/engine/src/tui/tui_output.rs new file mode 100644 index 00000000..bb11a0f2 --- /dev/null +++ b/engine/src/tui/tui_output.rs @@ -0,0 +1,116 @@ +use crate::{*, tui::*}; + +pub struct TuiOut { + pub buffer: Buffer, + pub area: [u16;4] +} + +impl Output for TuiOut { + #[inline] fn area (&self) -> [u16;4] { self.area } + #[inline] fn area_mut (&mut self) -> &mut [u16;4] { &mut self.area } + #[inline] fn place (&mut self, area: [u16;4], content: &impl Content) { + let last = self.area(); + *self.area_mut() = area; + content.render(self); + *self.area_mut() = last; + } +} + +impl TuiOut { + pub fn buffer_update (&mut self, area: [u16;4], callback: &impl Fn(&mut Cell, u16, u16)) { + buffer_update(&mut self.buffer, area, callback); + } + pub fn fill_bold (&mut self, area: [u16;4], on: bool) { + if on { + self.buffer_update(area, &|cell,_,_|cell.modifier.insert(Modifier::BOLD)) + } else { + self.buffer_update(area, &|cell,_,_|cell.modifier.remove(Modifier::BOLD)) + } + } + pub fn fill_bg (&mut self, area: [u16;4], color: Color) { + self.buffer_update(area, &|cell,_,_|{cell.set_bg(color);}) + } + pub fn fill_fg (&mut self, area: [u16;4], color: Color) { + self.buffer_update(area, &|cell,_,_|{cell.set_fg(color);}) + } + pub fn fill_ul (&mut self, area: [u16;4], color: Color) { + self.buffer_update(area, &|cell,_,_|{ + cell.modifier = ratatui::prelude::Modifier::UNDERLINED; + cell.underline_color = color; + }) + } + pub fn fill_char (&mut self, area: [u16;4], c: char) { + self.buffer_update(area, &|cell,_,_|{cell.set_char(c);}) + } + pub fn make_dim (&mut self) { + for cell in self.buffer.content.iter_mut() { + cell.bg = ratatui::style::Color::Rgb(30,30,30); + cell.fg = ratatui::style::Color::Rgb(100,100,100); + cell.modifier = ratatui::style::Modifier::DIM; + } + } + pub fn blit ( + &mut self, text: &impl AsRef, x: u16, y: u16, style: Option