diff --git a/.dockerignore b/.dockerignore index e69de29b..72e8ffc0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -0,0 +1 @@ +* diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..7b08ef33 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,3 @@ +root = true +[*] +max_line_length = 132 diff --git a/.envrc b/.envrc new file mode 100644 index 00000000..1d953f4b --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use nix diff --git a/.forgejo/workflows/build.nix b/.forgejo/workflows/build.nix deleted file mode 100644 index cb702884..00000000 --- a/.forgejo/workflows/build.nix +++ /dev/null @@ -1,38 +0,0 @@ -{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 deleted file mode 100644 index 2eed869f..00000000 --- a/.forgejo/workflows/build.yaml +++ /dev/null @@ -1,11 +0,0 @@ -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 new file mode 100644 index 00000000..e2371720 --- /dev/null +++ b/.forgejo/workflows/release.yml.off @@ -0,0 +1,36 @@ +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 new file mode 100644 index 00000000..16d33122 --- /dev/null +++ b/.forgejo/workflows/test.yaml @@ -0,0 +1,49 @@ +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 1a417612..e5790860 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,15 @@ -target +target/* +!target/.gitkeep 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 3c5a123f..15f065ba 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,3 +2,9 @@ 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/examples/demo.rs.fixme b/.old/demo.rs.old similarity index 72% rename from examples/demo.rs.fixme rename to .old/demo.rs.old index ce3c6556..6b205580 100644 --- a/examples/demo.rs.fixme +++ b/.old/demo.rs.old @@ -14,20 +14,7 @@ impl Demo { fn new () -> Self { Self { index: 0, - items: vec![ - //Box::new(tek_sequencer::TransportPlayPauseButton { - //_engine: Default::default(), - //transport: None, - //value: Some(TransportState::Stopped), - //focused: true - //}), - //Box::new(tek_sequencer::TransportPlayPauseButton { - //_engine: Default::default(), - //transport: None, - //value: Some(TransportState::Rolling), - //focused: false - //}), - ] + items: vec![] } } } @@ -104,7 +91,7 @@ impl Content for Demo { } } -impl Handle for Demo { +impl Handle for Demo { fn handle (&mut self, from: &TuiIn) -> Perhaps { use KeyCode::{PageUp, PageDown}; match from.event() { @@ -123,22 +110,3 @@ impl Handle for Demo { Ok(Some(true)) } } - -//lisp!(CONTENT Demo (LET - //(BORDER-STYLE (STYLE (FG (RGB 0 0 0)))) - //(BG-COLOR-0 (RGB 0 128 128)) - //(BG-COLOR-1 (RGB 128 96 0)) - //(BG-COLOR-2 (RGB 128 64 0)) - //(BG-COLOR-3 (RGB 96 64 0)) - //(CENTER (LAYERS - //(BACKGROUND BG-COLOR-0) - //(OUTSET-XY 1 1 (SPLIT-DOWN - //(LAYERS (BACKGROUND BG-COLOR-1) - //(BORDER SQUARE BORDER-STYLE) - //(OUTSET-XY 2 1 "...")) - //(LAYERS (BACKGROUND BG-COLOR-2) - //(BORDER LOZENGE BORDER-STYLE) - //(OUTSET-XY 4 2 "---")) - //(LAYERS (BACKGROUND BG-COLOR-3) - //(BORDER SQUARE-BOLD BORDER-STYLE) - //(OUTSET-XY 2 1 "~~~")))))))) diff --git a/.old/from_arranger.rs b/.old/from_arranger.rs new file mode 100644 index 00000000..07502a6b --- /dev/null +++ b/.old/from_arranger.rs @@ -0,0 +1,188 @@ + + +//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 new file mode 100644 index 00000000..8602766a --- /dev/null +++ b/.old/midi.scratch.rs @@ -0,0 +1,31 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +//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 new file mode 100644 index 00000000..d2cceae2 --- /dev/null +++ b/.old/midi_import.rs @@ -0,0 +1,20 @@ +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 new file mode 100644 index 00000000..82009355 --- /dev/null +++ b/.old/sampler_scratch.rs @@ -0,0 +1,105 @@ +//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/.scratch.rs b/.old/scratch.rs similarity index 100% rename from .scratch.rs rename to .old/scratch.rs diff --git a/.old/tek.rs.old b/.old/tek.rs.old new file mode 100644 index 00000000..e64fd51b --- /dev/null +++ b/.old/tek.rs.old @@ -0,0 +1,2113 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// + +//#[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 new file mode 100644 index 00000000..81fc7dc7 --- /dev/null +++ b/.old/todo_arranger.edn @@ -0,0 +1,83 @@ + 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/bin/todo_cli_mixer.rs b/.old/todo_cli_mixer.rs similarity index 100% rename from bin/todo_cli_mixer.rs rename to .old/todo_cli_mixer.rs diff --git a/bin/todo_cli_plugin.rs b/.old/todo_cli_plugin.rs similarity index 100% rename from bin/todo_cli_plugin.rs rename to .old/todo_cli_plugin.rs diff --git a/bin/todo_cli_sampler.rs b/.old/todo_cli_sampler.rs similarity index 100% rename from bin/todo_cli_sampler.rs rename to .old/todo_cli_sampler.rs diff --git a/examples/sequencer.edn b/.old/todo_project0.edn similarity index 100% rename from examples/sequencer.edn rename to .old/todo_project0.edn diff --git a/examples/mixer.edn b/.old/todo_project1.edn similarity index 100% rename from examples/mixer.edn rename to .old/todo_project1.edn diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..f40cf8e8 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,18 @@ +## 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 c73f9036..34c84187 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,22 @@ # 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" @@ -13,9 +29,22 @@ dependencies = [ [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +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", +] [[package]] name = "allocator-api2" @@ -24,10 +53,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] -name = "anstream" -version = "0.6.18" +name = "android-activity" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" dependencies = [ "anstyle", "anstyle-parse", @@ -40,36 +96,37 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.6" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", - "windows-sys 0.59.0", + "once_cell_polyfill", + "windows-sys 0.60.2", ] [[package]] @@ -81,12 +138,30 @@ 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" @@ -95,15 +170,15 @@ checksum = "628d228f918ac3b82fe590352cc719d30664a0c13ca3a60266fe02c7132d480a" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", "cfg-if", @@ -111,7 +186,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -124,6 +199,21 @@ 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" @@ -132,15 +222,24 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2", +] [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "by_address" @@ -150,15 +249,41 @@ checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" [[package]] name = "bytemuck" -version = "1.21.0" +version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" +checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" [[package]] -name = "byteorder" -version = "1.5.0" +name = "bytes" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +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", +] [[package]] name = "cassowary" @@ -168,24 +293,47 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "castaway" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" dependencies = [ "rustversion", ] [[package]] -name = "cfg-if" -version = "1.0.0" +name = "cc" +version = "1.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +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" +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" [[package]] name = "clap" -version = "4.5.23" +version = "4.5.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" +checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" dependencies = [ "clap_builder", "clap_derive", @@ -193,9 +341,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.23" +version = "4.5.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" +checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" dependencies = [ "anstream", "anstyle", @@ -205,9 +353,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.18" +version = "4.5.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" dependencies = [ "heck", "proc-macro2", @@ -217,24 +365,25 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -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", -] +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] [[package]] name = "compact_str" @@ -251,10 +400,19 @@ dependencies = [ ] [[package]] -name = "console" -version = "0.15.10" +name = "concurrent-queue" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" dependencies = [ "encode_unicode", "libc", @@ -262,6 +420,86 @@ 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" @@ -293,11 +531,29 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.3", "crossterm_winapi", "mio", - "parking_lot 0.12.3", - "rustix", + "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", "signal-hook", "signal-hook-mio", "winapi", @@ -313,10 +569,26 @@ dependencies = [ ] [[package]] -name = "darling" -version = "0.20.10" +name = "ctor" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ "darling_core", "darling_macro", @@ -324,9 +596,9 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" dependencies = [ "fnv", "ident_case", @@ -338,9 +610,9 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", @@ -348,16 +620,67 @@ dependencies = [ ] [[package]] -name = "diff" -version = "0.1.13" +name = "derive_more" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +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" [[package]] name = "either" -version = "1.13.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "encode_unicode" @@ -376,18 +699,18 @@ dependencies = [ [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.10" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -402,6 +725,12 @@ 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" @@ -410,19 +739,68 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" +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", +] [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", - "wasi", + "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", ] [[package]] @@ -433,9 +811,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", @@ -448,6 +826,12 @@ 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" @@ -466,9 +850,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "indexmap" -version = "2.7.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" dependencies = [ "equivalent", "hashbrown", @@ -476,19 +860,18 @@ dependencies = [ [[package]] name = "indoc" -version = "2.0.5" +version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" +checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" [[package]] name = "instability" -version = "0.3.5" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "898e106451f7335950c9cc64f8ec67b5f65698679ac67ed00619aeef14e1cf75" +checksum = "435d80800b936787d62688c927b6490e887c7ef5ff9ce922c6c6050fca75eb9a" dependencies = [ "darling", "indoc", - "pretty_assertions", "proc-macro2", "quote", "syn", @@ -519,16 +902,28 @@ dependencies = [ ] [[package]] -name = "itoa" -version = "1.0.14" +name = "itertools" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jack" version = "0.13.0" dependencies = [ - "bitflags 2.6.0", + "approx", + "bitflags 2.9.3", + "crossbeam-channel", + "ctor", "jack-sys", "lazy_static", "libc", @@ -539,7 +934,7 @@ dependencies = [ name = "jack-sys" version = "0.5.1" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.3", "lazy_static", "libc", "libloading", @@ -548,15 +943,74 @@ dependencies = [ ] [[package]] -name = "js-sys" -version = "0.3.76" +name = "jni" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 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" @@ -565,18 +1019,29 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.169" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libloading" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets", + "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", ] [[package]] @@ -602,9 +1067,21 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +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" [[package]] name = "livi" @@ -621,9 +1098,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", @@ -631,9 +1108,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "lru" @@ -661,9 +1138,18 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "memmap2" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" +dependencies = [ + "libc", +] [[package]] name = "midly" @@ -676,23 +1162,53 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.8.2" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", ] [[package]] name = "mio" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", "log", - "wasi", - "windows-sys 0.52.0", + "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", ] [[package]] @@ -704,6 +1220,231 @@ 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" @@ -715,17 +1456,32 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.2" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] -name = "ordered-float" -version = "4.6.0" +name = "once_cell_polyfill" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "orbclient" +version = "0.3.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba0b26cec2e24f08ed8bb31519a9333140a6599b867dac464bb150bdb796fd43" dependencies = [ - "num-traits", + "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", ] [[package]] @@ -738,7 +1494,7 @@ dependencies = [ "fast-srgb8", "palette_derive", "phf", - "rand", + "rand 0.8.5", ] [[package]] @@ -766,12 +1522,12 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", - "parking_lot_core 0.9.10", + "parking_lot_core 0.9.11", ] [[package]] @@ -790,15 +1546,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.8", + "redox_syscall 0.5.17", "smallvec", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -808,10 +1564,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] -name = "phf" -version = "0.11.2" +name = "percent-encoding" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ "phf_macros", "phf_shared", @@ -819,19 +1581,19 @@ dependencies = [ [[package]] name = "phf_generator" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", - "rand", + "rand 0.8.5", ] [[package]] name = "phf_macros" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" dependencies = [ "phf_generator", "phf_shared", @@ -842,71 +1604,162 @@ dependencies = [ [[package]] name = "phf_shared" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ "siphasher", ] [[package]] -name = "pkg-config" -version = "0.3.31" +name = "pin-project" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +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" +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", +] [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] [[package]] -name = "pretty_assertions" -version = "1.4.1" +name = "proc-macro-crate" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" dependencies = [ - "diff", - "yansi", + "toml_edit", ] [[package]] name = "proc-macro2" -version = "1.0.92" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] [[package]] -name = "quanta" -version = "0.12.4" +name = "proptest" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773ce68d0bb9bc7ef20be3536ffe94e223e1f365bd374108b2659fac0c65cfe6" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" dependencies = [ "crossbeam-utils", "libc", "once_cell", "raw-cpuid", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "web-sys", "winapi", ] [[package]] -name = "quote" -version = "1.0.38" +name = "quick-error" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 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" @@ -914,8 +1767,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "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", ] [[package]] @@ -925,7 +1788,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "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", ] [[package]] @@ -934,7 +1807,25 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "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", ] [[package]] @@ -943,13 +1834,13 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.3", "cassowary", "compact_str", - "crossterm", + "crossterm 0.28.1", "indoc", "instability", - "itertools", + "itertools 0.13.0", "lru", "paste", "strum", @@ -960,18 +1851,24 @@ dependencies = [ [[package]] name = "raw-cpuid" -version = "11.2.0" +version = "11.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ab240315c661615f2ee9f0f2cd32d5a7343a84d5ebcccb99d46e6637565e7b0" +checksum = "c6df7ab838ed27997ba19a4664507e6f82b41fe6e20be42929332156e5e85146" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.3", ] [[package]] -name = "rayon" -version = "1.10.0" +name = "raw-window-handle" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -979,9 +1876,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -998,13 +1895,28 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.8" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ - "bitflags 2.6.0", + "bitflags 1.3.2", ] +[[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" @@ -1016,34 +1928,74 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustix" -version = "0.38.42" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.3", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.4.15", "windows-sys 0.59.0", ] [[package]] -name = "rustversion" -version = "1.0.19" +name = "rustix" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" +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" +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", +] [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +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" [[package]] name = "scopeguard" @@ -1052,19 +2004,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] -name = "serde" -version = "1.0.217" +name = "sctk-adwaita" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +dependencies = [ + "ab_glyph", + "log", + "memmap2", + "smithay-client-toolkit", + "tiny-skia", +] + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", @@ -1073,18 +2038,24 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.8" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" dependencies = [ "serde", ] [[package]] -name = "signal-hook" -version = "0.3.17" +name = "shlex" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" dependencies = [ "libc", "signal-hook-registry", @@ -1103,24 +2074,64 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] [[package]] name = "siphasher" -version = "0.3.11" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" -version = "1.13.2" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +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", +] [[package]] name = "static_assertions" @@ -1128,6 +2139,12 @@ 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" @@ -1353,9 +2370,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.93" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c786062daee0d6db1132800e623df74274a0a87322d8e183338e01b3d98d058" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -1364,40 +2381,137 @@ dependencies = [ [[package]] name = "tek" -version = "0.2.0" +version = "0.3.0" dependencies = [ "atomic_float", "backtrace", "clap", - "clojure-reader", "jack", + "konst", "livi", "midly", - "once_cell", "palette", - "quanta", - "rand", + "proptest", + "proptest-derive", + "rand 0.8.5", "symphonia", - "tek_layout", + "tek_device", + "tengri", "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.2.0" +version = "0.3.0" dependencies = [ - "better-panic", - "crossterm", - "ratatui", + "atomic_float", + "jack", + "midly", + "proptest", + "proptest-derive", + "tengri", ] [[package]] -name = "tek_layout" -version = "0.2.0" +name = "tempfile" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" dependencies = [ - "tek_engine", + "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", + "ratatui", + "tengri_core", + "tengri_dsl", + "tengri_input", + "tengri_output", + "unicode-width 0.2.0", ] [[package]] @@ -1406,7 +2520,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "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", ] [[package]] @@ -1421,44 +2544,145 @@ dependencies = [ ] [[package]] -name = "toml" -version = "0.8.19" +name = "thiserror-impl" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" +dependencies = [ + "indexmap", "serde", "serde_spanned", - "toml_datetime", - "toml_edit", + "toml_datetime 0.7.0", + "toml_parser", + "toml_writer", + "winnow", ] [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_datetime" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.22" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", - "serde", - "serde_spanned", - "toml_datetime", + "toml_datetime 0.6.11", "winnow", ] [[package]] -name = "unicode-ident" -version = "1.0.14" +name = "toml_parser" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-segmentation" @@ -1472,7 +2696,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" dependencies = [ - "itertools", + "itertools 0.13.0", "unicode-segmentation", "unicode-width 0.1.14", ] @@ -1489,6 +2713,12 @@ 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" @@ -1497,35 +2727,72 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.11.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" dependencies = [ - "getrandom", + "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", ] [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +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", +] [[package]] name = "wasm-bindgen" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", @@ -1536,10 +2803,23 @@ dependencies = [ ] [[package]] -name = "wasm-bindgen-macro" -version = "0.2.99" +name = "wasm-bindgen-futures" +version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1547,9 +2827,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", @@ -1560,9 +2840,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "wavers" @@ -1574,14 +2857,133 @@ dependencies = [ "i24", "num-traits", "paste", - "thiserror", + "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", ] [[package]] name = "web-sys" -version = "0.3.76" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" +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" dependencies = [ "js-sys", "wasm-bindgen", @@ -1603,19 +3005,43 @@ 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", + "windows-targets 0.52.6", ] [[package]] @@ -1624,7 +3050,46 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.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", ] [[package]] @@ -1633,58 +3098,201 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.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" @@ -1692,35 +3300,158 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "winnow" -version = "0.6.20" +name = "windows_x86_64_msvc" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] [[package]] -name = "yansi" -version = "1.0.1" +name = "wit-bindgen-rt" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +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" [[package]] name = "zerocopy" -version = "0.7.35" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ - "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 3fcd44a1..9f379dc6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,66 +1,53 @@ -[package] -name = "tek" -edition = "2021" -version = "0.2.0" +[workspace] +resolver = "2" +members = [ "./app", "./engine", "./device" ] +exclude = [ "./deps/tengri" ] -[dependencies] -tek_layout = { path = "./layout" } +[workspace.package] +edition = "2024" +version = "0.3.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" +[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" #no_deadlocks = "1.3.2" #suil-rs = { path = "../suil" } #vst = "0.4.0" #vst3 = "0.1.0" -#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 +proptest = { version = "^1" } +proptest-derive = { version = "^0.5.1" } diff --git a/Justfile b/Justfile index 6c5ebfa0..94551690 100644 --- a/Justfile +++ b/Justfile @@ -1,120 +1,117 @@ -default: - just -l +export RUSTFLAGS := "--cfg procmacro2_semver_exempt -Zmacro-backtrace -Clink-arg=-fuse-ld=mold" +export RUST_BACKTRACE := "1" -status: - cargo c - cloc --by-file src/ - git status +default: + @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 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 -transport: - reset - cargo run --bin tek_transport -transport-release: - reset - cargo run --release --bin tek_transport +name := "-n tek" +bpm := "-b 174" +clock: + {{debug}} {{name}} {{bpm}} clock +clock-release: + {{release}} {{name}} {{bpm}} clock +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: - reset - cargo run --bin tek_arranger + {{debug}} {{name}} {{bpm}} arranger arranger-ext: - 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" + {{debug}} {{name}} {{bpm}} {{midi-in}} {{midi-out}} arranger arranger-release: - reset - cargo run --release --bin tek_arranger + {{release}} {{name}} {{bpm}} arranger arranger-release-ext: - 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" + {{release}} {{name}} {{bpm}} {{midi-in}} {{firefox-in}} {{midi-out}} arranger groovebox: - reset - cargo run --bin tek_groovebox -- -b 174 + {{debug}} {{name}} {{bpm}} groovebox groovebox-ext: reset - 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" + {{debug}} {{name}} {{bpm}} {{midi-in}} {{midi-out}} {{audio-in}} {{audio-out}} groovebox +groovebox-browser: + {{debug}} {{name}} {{bpm}} {{audio-in}} groovebox groovebox-release: - reset - cargo run --release --bin tek_groovebox + {{release}} {{name}} {{bpm}} groovebox groovebox-release-ext: - 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" + {{release}} {{name}} {{bpm}} {{midi-in}} {{midi-out}} {{audio-in}} {{audio-out}} groovebox groovebox-release-ext-browser: - 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" + {{release}} {{name}} {{bpm}} {{midi-in}} {{firefox-in}} {{audio-out}} groovebox sequencer: - reset - cargo run --bin tek_sequencer + {{debug}} {{name}} {{bpm}} sequencer sequencer-ext: - 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" + {{debug}} {{name}} {{bpm}} {{midi-in}} {{midi-out}} sequencer sequencer-release: - reset - cargo run --release --bin tek_sequencer + {{release}} {{name}} {{bpm}} sequencer sequencer-release-ext: - 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" + {{release}} {{name}} {{bpm}} {{midi-in}} {{midi-out}} sequencer mixer: - reset - cargo run --bin tek_mixer + {{debug}} mixer track: - reset - cargo run --bin tek_track + {{debug}} track sampler: - reset - cargo run --bin tek_sampler + {{debug}} sampler plugin: - reset - cargo run --bin tek_plugin + {{debug}} plugin diff --git a/LICENSE b/LICENSE index 4b61f9fc..be3f7b28 100644 --- a/LICENSE +++ b/LICENSE @@ -1,8 +1,661 @@ -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. + 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 +. diff --git a/README.md b/README.md index 710719a0..64655591 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). -hmu on [**mastodon** `@unspeaker@mastodon.social`](https://mastodon.social/@unspeaker) +author is reachable via [**mastodon** `@unspeaker@mastodon.social`](https://mastodon.social/@unspeaker) or [**matrix** `@unspeaker:matrix.org`](https://matrix.to/#/@unspeaker:matrix.org) -![Screenshot](https://codeberg.org/unspeaker/tek/attachments/549efab7-f46b-438b-9508-cd499d044b41) +| | | +|-|-| +|![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)| -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 +## usage * **requirements:** linux; jack or pipewire; 24-bit terminal (i use `kitty`) * **recommended:** midi controller; samples in wav format; lv2 plugins. -### arch linux +## keymaps -[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`): +* 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`): ```sh paru -S tek ``` -### downloads - -see the [releases page](https://codeberg.org/unspeaker/tek/releases). - ### building from source -you'll need a Rust toolchain and various system libraries. +requires docker. -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 +``` +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 ``` ## design goals -### lightweight +* inspired by trackers and hardware sequencers, + but with the critical feature that 90s samplers lack: + able to **resample, i.e. record while playing!** -* pop-up scratchpad for musical ideas -* low resource consumption, can stay open in background -* advanced toolset allows quickly expanding on compositions +* **pop-up scratchpad for musical ideas.** + low resource consumption, can stay open in background. + but flexible enough to allow expanding on compositions -### 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) +* **human- and machine- readable project format** + simple representation for project data + enable scripting and remapping. diff --git a/app/Cargo.toml b/app/Cargo.toml new file mode 100644 index 00000000..d6c3afa9 --- /dev/null +++ b/app/Cargo.toml @@ -0,0 +1,58 @@ +[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 new file mode 100644 index 00000000..17b02a6c --- /dev/null +++ b/app/tek.edn @@ -0,0 +1,224 @@ +(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 new file mode 100644 index 00000000..1ce0c252 --- /dev/null +++ b/app/tek.rs @@ -0,0 +1,468 @@ +#![feature( + adt_const_params, + associated_type_defaults, + closure_lifetime_binder, + if_let_guard, + impl_trait_in_assoc_type, + trait_alias, + type_alias_impl_trait, + type_changing_struct_update, +)] + +#![allow( + clippy::unit_arg +)] + +#[cfg(test)] mod tek_test; + +mod tek_bind; pub use self::tek_bind::*; +mod tek_cfg; pub use self::tek_cfg::*; +mod tek_deps; pub use self::tek_deps::*; +mod tek_mode; pub use self::tek_mode::*; +mod tek_view; pub use self::tek_view::*; + +/// Total state +#[derive(Default, Debug)] +pub struct App { + /// Base color. + pub color: ItemTheme, + /// Must not be dropped for the duration of the process + pub jack: Jack<'static>, + /// Display size + pub size: Measure, + /// Performance counter + pub perf: PerfModel, + /// Available view modes and input bindings + pub config: Config, + /// Currently selected mode + pub mode: Arc>>, + /// Undo history + pub history: Vec<(AppCommand, Option)>, + /// Dialog overlay + pub dialog: Dialog, + /// Contains all recently created clips. + pub pool: Pool, + /// Contains the currently edited musical arrangement + pub project: Arrangement, +} + +audio!( + |self: App, client, scope|{ + let t0 = self.perf.get_t0(); + self.clock().update_from_scope(scope).unwrap(); + let midi_in = self.project.midi_input_collect(scope); + if let Some(editor) = &self.editor() { + let mut pitch: Option = None; + for port in midi_in.iter() { + for event in port.iter() { + if let (_, Ok(LiveEvent::Midi {message: MidiMessage::NoteOn {key, ..}, ..})) + = event + { + pitch = Some(key.clone()); + } + } + } + if let Some(pitch) = pitch { + editor.set_note_pos(pitch.as_int() as usize); + } + } + let result = self.project.process_tracks(client, scope); + self.perf.update_from_jack_scope(t0, scope); + result + }; + |self, event|{ + use JackEvent::*; + match event { + SampleRate(sr) => { self.clock().timebase.sr.set(sr as f64); }, + PortRegistration(_id, true) => { + //let port = self.jack().port_by_id(id); + //println!("\rport add: {id} {port:?}"); + //println!("\rport add: {id}"); + }, + PortRegistration(_id, false) => { + /*println!("\rport del: {id}")*/ + }, + PortsConnected(_a, _b, true) => { /*println!("\rport conn: {a} {b}")*/ }, + PortsConnected(_a, _b, false) => { /*println!("\rport disc: {a} {b}")*/ }, + ClientRegistration(_id, true) => {}, + ClientRegistration(_id, false) => {}, + ThreadInit => {}, + XRun => {}, + GraphReorder => {}, + _ => { panic!("{event:?}"); } + } + } +); + +// Allow source to be read as Literal string +dsl_ns!(App: Arc { + literal = |dsl|Ok(dsl.src()?.map(|x|x.into())); +}); + +// Provide boolean values. +dsl_ns!(App: bool { + // TODO literal = ... + word = |app| { + ":mode/editor" => app.project.editor.is_some(), + ":focused/dialog" => !matches!(app.dialog, Dialog::None), + ":focused/message" => matches!(app.dialog, Dialog::Message(..)), + ":focused/add_device" => matches!(app.dialog, Dialog::Device(..)), + ":focused/browser" => app.dialog.browser().is_some(), + ":focused/pool/import" => matches!(app.pool.mode, Some(PoolMode::Import(..))), + ":focused/pool/export" => matches!(app.pool.mode, Some(PoolMode::Export(..))), + ":focused/pool/rename" => matches!(app.pool.mode, Some(PoolMode::Rename(..))), + ":focused/pool/length" => matches!(app.pool.mode, Some(PoolMode::Length(..))), + ":focused/clip" => !app.editor_focused() && matches!(app.selection(), + Selection::TrackClip{..}), + ":focused/track" => !app.editor_focused() && matches!(app.selection(), + Selection::Track(..)), + ":focused/scene" => !app.editor_focused() && matches!(app.selection(), + Selection::Scene(..)), + ":focused/mix" => !app.editor_focused() && matches!(app.selection(), + Selection::Mix), + }; +}); + +// TODO: provide colors here +dsl_ns!(App: ItemTheme {}); + +dsl_ns!(App: Dialog { + word = |app| { + ":dialog/none" => Dialog::None, + ":dialog/options" => Dialog::Options, + ":dialog/device" => Dialog::Device(0), + ":dialog/device/prev" => Dialog::Device(0), + ":dialog/device/next" => Dialog::Device(0), + ":dialog/help" => Dialog::Help(0), + ":dialog/save" => Dialog::Browse(BrowseTarget::SaveProject, + Browse::new(None).unwrap().into()), + ":dialog/load" => Dialog::Browse(BrowseTarget::LoadProject, + Browse::new(None).unwrap().into()), + ":dialog/import/clip" => Dialog::Browse(BrowseTarget::ImportClip(Default::default()), + Browse::new(None).unwrap().into()), + ":dialog/export/clip" => Dialog::Browse(BrowseTarget::ExportClip(Default::default()), + Browse::new(None).unwrap().into()), + ":dialog/import/sample" => Dialog::Browse(BrowseTarget::ImportSample(Default::default()), + Browse::new(None).unwrap().into()), + ":dialog/export/sample" => Dialog::Browse(BrowseTarget::ExportSample(Default::default()), + Browse::new(None).unwrap().into()), + }; +}); + +dsl_ns!(App: Selection { + word = |app| { + ":select/scene" => app.selection().select_scene(app.tracks().len()), + ":select/scene/next" => app.selection().select_scene_next(app.scenes().len()), + ":select/scene/prev" => app.selection().select_scene_prev(), + ":select/track" => app.selection().select_track(app.tracks().len()), + ":select/track/next" => app.selection().select_track_next(app.tracks().len()), + ":select/track/prev" => app.selection().select_track_prev(), + }; +}); + +dsl_ns!(App: Color { + word = |app| { + ":color/bg" => Color::Rgb(28, 32, 36), + }; + expr = |app| { + "g" (n: u8) => Color::Rgb(n, n, n), + "rgb" (r: u8, g: u8, b: u8) => Color::Rgb(r, g, b), + }; +}); + +dsl_ns!(App: Option { + word = |app| { + ":editor/pitch" => Some( + (app.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into() + ) + }; +}); + +dsl_ns!(App: Option { + word = |app| { + ":selected/scene" => app.selection().scene(), + ":selected/track" => app.selection().track(), + }; +}); + +dsl_ns!(App: Option>> { + word = |app| { + ":selected/clip" => if let Selection::TrackClip { track, scene } = app.selection() { + app.scenes()[*scene].clips[*track].clone() + } else { + None + } + }; +}); + +dsl_ns!(App: u8 { + literal = |dsl|Ok(if let Some(src) = dsl.src()? { + Some(to_number(src)? as u8) + } else { + None + }); +}); + +dsl_ns!(App: u16 { + literal = |dsl|Ok(if let Some(src) = dsl.src()? { + Some(to_number(src)? as u16) + } else { + None + }); + word = |app| { + ":w/sidebar" => app.project.w_sidebar(app.editor().is_some()), + ":h/sample-detail" => 6.max(app.height() as u16 * 3 / 9), + }; +}); + +dsl_ns!(App: usize { + literal = |dsl|Ok(if let Some(src) = dsl.src()? { + Some(to_number(src)? as usize) + } else { + None + }); + word = |app| { + ":scene-count" => app.scenes().len(), + ":track-count" => app.tracks().len(), + ":device-kind" => app.dialog.device_kind().unwrap_or(0), + ":device-kind/next" => app.dialog.device_kind_next().unwrap_or(0), + ":device-kind/prev" => app.dialog.device_kind_prev().unwrap_or(0), + }; +}); + +dsl_ns!(App: isize { + literal = |dsl|Ok(if let Some(src) = dsl.src()? { + Some(to_number(src)? as isize) + } else { + None + }); +}); + +has!(Jack<'static>: |self: App|self.jack); +has!(Pool: |self: App|self.pool); +has!(Dialog: |self: App|self.dialog); +has!(Clock: |self: App|self.project.clock); +has!(Option: |self: App|self.project.editor); +has!(Selection: |self: App|self.project.selection); +has!(Vec: |self: App|self.project.midi_ins); +has!(Vec: |self: App|self.project.midi_outs); +has!(Vec: |self: App|self.project.scenes); +has!(Vec: |self: App|self.project.tracks); +has!(Measure: |self: App|self.size); +has_clips!( |self: App|self.pool.clips); +maybe_has!(Track: |self: App| { MaybeHas::::get(&self.project) }; + { MaybeHas::::get_mut(&mut self.project) }); +maybe_has!(Scene: |self: App| { MaybeHas::::get(&self.project) }; + { MaybeHas::::get_mut(&mut self.project) }); + +impl HasClipsSize for App { + fn clips_size (&self) -> &Measure { &self.project.size_inner } +} +impl HasTrackScroll for App { + fn track_scroll (&self) -> usize { self.project.track_scroll() } +} +impl HasSceneScroll for App { + fn scene_scroll (&self) -> usize { self.project.scene_scroll() } +} +impl HasJack<'static> for App { + fn jack (&self) -> &Jack<'static> { &self.jack } +} +impl ScenesView for App { + fn w_side (&self) -> u16 { 20 } + fn w_mid (&self) -> u16 { (self.width() as u16).saturating_sub(self.w_side()) } + fn h_scenes (&self) -> u16 { (self.height() as u16).saturating_sub(20) } +} + + +impl App { + + pub fn editor_focused (&self) -> bool { + false + } + + pub fn toggle_dialog (&mut self, mut dialog: Dialog) -> Dialog { + std::mem::swap(&mut self.dialog, &mut dialog); + dialog + } + + pub fn toggle_editor (&mut self, value: Option) { + //FIXME: self.editing.store(value.unwrap_or_else(||!self.is_editing()), Relaxed); + let value = value.unwrap_or_else(||!self.editor().is_some()); + if value { + // Create new clip in pool when entering empty cell + if let Selection::TrackClip { track, scene } = *self.selection() + && let Some(scene) = self.project.scenes.get_mut(scene) + && let Some(slot) = scene.clips.get_mut(track) + && slot.is_none() + && let Some(track) = self.project.tracks.get_mut(track) + { + let (index, mut clip) = self.pool.add_new_clip(); + // autocolor: new clip colors from scene and track color + let color = track.color.base.mix(scene.color.base, 0.5); + clip.write().unwrap().color = ItemColor::random_near(color, 0.2).into(); + if let Some(editor) = &mut self.project.editor { + editor.set_clip(Some(&clip)); + } + *slot = Some(clip.clone()); + //Some(clip) + } else { + //None + } + } else if let Selection::TrackClip { track, scene } = *self.selection() + && let Some(scene) = self.project.scenes.get_mut(scene) + && let Some(slot) = scene.clips.get_mut(track) + && let Some(clip) = slot.as_mut() + { + // Remove clip from arrangement when exiting empty clip editor + let mut swapped = None; + if clip.read().unwrap().count_midi_messages() == 0 { + std::mem::swap(&mut swapped, slot); + } + if let Some(clip) = swapped { + self.pool.delete_clip(&clip.read().unwrap()); + } + } + } + + pub fn browser (&self) -> Option<&Browse> { + if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None } + } + + pub fn device_pick (&mut self, index: usize) { + self.dialog = Dialog::Device(index); + } + + pub fn add_device (&mut self, index: usize) -> Usually<()> { + match index { + 0 => { + let name = self.jack.with_client(|c|c.name().to_string()); + let midi = self.project.track().expect("no active track").sequencer.midi_outs[0].port_name(); + let track = self.track().expect("no active track"); + let port = format!("{}/Sampler", &track.name); + let connect = Connect::exact(format!("{name}:{midi}")); + let sampler = if let Ok(sampler) = Sampler::new( + &self.jack, &port, &[connect], &[&[], &[]], &[&[], &[]] + ) { + self.dialog = Dialog::None; + Device::Sampler(sampler) + } else { + self.dialog = Dialog::Message("Failed to add device.".into()); + return Err("failed to add device".into()) + }; + let track = self.track_mut().expect("no active track"); + track.devices.push(sampler); + Ok(()) + }, + 1 => { + todo!(); + Ok(()) + }, + _ => unreachable!(), + } + } + + pub fn update_clock (&self) { + ViewCache::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80) + } +} + +/// Various possible dialog modes. +#[derive(Debug, Clone, Default, PartialEq)] +pub enum Dialog { + #[default] None, + Help(usize), + Menu(usize, MenuItems), + Device(usize), + Message(Arc), + Browse(BrowseTarget, Arc), + Options, +} + +#[derive(Debug, Clone, Default, PartialEq)] +pub struct MenuItems(pub Arc<[MenuItem]>); + +impl AsRef> for MenuItems { + fn as_ref (&self) -> &Arc<[MenuItem]> { + &self.0 + } +} + +#[derive(Clone)] +pub struct MenuItem( + /// Label + pub Arc, + /// Callback + pub ArcUsually<()> + Send + Sync>> +); + +impl Default for MenuItem { + fn default () -> Self { Self("".into(), Arc::new(Box::new(|_|Ok(())))) } +} + +impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) }); + +impl PartialEq for MenuItem { + fn eq (&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl Dialog { + pub fn welcome () -> Self { + Self::Menu(1, MenuItems([ + MenuItem("Resume session".into(), Arc::new(Box::new(|_|Ok(())))), + MenuItem("Create new session".into(), Arc::new(Box::new(|app|Ok({ + app.dialog = Dialog::None; + app.mode = app.config.modes.clone().read().unwrap().get(":arranger").cloned().unwrap(); + })))), + MenuItem("Load old session".into(), Arc::new(Box::new(|_|Ok(())))), + ].into())) + } + pub fn menu_next (&self) -> Self { + match self { + Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.0.len()), items.clone()), + _ => Self::None + } + } + pub fn menu_prev (&self) -> Self { + match self { + Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.0.len()), items.clone()), + _ => Self::None + } + } + pub fn menu_selected (&self) -> Option { + if let Self::Menu(selected, _) = self { Some(*selected) } else { None } + } + pub fn device_kind (&self) -> Option { + if let Self::Device(index) = self { Some(*index) } else { None } + } + pub fn device_kind_next (&self) -> Option { + self.device_kind().map(|index|(index + 1) % device_kinds().len()) + } + pub fn device_kind_prev (&self) -> Option { + self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1))) + } + pub fn message (&self) -> Option<&str> { + todo!() + } + pub fn browser (&self) -> Option<&Arc> { + todo!() + } + pub fn browser_target (&self) -> Option<&BrowseTarget> { + todo!() + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +//has_editor!(|self: App|{ + //editor = self.editor; + //editor_w = { + //let size = self.size.w(); + //let editor = self.editor.as_ref().expect("missing editor"); + //let time_len = editor.time_len().get(); + //let time_zoom = editor.time_zoom().get().max(1); + //(5 + (time_len / time_zoom)).min(size.saturating_sub(20)).max(16) + //}; + //editor_h = 15; + //is_editing = self.editor.is_some(); +//}); diff --git a/app/tek_bind.rs b/app/tek_bind.rs new file mode 100644 index 00000000..84763b70 --- /dev/null +++ b/app/tek_bind.rs @@ -0,0 +1,325 @@ +use crate::*; + +pub type Binds = Arc, EventMap>>>>; + +/// A collection of input bindings. +#[derive(Debug)] +pub struct EventMap( + /// Map of each event (e.g. key combination) to + /// all command expressions bound to it by + /// all loaded input layers. + pub BTreeMap>> +); + +/// An input binding. +#[derive(Debug, Clone)] +pub struct Binding { + pub commands: Arc<[C]>, + pub condition: Option, + pub description: Option>, + pub source: Option>, +} + +/// Input bindings are only returned if this evaluates to true +#[derive(Clone)] +pub struct Condition(Arcbool + Send + Sync>>); + +/// Default is always empty map regardless if `E` and `C` implement [Default]. +impl Default for EventMap { + fn default () -> Self { Self(Default::default()) } +} + +impl EventMap { + /// Create a new event map + pub fn new () -> Self { + Default::default() + } + /// Add a binding to an owned event map. + pub fn def (mut self, event: E, binding: Binding) -> Self { + self.add(event, binding); + self + } + /// Add a binding to an event map. + pub fn add (&mut self, event: E, binding: Binding) -> &mut Self { + if !self.0.contains_key(&event) { + self.0.insert(event.clone(), Default::default()); + } + self.0.get_mut(&event).unwrap().push(binding); + self + } + /// Return the binding(s) that correspond to an event. + pub fn query (&self, event: &E) -> Option<&[Binding]> { + self.0.get(event).map(|x|x.as_slice()) + } + /// Return the first binding that corresponds to an event, considering conditions. + pub fn dispatch (&self, event: &E) -> Option<&Binding> { + self.query(event) + .map(|bb|bb.iter().filter(|b|b.condition.as_ref().map(|c|(c.0)()).unwrap_or(true)).next()) + .flatten() + } +} + +impl EventMap> { + pub fn load_into (binds: &Binds, name: &impl AsRef, body: &impl Dsl) -> Usually<()> { + println!("EventMap::load_into: {}: {body:?}", name.as_ref()); + let mut map = Self::new(); + body.each(|item|if item.expr().head() == Ok(Some("see")) { + // TODO + Ok(()) + } else if let Ok(Some(_word)) = item.expr().head().word() { + if let Some(key) = TuiEvent::from_dsl(item.expr()?.head()?)? { + map.add(key, Binding { + commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(), + condition: None, + description: None, + source: None + }); + Ok(()) + } else if Some(":char") == item.expr()?.head()? { + // TODO + return Ok(()) + } else { + return Err(format!("Config::load_bind: invalid key: {:?}", item.expr()?.head()?).into()) + } + } else { + return Err(format!("Config::load_bind: unexpected: {item:?}").into()) + })?; + binds.write().unwrap().insert(name.as_ref().into(), map); + Ok(()) + } +} + +impl Binding { + pub fn from_dsl (dsl: impl Dsl) -> Usually { + let command: Option = None; + let condition: Option = None; + let description: Option> = None; + let source: Option> = None; + if let Some(command) = command { + Ok(Self { commands: [command].into(), condition, description, source }) + } else { + Err(format!("no command in {dsl:?}").into()) + } + } +} + +impl_debug!(Condition |self, w| { write!(w, "*") }); + +handle!(TuiIn:|self: App, input|{ + let mut commands = vec![]; + for id in self.mode.keys.iter() { + if let Some(event_map) = self.config.binds.clone().read().unwrap().get(id.as_ref()) { + if let Some(bindings) = event_map.query(input.event()) { + for binding in bindings { + for command in binding.commands.iter() { + if let Some(command) = self.from(command)? as Option { + commands.push(command) + } + } + } + } + } + } + for command in commands.into_iter() { + let result = command.execute(self); + match result { + Ok(undo) => { + self.history.push((command, undo)); + }, + Err(e) => { + self.history.push((command, None)); + return Err(e) + } + } + } + Ok(None) +}); + +#[derive(Debug, Copy, Clone)] +pub enum Axis { X, Y, Z, I } + +impl<'a> DslNs<'a, AppCommand> for App {} +impl<'a> DslNsExprs<'a, AppCommand> for App {} +impl<'a> DslNsWords<'a, AppCommand> for App { + dsl_words!('a |app| -> AppCommand { + "x/inc" => AppCommand::Inc { axis: Axis::X }, + "x/dec" => AppCommand::Dec { axis: Axis::X }, + "y/inc" => AppCommand::Inc { axis: Axis::Y }, + "y/dec" => AppCommand::Dec { axis: Axis::Y }, + "confirm" => AppCommand::Confirm, + "cancel" => AppCommand::Cancel, + }); +} + +impl Default for AppCommand { fn default () -> Self { Self::Nop } } + +def_command!(AppCommand: |app: App| { + Nop => Ok(None), + Confirm => Ok(match &app.dialog { + Dialog::Menu(index, items) => { + let callback = items.0[*index].1.clone(); + callback(app)?; + None + }, + _ => todo!(), + }), + Cancel => todo!(), // TODO delegate: + Inc { axis: Axis } => Ok(match (&app.dialog, axis) { + (Dialog::None, _) => todo!(), + (Dialog::Menu(_, _), Axis::Y) => AppCommand::SetDialog { dialog: app.dialog.menu_next() } + .execute(app)?, + _ => todo!() + }), + Dec { axis: Axis } => Ok(match (&app.dialog, axis) { + (Dialog::None, _) => None, + (Dialog::Menu(_, _), Axis::Y) => AppCommand::SetDialog { dialog: app.dialog.menu_prev() } + .execute(app)?, + _ => todo!() + }), + SetDialog { dialog: Dialog } => { + swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog }) + }, +}); + + //AppCommand => { + //("x/inc" / + //("stop-all") => todo!(),//app.project.stop_all(), + //("enqueue", clip: Option>>) => todo!(), + //("history", delta: isize) => todo!(), + //("zoom", zoom: usize) => todo!(), + //("select", selection: Selection) => todo!(), + //("dialog" / command: DialogCommand) => todo!(), + //("project" / command: ArrangementCommand) => todo!(), + //("clock" / command: ClockCommand) => todo!(), + //("sampler" / command: SamplerCommand) => todo!(), + //("pool" / command: PoolCommand) => todo!(), + //("edit" / editor: MidiEditCommand) => todo!(), + //}; + + //DialogCommand; + + //ArrangementCommand; + + //ClockCommand; + + //SamplerCommand; + + //PoolCommand; + + //MidiEditCommand; + + +//take!(DialogCommand |state: App, iter|Take::take(&state.dialog, iter)); +//#[derive(Clone, Debug)] +//pub enum DialogCommand { + //Open { dialog: Dialog }, + //Close +//} + +//impl Command> for DialogCommand { + //fn execute (self, state: &mut Option) -> Perhaps { + //match self { + //Self::Open { dialog } => { + //*state = Some(dialog); + //}, + //Self::Close => { + //*state = None; + //} + //}; + //Ok(None) + //} +//} + +//dsl!(DialogCommand: |self: Dialog, iter|todo!()); +//Dsl::take(&mut self.dialog, iter)); + +//#[tengri_proc::command(Option)]//Nope. +//impl DialogCommand { + //fn open (dialog: &mut Option, new: Dialog) -> Perhaps { + //*dialog = Some(new); + //Ok(None) + //} + //fn close (dialog: &mut Option) -> Perhaps { + //*dialog = None; + //Ok(None) + //} +//} +// +//dsl_bind!(AppCommand: App { + //enqueue = |app, clip: Option>>| { todo!() }; + //history = |app, delta: isize| { todo!() }; + //zoom = |app, zoom: usize| { todo!() }; + //stop_all = |app| { app.tracks_stop_all(); Ok(None) }; + ////dialog = |app, command: DialogCommand| + ////Ok(command.delegate(&mut app.dialog, |c|Self::Dialog{command: c})?); + //project = |app, command: ArrangementCommand| + //Ok(command.delegate(&mut app.project, |c|Self::Project{command: c})?); + //clock = |app, command: ClockCommand| + //Ok(command.execute(app.clock_mut())?.map(|c|Self::Clock{command: c})); + //sampler = |app, command: SamplerCommand| + //Ok(app.project.sampler_mut().map(|s|command.delegate(s, |command|Self::Sampler{command})) + //.transpose()?.flatten()); + //pool = |app, command: PoolCommand| { + //let undo = command.clone().delegate(&mut app.pool, |command|AppCommand::Pool{command})?; + //// update linked editor after pool action + //match command { + //// autoselect: automatically load selected clip in editor + //PoolCommand::Select { .. } | + //// autocolor: update color in all places simultaneously + //PoolCommand::Clip { command: PoolClipCommand::SetColor { .. } } => { + //let clip = app.pool.clip().clone(); + //app.editor_mut().map(|editor|editor.set_clip(clip.as_ref())) + //}, + //_ => None + //}; + //Ok(undo) + //}; + //select = |app, selection: Selection| { + //*app.project.selection_mut() = selection; + ////todo! + ////if let Some(ref mut editor) = app.editor_mut() { + ////editor.set_clip(match selection { + ////Selection::TrackClip { track, scene } if let Some(Some(Some(clip))) = app + ////.project + ////.scenes.get(scene) + ////.map(|s|s.clips.get(track)) + ////=> + ////Some(clip), + ////_ => + ////None + ////}); + ////} + //Ok(None) + ////("select" [t: usize, s: usize] Some(match (t.expect("no track"), s.expect("no scene")) { + ////(0, 0) => Self::Select(Selection::Mix), + ////(t, 0) => Self::Select(Selection::Track(t)), + ////(0, s) => Self::Select(Selection::Scene(s)), + ////(t, s) => Self::Select(Selection::TrackClip { track: t, scene: s }) }))) + //// autoedit: load focused clip in editor. + //}; + ////fn color (app: &mut App, theme: ItemTheme) -> Perhaps { + ////Ok(app.set_color(Some(theme)).map(|theme|Self::Color{theme})) + ////} + ////fn launch (app: &mut App) -> Perhaps { + ////app.project.launch(); + ////Ok(None) + ////} + //toggle_editor = |app, value: bool|{ app.toggle_editor(Some(value)); Ok(None) }; + //editor = |app, command: MidiEditCommand| Ok(if let Some(editor) = app.editor_mut() { + //let undo = command.clone().delegate(editor, |command|AppCommand::Editor{command})?; + //// update linked sampler after editor action + //app.project.sampler_mut().map(|sampler|match command { + //// autoselect: automatically select sample in sampler + //MidiEditCommand::SetNotePos { pos } => { sampler.set_note_pos(pos); }, + //_ => {} + //}); + //undo + //} else { + //None + //}); +//}); +//take!(ClockCommand |state: App, iter|Take::take(state.clock(), iter)); +//take!(MidiEditCommand |state: App, iter|Ok(state.editor().map(|x|Take::take(x, iter)).transpose()?.flatten())); +//take!(PoolCommand |state: App, iter|Take::take(&state.pool, iter)); +//take!(SamplerCommand |state: App, iter|Ok(state.project.sampler().map(|x|Take::take(x, iter)).transpose()?.flatten())); +//take!(ArrangementCommand |state: App, iter|Take::take(&state.project, iter)); diff --git a/app/tek_cfg.rs b/app/tek_cfg.rs new file mode 100644 index 00000000..ee323bb6 --- /dev/null +++ b/app/tek_cfg.rs @@ -0,0 +1,65 @@ +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 new file mode 100644 index 00000000..3562ecab --- /dev/null +++ b/app/tek_cli.rs @@ -0,0 +1,132 @@ +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 new file mode 100644 index 00000000..b4af1e3f --- /dev/null +++ b/app/tek_deps.rs @@ -0,0 +1,38 @@ +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 new file mode 100644 index 00000000..f304b517 --- /dev/null +++ b/app/tek_mode.rs @@ -0,0 +1,58 @@ +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 new file mode 100644 index 00000000..71f604be --- /dev/null +++ b/app/tek_test.rs @@ -0,0 +1,103 @@ +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 new file mode 100644 index 00000000..9198ca0c --- /dev/null +++ b/app/tek_view.rs @@ -0,0 +1,353 @@ +use crate::*; + +pub type Views = Arc, Arc>>>; + +impl Draw for App { + fn draw (&self, to: &mut TuiOut) { + for (index, dsl) in self.mode.view.iter().enumerate() { + if let Err(e) = self.view(to, dsl) { + panic!("render #{index} failed ({e}): {dsl}"); + } + } + } +} + +impl View for App { + fn view_expr <'a> (&'a self, to: &mut TuiOut, expr: &'a impl DslExpr) -> Usually<()> { + if evaluate_output_expression(self, to, expr)? + || evaluate_output_expression_tui(self, to, expr)? { + Ok(()) + } else { + Err(format!("App::view_expr: unexpected: {expr:?}").into()) + } + } + fn view_word <'a> (&'a self, to: &mut TuiOut, dsl: &'a impl DslExpr) -> Usually<()> { + let mut frags = dsl.src()?.unwrap().split("/"); + match frags.next() { + Some(":logo") => to.place(&view_logo()), + Some(":status") => to.place(&Fixed::Y(1, "TODO: Status Bar")), + Some(":meters") => match frags.next() { + Some("input") => to.place(&Tui::bg(Rgb(30, 30, 30), Fill::Y(Align::s("Input Meters")))), + Some("output") => to.place(&Tui::bg(Rgb(30, 30, 30), Fill::Y(Align::s("Output Meters")))), + _ => panic!() + }, + Some(":tracks") => match frags.next() { + None => to.place(&"TODO tracks"), + Some("names") => to.place(&self.project.view_track_names(self.color.clone())),//Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Names")))), + Some("inputs") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Inputs")))), + Some("devices") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Devices")))), + Some("outputs") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Outputs")))), + _ => panic!() + }, + Some(":scenes") => match frags.next() { + None => to.place(&"TODO scenes"), + Some(":scenes/names") => to.place(&"TODO Scene Names"), + _ => panic!() + }, + Some(":editor") => to.place(&"TODO Editor"), + Some(":dialog") => match frags.next() { + Some("menu") => to.place(&if let Dialog::Menu(selected, items) = &self.dialog { + let items = items.clone(); + let selected = selected; + Some(Fill::XY(Thunk::new(move|to: &mut TuiOut|{ + for (index, MenuItem(item, _)) in items.0.iter().enumerate() { + to.place(&Push::Y((2 * index) as u16, + Tui::fg_bg( + if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) }, + if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) }, + Fixed::Y(2, Align::n(Fill::X(item))) + ))); + } + }))) + } else { + None + }), + _ => unimplemented!("App::view_word: {dsl:?} ({frags:?})"), + }, + Some(":templates") => to.place(&{ + let modes = self.config.modes.clone(); + let height = (modes.read().unwrap().len() * 2) as u16; + Fixed::Y(height, Min::X(30, Thunk::new(move |to: &mut TuiOut|{ + for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() { + let bg = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) }; + let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or(""); + let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or(""); + let fg1 = Rgb(224, 192, 128); + let fg2 = Rgb(224, 128, 32); + let field_name = Fill::X(Align::w(Tui::fg(fg1, name))); + let field_id = Fill::X(Align::e(Tui::fg(fg2, id))); + let field_info = Fill::X(Align::w(info)); + to.place(&Push::Y((2 * index) as u16, + Fixed::Y(2, Fill::X(Tui::bg(bg, Bsp::s( + Bsp::a(field_name, field_id), field_info)))))); + } + }))) + }), + Some(":sessions") => to.place(&Fixed::Y(6, Min::X(30, Thunk::new(|to: &mut TuiOut|{ + let fg = Rgb(224, 192, 128); + for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { + let bg = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; + to.place(&Push::Y((2 * index) as u16, + &Fixed::Y(2, Fill::X(Tui::bg(bg, Align::w(Tui::fg(fg, name))))))); + } + })))), + Some(":browse/title") => to.place(&Fill::X(Align::w(FieldV(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 new file mode 100644 index 00000000..4af91cd0 --- /dev/null +++ b/bacon.toml @@ -0,0 +1,64 @@ +# 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 deleted file mode 100644 index a2b9d77a..00000000 --- a/bin/cli_arranger.rs +++ /dev/null @@ -1,126 +0,0 @@ -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 deleted file mode 100644 index 472916e3..00000000 --- a/bin/cli_groovebox.rs +++ /dev/null @@ -1,76 +0,0 @@ -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 deleted file mode 100644 index d28bcf52..00000000 --- a/bin/cli_sampler.rs +++ /dev/null @@ -1,48 +0,0 @@ -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 deleted file mode 100644 index 0e644967..00000000 --- a/bin/cli_sequencer.rs +++ /dev/null @@ -1,47 +0,0 @@ -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 deleted file mode 100644 index 2bee935c..00000000 --- a/bin/cli_transport.rs +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index 0819abb8..00000000 --- a/bin/lib.rs +++ /dev/null @@ -1,56 +0,0 @@ -#[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/build/Dockerfile.glibc b/build/Dockerfile.glibc new file mode 100644 index 00000000..ec33045e --- /dev/null +++ b/build/Dockerfile.glibc @@ -0,0 +1,14 @@ +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 new file mode 100644 index 00000000..ed350cdc --- /dev/null +++ b/build/Dockerfile.musl @@ -0,0 +1,13 @@ +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 new file mode 100644 index 00000000..2f70feaa --- /dev/null +++ b/build/README.md @@ -0,0 +1,11 @@ +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 new file mode 100755 index 00000000..3a22285a --- /dev/null +++ b/build/release-glibc-shell.sh @@ -0,0 +1,11 @@ +#!/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 new file mode 100755 index 00000000..b7c01fc1 --- /dev/null +++ b/build/release-glibc.sh @@ -0,0 +1,14 @@ +#!/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 new file mode 100755 index 00000000..8e5c047c --- /dev/null +++ b/build/release-musl-shell.sh @@ -0,0 +1,11 @@ +#!/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 new file mode 100755 index 00000000..b8526fb5 --- /dev/null +++ b/build/release-musl.sh @@ -0,0 +1,14 @@ +#!/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 new file mode 160000 index 00000000..764a38a8 --- /dev/null +++ b/deps/rust-jack @@ -0,0 +1 @@ +Subproject commit 764a38a880ab4749ea60aa7e53cd814b858e606c diff --git a/suil/Cargo.toml b/deps/suil/Cargo.toml similarity index 100% rename from suil/Cargo.toml rename to deps/suil/Cargo.toml diff --git a/suil/build.rs b/deps/suil/build.rs similarity index 100% rename from suil/build.rs rename to deps/suil/build.rs diff --git a/suil/src/bound.rs b/deps/suil/src/bound.rs similarity index 100% rename from suil/src/bound.rs rename to deps/suil/src/bound.rs diff --git a/suil/src/gtk.rs b/deps/suil/src/gtk.rs similarity index 100% rename from suil/src/gtk.rs rename to deps/suil/src/gtk.rs diff --git a/suil/src/lib.rs b/deps/suil/src/lib.rs similarity index 100% rename from suil/src/lib.rs rename to deps/suil/src/lib.rs diff --git a/suil/src/test.rs b/deps/suil/src/test.rs similarity index 100% rename from suil/src/test.rs rename to deps/suil/src/test.rs diff --git a/suil/stdbool.h b/deps/suil/stdbool.h similarity index 100% rename from suil/stdbool.h rename to deps/suil/stdbool.h diff --git a/suil/stdint.h b/deps/suil/stdint.h similarity index 100% rename from suil/stdint.h rename to deps/suil/stdint.h diff --git a/suil/wrapper.h b/deps/suil/wrapper.h similarity index 100% rename from suil/wrapper.h rename to deps/suil/wrapper.h diff --git a/deps/tengri b/deps/tengri new file mode 160000 index 00000000..8c54510f --- /dev/null +++ b/deps/tengri @@ -0,0 +1 @@ +Subproject commit 8c54510f630e8a81b7d7bdca0a51a69cdb9dffcc diff --git a/deps/vst/.github/workflows/deploy.yml b/deps/vst/.github/workflows/deploy.yml new file mode 100644 index 00000000..4636f530 --- /dev/null +++ b/deps/vst/.github/workflows/deploy.yml @@ -0,0 +1,33 @@ +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 new file mode 100644 index 00000000..6cb04feb --- /dev/null +++ b/deps/vst/.github/workflows/docs.yml @@ -0,0 +1,46 @@ +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 new file mode 100644 index 00000000..a453c48b --- /dev/null +++ b/deps/vst/.github/workflows/rust.yml @@ -0,0 +1,38 @@ +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 new file mode 100644 index 00000000..06b76755 --- /dev/null +++ b/deps/vst/.gitignore @@ -0,0 +1,21 @@ +# 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 new file mode 100644 index 00000000..df61cc20 --- /dev/null +++ b/deps/vst/CHANGELOG.md @@ -0,0 +1,86 @@ +# 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 new file mode 100644 index 00000000..ae189138 --- /dev/null +++ b/deps/vst/Cargo.toml @@ -0,0 +1,75 @@ +[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 new file mode 100644 index 00000000..29e06b29 --- /dev/null +++ b/deps/vst/LICENSE @@ -0,0 +1,21 @@ +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 new file mode 100644 index 00000000..a4b28af3 --- /dev/null +++ b/deps/vst/README.md @@ -0,0 +1,112 @@ +# 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 new file mode 100644 index 00000000..0fbe008a --- /dev/null +++ b/deps/vst/examples/dimension_expander.rs @@ -0,0 +1,222 @@ +// 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 new file mode 100644 index 00000000..c5818fbc --- /dev/null +++ b/deps/vst/examples/fwd_midi.rs @@ -0,0 +1,71 @@ +#[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 new file mode 100644 index 00000000..cbe06bd7 --- /dev/null +++ b/deps/vst/examples/gain_effect.rs @@ -0,0 +1,129 @@ +// 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 new file mode 100644 index 00000000..0c6dac81 --- /dev/null +++ b/deps/vst/examples/ladder_filter.rs @@ -0,0 +1,248 @@ +//! 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 new file mode 100644 index 00000000..d8bafbdc --- /dev/null +++ b/deps/vst/examples/simple_host.rs @@ -0,0 +1,63 @@ +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 new file mode 100644 index 00000000..81a0475a --- /dev/null +++ b/deps/vst/examples/sine_synth.rs @@ -0,0 +1,160 @@ +// 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 new file mode 100644 index 00000000..ba50b121 --- /dev/null +++ b/deps/vst/examples/transfer_and_smooth.rs @@ -0,0 +1,136 @@ +// 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 new file mode 100755 index 00000000..b28d7da4 --- /dev/null +++ b/deps/vst/osx_vst_bundler.sh @@ -0,0 +1,61 @@ +#!/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 new file mode 100644 index 00000000..866c7561 --- /dev/null +++ b/deps/vst/rustfmt.toml @@ -0,0 +1 @@ +max_width = 120 \ No newline at end of file diff --git a/deps/vst/src/api.rs b/deps/vst/src/api.rs new file mode 100644 index 00000000..44a1a78a --- /dev/null +++ b/deps/vst/src/api.rs @@ -0,0 +1,927 @@ +//! 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 new file mode 100644 index 00000000..9a32d789 --- /dev/null +++ b/deps/vst/src/buffer.rs @@ -0,0 +1,606 @@ +//! 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 new file mode 100644 index 00000000..f6a1fd2e --- /dev/null +++ b/deps/vst/src/cache.rs @@ -0,0 +1,19 @@ +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 new file mode 100644 index 00000000..e72879fd --- /dev/null +++ b/deps/vst/src/channels.rs @@ -0,0 +1,352 @@ +//! 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 new file mode 100644 index 00000000..923cf873 --- /dev/null +++ b/deps/vst/src/editor.rs @@ -0,0 +1,155 @@ +//! 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 new file mode 100644 index 00000000..580fd54a --- /dev/null +++ b/deps/vst/src/event.rs @@ -0,0 +1,133 @@ +//! 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 new file mode 100644 index 00000000..2ed684f6 --- /dev/null +++ b/deps/vst/src/host.rs @@ -0,0 +1,962 @@ +//! 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 new file mode 100644 index 00000000..6b5261e7 --- /dev/null +++ b/deps/vst/src/interfaces.rs @@ -0,0 +1,370 @@ +//! 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 new file mode 100755 index 00000000..b0c26de3 --- /dev/null +++ b/deps/vst/src/lib.rs @@ -0,0 +1,416 @@ +#![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 new file mode 100644 index 00000000..90a3fb17 --- /dev/null +++ b/deps/vst/src/plugin.rs @@ -0,0 +1,1086 @@ +//! 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 new file mode 100644 index 00000000..dda5705e --- /dev/null +++ b/deps/vst/src/prelude.rs @@ -0,0 +1,12 @@ +//! 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 new file mode 100644 index 00000000..e1cce2df --- /dev/null +++ b/deps/vst/src/util/atomic_float.rs @@ -0,0 +1,59 @@ +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 new file mode 100644 index 00000000..fbe7a87e --- /dev/null +++ b/deps/vst/src/util/mod.rs @@ -0,0 +1,7 @@ +//! 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 new file mode 100644 index 00000000..37ebc92b --- /dev/null +++ b/deps/vst/src/util/parameter_transfer.rs @@ -0,0 +1,187 @@ +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 new file mode 100644 index 00000000..345640b0 --- /dev/null +++ b/device/Cargo.toml @@ -0,0 +1,59 @@ +[package] +name = "tek_device" +edition = { workspace = true } +version = { workspace = true } + +[lib] +path = "device.rs" + +[target.'cfg(target_os = "linux")'] +rustflags = ["-C", "link-arg=-fuse-ld=mold"] + +[dependencies] +tek_engine = { path = "../engine" } + +atomic_float = { workspace = true } +backtrace = { workspace = true } +clap = { workspace = true, optional = true } +jack = { workspace = true } +konst = { workspace = true } +livi = { workspace = true, optional = true } +midly = { workspace = true } +palette = { workspace = true } +rand = { workspace = true } +symphonia = { workspace = true, optional = true } +tengri = { workspace = true } +toml = { workspace = true } +uuid = { workspace = true, optional = true } +wavers = { workspace = true, optional = true } +winit = { workspace = true, optional = true } +xdg = { workspace = true } + +[dev-dependencies] +proptest = { workspace = true } +proptest-derive = { workspace = true } + +[features] +arranger = ["port", "editor", "sequencer", "editor", "track", "scene", "clip", "select"] +browse = [] +clap = [] +cli = ["dep:clap"] +clip = [] +clock = [] +default = ["cli", "arranger", "sampler", "track", "lv2"] +editor = [] +host = ["lv2"] +lv2 = ["port", "livi"] +lv2_gui = ["lv2", "winit"] +meter = [] +mixer = [] +pool = [] +port = [] +sampler = ["port", "meter", "mixer", "browse", "symphonia", "wavers"] +select = [] +scene = [] +sequencer = ["port", "clock", "uuid", "pool"] +sf2 = [] +track = [] +vst2 = [] +vst3 = [] diff --git a/device/arranger.rs b/device/arranger.rs new file mode 100644 index 00000000..10a624b1 --- /dev/null +++ b/device/arranger.rs @@ -0,0 +1,626 @@ +use crate::*; + +#[derive(Default, Debug)] pub struct Arrangement { + /// Project name. + pub name: Arc, + /// Base color. + pub color: ItemTheme, + /// JACK client handle. + pub jack: Jack<'static>, + /// FIXME a render of the project arrangement, redrawn on update. + /// TODO rename to "render_cache" or smth + pub arranger: Arc>, + /// Display size + pub size: Measure, + /// Display size of clips area + pub size_inner: Measure, + /// Source of time + #[cfg(feature = "clock")] pub clock: Clock, + /// Allows one MIDI clip to be edited + #[cfg(feature = "editor")] pub editor: Option, + /// List of global midi inputs + #[cfg(feature = "port")] pub midi_ins: Vec, + /// List of global midi outputs + #[cfg(feature = "port")] pub midi_outs: Vec, + /// List of global audio inputs + #[cfg(feature = "port")] pub audio_ins: Vec, + /// List of global audio outputs + #[cfg(feature = "port")] pub audio_outs: Vec, + /// Selected UI element + #[cfg(feature = "select")] pub selection: Selection, + /// Last track number (to avoid duplicate port names) + #[cfg(feature = "track")] pub track_last: usize, + /// List of tracks + #[cfg(feature = "track")] pub tracks: Vec, + /// Scroll offset of tracks + #[cfg(feature = "track")] pub track_scroll: usize, + /// List of scenes + #[cfg(feature = "scene")] pub scenes: Vec, + /// Scroll offset of scenes + #[cfg(feature = "scene")] pub scene_scroll: usize, +} +impl HasJack<'static> for Arrangement { + fn jack (&self) -> &Jack<'static> { + &self.jack + } +} +has!(Jack<'static>: |self: Arrangement|self.jack); +has!(Measure: |self: Arrangement|self.size); +#[cfg(feature = "editor")] has!(Option: |self: Arrangement|self.editor); +#[cfg(feature = "port")] has!(Vec: |self: Arrangement|self.midi_ins); +#[cfg(feature = "port")] has!(Vec: |self: Arrangement|self.midi_outs); +#[cfg(feature = "clock")] has!(Clock: |self: Arrangement|self.clock); +#[cfg(feature = "select")] has!(Selection: |self: Arrangement|self.selection); +#[cfg(all(feature = "select", feature = "track"))] has!(Vec: |self: Arrangement|self.tracks); +#[cfg(all(feature = "select", feature = "track"))] maybe_has!(Track: |self: Arrangement| + { Has::::get(self).track().map(|index|Has::>::get(self).get(index)).flatten() }; + { Has::::get(self).track().map(|index|Has::>::get_mut(self).get_mut(index)).flatten() }); +#[cfg(all(feature = "select", feature = "scene"))] has!(Vec: |self: Arrangement|self.scenes); +#[cfg(all(feature = "select", feature = "scene"))] maybe_has!(Scene: |self: Arrangement| + { Has::::get(self).track().map(|index|Has::>::get(self).get(index)).flatten() }; + { Has::::get(self).track().map(|index|Has::>::get_mut(self).get_mut(index)).flatten() }); + +#[cfg(feature = "select")] +impl Arrangement { + #[cfg(feature = "clip")] fn selected_clip (&self) -> Option { todo!() } + #[cfg(feature = "scene")] fn selected_scene (&self) -> Option { todo!() } + #[cfg(feature = "track")] fn selected_track (&self) -> Option { todo!() } + #[cfg(feature = "port")] fn selected_midi_in (&self) -> Option { todo!() } + #[cfg(feature = "port")] fn selected_midi_out (&self) -> Option { todo!() } + fn selected_device (&self) -> Option { todo!() } + fn unselect (&self) -> Selection { + Selection::Nothing + } +} + +impl Arrangement { + /// Width of display + pub fn w (&self) -> u16 { + self.size.w() as u16 + } + /// Width allocated for sidebar. + pub fn w_sidebar (&self, is_editing: bool) -> u16 { + self.w() / if is_editing { 16 } else { 8 } as u16 + } + /// Width available to display tracks. + pub fn w_tracks_area (&self, is_editing: bool) -> u16 { + self.w().saturating_sub(self.w_sidebar(is_editing)) + } + /// Height of display + pub fn h (&self) -> u16 { + self.size.h() as u16 + } + /// Height taken by visible device slots. + pub fn h_devices (&self) -> u16 { + 2 + //1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) + } +} + +#[cfg(feature = "track")] +impl TracksView for Arrangement {} + +#[cfg(feature = "track")] +impl Arrangement { + /// Get the active track + pub fn get_track (&self) -> Option<&Track> { + let index = self.selection().track()?; + Has::>::get(self).get(index) + } + /// Get a mutable reference to the active track + pub fn get_track_mut (&mut self) -> Option<&mut Track> { + let index = self.selection().track()?; + Has::>::get_mut(self).get_mut(index) + } + /// Add multiple tracks + pub fn tracks_add ( + &mut self, + count: usize, width: Option, + mins: &[Connect], mouts: &[Connect], + ) -> Usually<()> { + let track_color_1 = ItemColor::random(); + let track_color_2 = ItemColor::random(); + for i in 0..count { + let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into(); + let track = self.track_add(None, Some(color), mins, mouts)?.1; + if let Some(width) = width { + track.width = width; + } + } + Ok(()) + } + /// Add a track + pub fn track_add ( + &mut self, + name: Option<&str>, color: Option, + mins: &[Connect], mouts: &[Connect], + ) -> Usually<(usize, &mut Track)> { + let name: Arc = name.map_or_else( + ||format!("trk{:02}", self.track_last).into(), + |x|x.to_string().into() + ); + self.track_last += 1; + let track = Track { + width: (name.len() + 2).max(12), + color: color.unwrap_or_else(ItemTheme::random), + sequencer: Sequencer::new( + &format!("{name}"), + self.jack(), + Some(self.clock()), + None, + mins, + mouts + )?, + name, + ..Default::default() + }; + self.tracks_mut().push(track); + let len = self.tracks().len(); + let index = len - 1; + for scene in self.scenes_mut().iter_mut() { + while scene.clips.len() < len { + scene.clips.push(None); + } + } + Ok((index, &mut self.tracks_mut()[index])) + } + + pub fn view_inputs (&self, _theme: ItemTheme) -> impl Content + '_ { + Bsp::s( + Fixed::Y(1, self.view_inputs_header()), + Thunk::new(|to: &mut TuiOut|{ + for (index, port) in self.midi_ins().iter().enumerate() { + to.place(&Push::X(index as u16 * 10, Fixed::Y(1, self.view_inputs_row(port)))) + } + }) + ) + } + + fn view_inputs_header (&self) -> impl Content + '_ { + Bsp::e(Fixed::X(20, Align::w(button_3("i", "nput ", format!("{}", self.midi_ins.len()), false))), + Bsp::w(Fixed::X(4, button_2("I", "+", false)), Thunk::new(move|to: &mut TuiOut|for (_index, track, x1, _x2) in self.tracks_with_sizes() { + #[cfg(feature = "track")] + to.place(&Push::X(x1 as u16, Tui::bg(track.color.dark.rgb, Align::w(Fixed::X(track.width as u16, row!( + Either::new(track.sequencer.monitoring, Tui::fg(Green, "mon "), "mon "), + Either::new(track.sequencer.recording, Tui::fg(Red, "rec "), "rec "), + Either::new(track.sequencer.overdub, Tui::fg(Yellow, "dub "), "dub "), + )))))) + }))) + } + + fn view_inputs_row (&self, port: &MidiInput) -> impl Content { + Bsp::e(Fixed::X(20, Align::w(Bsp::e(" ● ", Tui::bold(true, Tui::fg(Rgb(255,255,255), port.port_name()))))), + Bsp::w(Fixed::X(4, ()), Thunk::new(move|to: &mut TuiOut|for (_index, track, _x1, _x2) in self.tracks_with_sizes() { + #[cfg(feature = "track")] + to.place(&Tui::bg(track.color.darker.rgb, Align::w(Fixed::X(track.width as u16, row!( + Either::new(track.sequencer.monitoring, Tui::fg(Green, " ● "), " · "), + Either::new(track.sequencer.recording, Tui::fg(Red, " ● "), " · "), + Either::new(track.sequencer.overdub, Tui::fg(Yellow, " ● "), " · "), + ))))) + }))) + } + + pub fn view_outputs (&self, theme: ItemTheme) -> impl Content { + let mut h = 1; + for output in self.midi_outs().iter() { + h += 1 + output.connections.len(); + } + let h = h as u16; + let list = Bsp::s( + Fixed::Y(1, Fill::X(Align::w(button_3("o", "utput", format!("{}", self.midi_outs.len()), false)))), + Fixed::Y(h - 1, Fill::XY(Align::nw(Thunk::new(|to: &mut TuiOut|{ + for (_index, port) in self.midi_outs().iter().enumerate() { + to.place(&Fixed::Y(1,Fill::X(Bsp::e( + Align::w(Bsp::e(" ● ", Tui::fg(Rgb(255,255,255),Tui::bold(true, port.port_name())))), + Fill::X(Align::e(format!("{}/{} ", + port.port().get_connections().len(), + port.connections.len()))))))); + for (index, conn) in port.connections.iter().enumerate() { + to.place(&Fixed::Y(1, Fill::X(Align::w(format!(" c{index:02}{}", conn.info()))))); + } + } + }))))); + Fixed::Y(h, view_track_row_section(theme, list, button_2("O", "+", false), + Tui::bg(theme.darker.rgb, Align::w(Fill::X( + Thunk::new(|to: &mut TuiOut|{ + for (index, track, _x1, _x2) in self.tracks_with_sizes() { + to.place(&Fixed::X(track_width(index, track), + Thunk::new(|to: &mut TuiOut|{ + to.place(&Fixed::Y(1, Align::w(Bsp::e( + Either::new(true, Tui::fg(Green, "play "), "play "), + Either::new(false, Tui::fg(Yellow, "solo "), "solo "), + )))); + for (_index, port) in self.midi_outs().iter().enumerate() { + to.place(&Fixed::Y(1, Align::w(Bsp::e( + Either::new(true, Tui::fg(Green, " ● "), " · "), + Either::new(false, Tui::fg(Yellow, " ● "), " · "), + )))); + for (_index, _conn) in port.connections.iter().enumerate() { + to.place(&Fixed::Y(1, Fill::X(""))); + } + }})))}})))))) + } + + pub fn view_track_devices (&self, theme: ItemTheme) -> impl Content { + let mut h = 2u16; + for track in self.tracks().iter() { + h = h.max(track.devices.len() as u16 * 2); + } + view_track_row_section(theme, + button_3("d", "evice", format!("{}", self.track().map(|t|t.devices.len()).unwrap_or(0)), false), + button_2("D", "+", false), + Thunk::new(move|to: &mut TuiOut|for (index, track, _x1, _x2) in self.tracks_with_sizes() { + to.place(&Fixed::XY(track_width(index, track), h + 1, + Tui::bg(track.color.dark.rgb, Align::nw(Map::south(2, move||0..h, + |_, _index|Fixed::XY(track.width as u16, 2, + Tui::fg_bg( + ItemTheme::G[32].lightest.rgb, + ItemTheme::G[32].dark.rgb, + Align::nw(format!(" · {}", "--"))))))))); + })) + } + +} + +#[cfg(feature = "track")] +pub fn view_track_row_section ( + _theme: ItemTheme, + button: impl Content, + button_add: impl Content, + content: impl Content, +) -> impl Content { + Bsp::w(Fill::Y(Fixed::X(4, Align::nw(button_add))), + Bsp::e(Fixed::X(20, Fill::Y(Align::nw(button))), Fill::XY(Align::c(content)))) +} + +#[cfg(feature = "scene")] +impl Arrangement { + /// Get the active scene + pub fn get_scene (&self) -> Option<&Scene> { + let index = self.selection().scene()?; + Has::>::get(self).get(index) + } + /// Get a mutable reference to the active scene + pub fn get_scene_mut (&mut self) -> Option<&mut Scene> { + let index = self.selection().scene()?; + Has::>::get_mut(self).get_mut(index) + } +} + +#[cfg(feature = "scene")] +impl ScenesView for Arrangement { + fn h_scenes (&self) -> u16 { + (self.height() as u16).saturating_sub(20) + } + fn w_side (&self) -> u16 { + (self.width() as u16 * 2 / 10).max(20) + } + fn w_mid (&self) -> u16 { + (self.width() as u16).saturating_sub(2 * self.w_side()).max(40) + } +} + +#[cfg(feature = "clip")] +impl Arrangement { + /// Get the active clip + pub fn get_clip (&self) -> Option>> { + self.get_scene()?.clips.get(self.selection().track()?)?.clone() + } + /// Put a clip in a slot + pub fn clip_put ( + &mut self, track: usize, scene: usize, clip: Option>> + ) -> Option>> { + let old = self.scenes[scene].clips[track].clone(); + self.scenes[scene].clips[track] = clip; + old + } + /// Change the color of a clip, returning the previous one + pub fn clip_set_color (&self, track: usize, scene: usize, color: ItemTheme) + -> Option + { + self.scenes[scene].clips[track].as_ref().map(|clip|{ + let mut clip = clip.write().unwrap(); + let old = clip.color.clone(); + clip.color = color.clone(); + panic!("{color:?} {old:?}"); + old + }) + } + /// Toggle looping for the active clip + pub fn toggle_loop (&mut self) { + if let Some(clip) = self.get_clip() { + clip.write().unwrap().toggle_loop() + } + } +} + +#[cfg(feature = "sampler")] +impl Arrangement { + /// Get the first sampler of the active track + pub fn sampler (&self) -> Option<&Sampler> { + self.get_track()?.sampler(0) + } + /// Get the first sampler of the active track + pub fn sampler_mut (&mut self) -> Option<&mut Sampler> { + self.get_track_mut()?.sampler_mut(0) + } +} + +pub(crate) fn wrap (bg: Color, fg: Color, content: impl Content) -> impl Content { + let left = Tui::fg_bg(bg, Reset, Fixed::X(1, RepeatV("▐"))); + let right = Tui::fg_bg(bg, Reset, Fixed::X(1, RepeatV("▌"))); + Bsp::e(left, Bsp::w(right, Tui::fg_bg(fg, bg, content))) +} + +pub trait HasClipsSize { + fn clips_size (&self) -> &Measure; +} + +impl HasClipsSize for Arrangement { + fn clips_size (&self) -> &Measure { &self.size_inner } +} + +pub trait HasWidth { + const MIN_WIDTH: usize; + /// Increment track width. + fn width_inc (&mut self); + /// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH]. + fn width_dec (&mut self); +} + +//def_command!(ArrangementCommand: |arranger: Arrangement| { + + //Home => { arranger.editor = None; Ok(None) }, + + //Edit => { + //let selection = arranger.selection().clone(); + //arranger.editor = if arranger.editor.is_some() { + //None + //} else { + //match selection { + //Selection::TrackClip { track, scene } => { + //let clip = &mut arranger.scenes_mut()[scene].clips[track]; + //if clip.is_none() { + ////app.clip_auto_create(); + //*clip = Some(Arc::new(RwLock::new(MidiClip::new( + //&format!("t{track:02}s{scene:02}"), + //false, 384, None, Some(ItemTheme::random()) + //)))); + //} + //clip.as_ref().map(|c|c.into()) + //} + //_ => { + //None + //} + //} + //}; + //if let Some(editor) = arranger.editor.as_mut() { + //if let Some(clip) = editor.clip() { + //let length = clip.read().unwrap().length.max(1); + //let width = arranger.size_inner.w().saturating_sub(20).max(1); + //editor.set_time_zoom(length / width); + //editor.redraw(); + //} + //} + //Ok(None) + //}, + + ////// Set the selection + //Select { selection: Selection } => { *arranger.selection_mut() = *selection; Ok(None) }, + + ////// Launch the selected clip or scene + //Launch => { + //match *arranger.selection() { + //Selection::Track(t) => { + //arranger.tracks[t].sequencer.enqueue_next(None) + //}, + //Selection::TrackClip { track, scene } => { + //arranger.tracks[track].sequencer.enqueue_next(arranger.scenes[scene].clips[track].as_ref()) + //}, + //Selection::Scene(s) => { + //for t in 0..arranger.tracks.len() { + //arranger.tracks[t].sequencer.enqueue_next(arranger.scenes[s].clips[t].as_ref()) + //} + //}, + //_ => {} + //}; + //Ok(None) + //}, + + ////// Set the color of the selected entity + //SetColor { palette: Option } => { + //let mut palette = palette.unwrap_or_else(||ItemTheme::random()); + //let selection = *arranger.selection(); + //Ok(Some(Self::SetColor { palette: Some(match selection { + //Selection::Mix => { + //std::mem::swap(&mut palette, &mut arranger.color); + //palette + //}, + //Selection::Scene(s) => { + //std::mem::swap(&mut palette, &mut arranger.scenes[s].color); + //palette + //} + //Selection::Track(t) => { + //std::mem::swap(&mut palette, &mut arranger.tracks[t].color); + //palette + //} + //Selection::TrackClip { track, scene } => { + //if let Some(ref clip) = arranger.scenes[scene].clips[track] { + //let mut clip = clip.write().unwrap(); + //std::mem::swap(&mut palette, &mut clip.color); + //palette + //} else { + //return Ok(None) + //} + //}, + //_ => todo!() + //}) })) + //}, + + //Track { track: TrackCommand } => { todo!("delegate") }, + + //TrackAdd => { + //let index = arranger.track_add(None, None, &[], &[])?.0; + //*arranger.selection_mut() = match arranger.selection() { + //Selection::Track(_) => Selection::Track(index), + //Selection::TrackClip { track: _, scene } => Selection::TrackClip { + //track: index, scene: *scene + //}, + //_ => *arranger.selection() + //}; + //Ok(Some(Self::TrackDelete { index })) + //}, + + //TrackSwap { index: usize, other: usize } => { + //let index = *index; + //let other = *other; + //Ok(Some(Self::TrackSwap { index, other })) + //}, + + //TrackDelete { index: usize } => { + //let index = *index; + //let exists = arranger.tracks().get(index).is_some(); + //if exists { + //let track = arranger.tracks_mut().remove(index); + //let Track { sequencer: Sequencer { midi_ins, midi_outs, .. }, .. } = track; + //for port in midi_ins.into_iter() { + //port.close()?; + //} + //for port in midi_outs.into_iter() { + //port.close()?; + //} + //for scene in arranger.scenes_mut().iter_mut() { + //scene.clips.remove(index); + //} + //} + //Ok(None) + ////TODO:Ok(Some(Self::TrackAdd ( index, track: Some(deleted_track) }) + //}, + + //MidiIn { input: MidiInputCommand } => { + //todo!("delegate"); Ok(None) + //}, + + //MidiInAdd => { + //arranger.midi_in_add()?; + //Ok(None) + //}, + + //MidiOut { output: MidiOutputCommand } => { + //todo!("delegate"); + //Ok(None) + //}, + + //MidiOutAdd => { + //arranger.midi_out_add()?; + //Ok(None) + //}, + + //Device { command: DeviceCommand } => { + //todo!("delegate"); + //Ok(None) + //}, + + //DeviceAdd { index: usize } => { + //todo!("delegate"); + //Ok(None) + //}, + + //Scene { scene: SceneCommand } => { + //todo!("delegate"); + //Ok(None) + //}, + + //OutputAdd => { + //arranger.midi_outs.push(MidiOutput::new( + //arranger.jack(), + //&format!("/M{}", arranger.midi_outs.len() + 1), + //&[] + //)?); + //Ok(None) + //}, + + //InputAdd => { + //arranger.midi_ins.push(MidiInput::new( + //arranger.jack(), + //&format!("M{}/", arranger.midi_ins.len() + 1), + //&[] + //)?); + //Ok(None) + //}, + + //SceneAdd => { + //let index = arranger.scene_add(None, None)?.0; + //*arranger.selection_mut() = match arranger.selection() { + //Selection::Scene(_) => Selection::Scene(index), + //Selection::TrackClip { track, scene } => Selection::TrackClip { + //track: *track, + //scene: index + //}, + //_ => *arranger.selection() + //}; + //Ok(None) // TODO + //}, + + //SceneSwap { index: usize, other: usize } => { + //let index = *index; + //let other = *other; + //Ok(Some(Self::SceneSwap { index, other })) + //}, + + //SceneDelete { index: usize } => { + //let index = *index; + //let scenes = arranger.scenes_mut(); + //Ok(if scenes.get(index).is_some() { + //let _scene = scenes.remove(index); + //None + //} else { + //None + //}) + //}, + + //SceneLaunch { index: usize } => { + //let index = *index; + //for track in 0..arranger.tracks.len() { + //let clip = arranger.scenes[index].clips[track].as_ref(); + //arranger.tracks[track].sequencer.enqueue_next(clip); + //} + //Ok(None) + //}, + + //Clip { scene: ClipCommand } => { + //todo!("delegate") + //}, + + //ClipGet { a: usize, b: usize } => { + ////(Get [a: usize, b: usize] cmd_todo!("\n\rtodo: clip: get: {a} {b}")) + ////("get" [a: usize, b: usize] Some(Self::Get(a.unwrap(), b.unwrap()))) + //todo!() + //}, + + //ClipPut { a: usize, b: usize } => { + ////(Put [t: usize, s: usize, c: MaybeClip] + ////Some(Self::Put(t, s, arranger.clip_put(t, s, c)))) + ////("put" [a: usize, b: usize, c: MaybeClip] Some(Self::Put(a.unwrap(), b.unwrap(), c.unwrap()))) + //todo!() + //}, + + //ClipDel { a: usize, b: usize } => { + ////("delete" [a: usize, b: usize] Some(Self::Put(a.unwrap(), b.unwrap(), None)))) + //todo!() + //}, + + //ClipEnqueue { a: usize, b: usize } => { + ////(Enqueue [t: usize, s: usize] + ////cmd!(arranger.tracks[t].sequencer.enqueue_next(arranger.scenes[s].clips[t].as_ref()))) + ////("enqueue" [a: usize, b: usize] Some(Self::Enqueue(a.unwrap(), b.unwrap()))) + //todo!() + //}, + + //ClipSwap { a: usize, b: usize }=> { + ////(Edit [clip: MaybeClip] cmd_todo!("\n\rtodo: clip: edit: {clip:?}")) + ////("edit" [a: MaybeClip] Some(Self::Edit(a.unwrap()))) + //todo!() + //}, + +//}); diff --git a/device/browse.rs b/device/browse.rs new file mode 100644 index 00000000..02d41448 --- /dev/null +++ b/device/browse.rs @@ -0,0 +1,220 @@ +use crate::*; +use std::path::PathBuf; +use std::ffi::OsString; + +#[derive(Clone, Debug)] +pub enum BrowseTarget { + SaveProject, + LoadProject, + ImportSample(Arc>>), + ExportSample(Arc>>), + ImportClip(Arc>>), + ExportClip(Arc>>), +} + +impl PartialEq for BrowseTarget { + fn eq (&self, other: &Self) -> bool { + match self { + Self::ImportSample(_) => false, + Self::ExportSample(_) => false, + Self::ImportClip(_) => false, + Self::ExportClip(_) => false, + t => matches!(other, t) + } + } +} + +/// Browses for phrase to import/export +#[derive(Debug, Clone, Default, PartialEq)] +pub struct Browse { + pub cwd: PathBuf, + pub dirs: Vec<(OsString, String)>, + pub files: Vec<(OsString, String)>, + pub filter: String, + pub index: usize, + pub scroll: usize, + pub size: Measure, +} + +impl Browse { + + pub fn new (cwd: Option) -> Usually { + let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? }; + let mut dirs = vec![]; + let mut files = vec![]; + for entry in std::fs::read_dir(&cwd)? { + let entry = entry?; + let name = entry.file_name(); + let decoded = name.clone().into_string().unwrap_or_else(|_|"".to_string()); + let meta = entry.metadata()?; + if meta.is_dir() { + dirs.push((name, format!("📁 {decoded}"))); + } else if meta.is_file() { + files.push((name, format!("📄 {decoded}"))); + } + } + Ok(Self { + cwd, + dirs, + files, + filter: "".to_string(), + index: 0, + scroll: 0, + size: Measure::new(), + }) + } + + pub fn len (&self) -> usize { + self.dirs.len() + self.files.len() + } + + pub fn is_dir (&self) -> bool { + self.index < self.dirs.len() + } + + pub fn is_file (&self) -> bool { + self.index >= self.dirs.len() + } + + pub fn path (&self) -> PathBuf { + self.cwd.join(if self.is_dir() { + &self.dirs[self.index].0 + } else if self.is_file() { + &self.files[self.index - self.dirs.len()].0 + } else { + unreachable!() + }) + } + + pub fn chdir (&self) -> Usually { + Self::new(Some(self.path())) + } + +} + +impl Browse { + fn _todo_stub_path_buf (&self) -> PathBuf { + todo!() + } + fn _todo_stub_usize (&self) -> usize { + todo!() + } + fn _todo_stub_arc_str (&self) -> Arc { + todo!() + } +} + +def_command!(BrowseCommand: |browse: Browse| { + SetVisible => Ok(None), + SetPath { address: PathBuf } => Ok(None), + SetSearch { filter: Arc } => Ok(None), + SetCursor { cursor: usize } => Ok(None), +}); + +impl HasContent for Browse { + fn content (&self) -> impl Content { + Map::south(1, ||EntriesIterator { + offset: 0, + index: 0, + length: self.dirs.len() + self.files.len(), + browser: self, + }, |entry, _index|Fill::X(Align::w(entry))) + } +} + +struct EntriesIterator<'a> { + browser: &'a Browse, + offset: usize, + length: usize, + index: usize, +} + +impl<'a> Iterator for EntriesIterator<'a> { + type Item = Modify<&'a str>; + fn next (&mut self) -> Option { + let dirs = self.browser.dirs.len(); + let files = self.browser.files.len(); + let index = self.index; + if self.index < dirs { + self.index += 1; + Some(Tui::bold(true, self.browser.dirs[index].1.as_str())) + } else if self.index < dirs + files { + self.index += 1; + Some(Tui::bold(false, self.browser.files[index - dirs].1.as_str())) + } else { + None + } + } +} + +// Commands supported by [Browse] +//#[derive(Debug, Clone, PartialEq)] +//pub enum BrowseCommand { + //Begin, + //Cancel, + //Confirm, + //Select(usize), + //Chdir(PathBuf), + //Filter(Arc), +//} + //fn begin (browse: &mut Browse) => { + //unreachable!(); + //} + //fn cancel (browse: &mut Browse) => { + //todo!() + ////browse.mode = None; + ////Ok(None) + //} + //fn confirm (browse: &mut Browse) => { + //todo!() + ////Ok(match browse.mode { + ////Some(PoolMode::Import(index, ref mut browse)) => { + ////if browse.is_file() { + ////let path = browse.path(); + ////browse.mode = None; + ////let _undo = PoolClipCommand::import(browse, index, path)?; + ////None + ////} else if browse.is_dir() { + ////browse.mode = Some(PoolMode::Import(index, browse.chdir()?)); + ////None + ////} else { + ////None + ////} + ////}, + ////Some(PoolMode::Export(index, ref mut browse)) => { + ////todo!() + ////}, + ////_ => unreachable!(), + ////}) + //} + //fn select (browse: &mut Browse, index: usize) => { + //todo!() + ////Ok(match browse.mode { + ////Some(PoolMode::Import(index, ref mut browse)) => { + ////browse.index = index; + ////None + ////}, + ////Some(PoolMode::Export(index, ref mut browse)) => { + ////browse.index = index; + ////None + ////}, + ////_ => unreachable!(), + ////}) + //} + //fn chdir (browse: &mut Browse, dir: PathBuf) => { + //todo!() + ////Ok(match browse.mode { + ////Some(PoolMode::Import(index, ref mut browse)) => { + ////browse.mode = Some(PoolMode::Import(index, Browse::new(Some(dir))?)); + ////None + ////}, + ////Some(PoolMode::Export(index, ref mut browse)) => { + ////browse.mode = Some(PoolMode::Export(index, Browse::new(Some(dir))?)); + ////None + ////}, + ////_ => unreachable!(), + ////}) + //} + //fn filter (browse: &mut Browse, filter: Arc) => { + //todo!() + //} diff --git a/src/core.rs b/device/clap.rs similarity index 100% rename from src/core.rs rename to device/clap.rs diff --git a/device/clip.rs b/device/clip.rs new file mode 100644 index 00000000..ac0ca95b --- /dev/null +++ b/device/clip.rs @@ -0,0 +1,215 @@ +use crate::*; + +pub trait HasMidiClip { + fn clip (&self) -> Option>>; +} + +#[macro_export] macro_rules! has_clip { + (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? HasMidiClip for $Struct $(<$($L),*$($T),*>)? { + fn clip (&$self) -> Option>> { $cb } + } + } +} + +/// A MIDI sequence. +#[derive(Debug, Clone, Default)] +pub struct MidiClip { + pub uuid: uuid::Uuid, + /// Name of clip + pub name: Arc, + /// Temporal resolution in pulses per quarter note + pub ppq: usize, + /// Length of clip in pulses + pub length: usize, + /// Notes in clip + pub notes: MidiData, + /// Whether to loop the clip or play it once + pub looped: bool, + /// Start of loop + pub loop_start: usize, + /// Length of loop + pub loop_length: usize, + /// All notes are displayed with minimum length + pub percussive: bool, + /// Identifying color of clip + pub color: ItemTheme, +} + +/// MIDI message structural +pub type MidiData = Vec>; + +impl MidiClip { + pub fn new ( + name: impl AsRef, + looped: bool, + length: usize, + notes: Option, + color: Option, + ) -> Self { + Self { + uuid: uuid::Uuid::new_v4(), + name: name.as_ref().into(), + ppq: PPQ, + length, + notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]), + looped, + loop_start: 0, + loop_length: length, + percussive: true, + color: color.unwrap_or_else(ItemTheme::random) + } + } + pub fn count_midi_messages (&self) -> usize { + let mut count = 0; + for tick in self.notes.iter() { + count += tick.len(); + } + count + } + pub fn set_length (&mut self, length: usize) { + self.length = length; + self.notes = vec![Vec::with_capacity(16);length]; + } + pub fn duplicate (&self) -> Self { + let mut clone = self.clone(); + clone.uuid = uuid::Uuid::new_v4(); + clone + } + pub fn toggle_loop (&mut self) { self.looped = !self.looped; } + pub fn record_event (&mut self, pulse: usize, message: MidiMessage) { + if pulse >= self.length { panic!("extend clip first") } + self.notes[pulse].push(message); + } + /// Check if a range `start..end` contains MIDI Note On `k` + pub fn contains_note_on (&self, k: u7, start: usize, end: usize) -> bool { + for events in self.notes[start.max(0)..end.min(self.notes.len())].iter() { + for event in events.iter() { + if let MidiMessage::NoteOn {key,..} = event { if *key == k { return true } } + } + } + false + } + pub fn stop_all () -> Self { + Self::new( + "Stop", + false, + 1, + Some(vec![vec![MidiMessage::Controller { + controller: 123.into(), + value: 0.into() + }]]), + Some(ItemColor::from_rgb(Color::Rgb(32, 32, 32)).into()) + ) + } +} + +impl PartialEq for MidiClip { + fn eq (&self, other: &Self) -> bool { + self.uuid == other.uuid + } +} + +impl Eq for MidiClip {} + +impl MidiClip { + fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } + fn _todo_bool_stub_ (&self) -> bool { todo!() } + fn _todo_usize_stub_ (&self) -> usize { todo!() } + fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } + fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } + fn _todo_opt_item_theme_stub (&self) -> Option { todo!() } +} + +def_command!(ClipCommand: |clip: MidiClip| { + + SetColor { color: Option } => { + //(SetColor [t: usize, s: usize, c: ItemTheme] + //clip.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o))))); + //("color" [a: usize, b: usize] Some(Self::SetColor(a.unwrap(), b.unwrap(), ItemTheme::random()))) + todo!() + }, + + SetLoop { looping: Option } => { + //(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}")) + //("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap()))) + todo!() + } + +}); + +pub trait ClipsView: + TracksView + + ScenesView + + HasClipsSize + + Send + + Sync +{ + fn view_scenes_clips <'a> (&'a self) + -> impl Content + 'a + { + self.clips_size().of(Fill::XY(Bsp::a( + Fill::XY(Align::se(Tui::fg(Green, format!("{}x{}", self.clips_size().w(), self.clips_size().h())))), + Thunk::new(|to: &mut TuiOut|for ( + track_index, track, _, _ + ) in self.tracks_with_sizes() { + to.place(&Fixed::X(track.width as u16, + Fill::Y(self.view_track_clips(track_index, track)))) + })))) + } + + fn view_track_clips <'a> (&'a self, track_index: usize, track: &'a Track) -> impl Content + 'a { + Thunk::new(move|to: &mut TuiOut|for ( + scene_index, scene, .. + ) in self.scenes_with_sizes() { + let (name, theme): (Arc, ItemTheme) = if let Some(Some(clip)) = &scene.clips.get(track_index) { + let clip = clip.read().unwrap(); + (format!(" ⏹ {}", &clip.name).into(), clip.color) + } else { + (" ⏹ -- ".into(), ItemTheme::G[32]) + }; + let fg = theme.lightest.rgb; + let mut outline = theme.base.rgb; + let bg = if self.selection().track() == Some(track_index) + && self.selection().scene() == Some(scene_index) + { + outline = theme.lighter.rgb; + theme.light.rgb + } else if self.selection().track() == Some(track_index) + || self.selection().scene() == Some(scene_index) + { + outline = theme.darkest.rgb; + theme.base.rgb + } else { + theme.dark.rgb + }; + let w = if self.selection().track() == Some(track_index) + && let Some(editor) = self.editor () + { + editor.width().max(24).max(track.width) + } else { + track.width + } as u16; + let y = if self.selection().scene() == Some(scene_index) + && let Some(editor) = self.editor () + { + editor.height().max(12) + } else { + Self::H_SCENE + } as u16; + + to.place(&Fixed::XY(w, y, Bsp::b( + Fill::XY(Outer(true, Style::default().fg(outline))), + Fill::XY(Bsp::b( + Bsp::b( + Tui::fg_bg(outline, bg, Fill::XY("")), + Fill::XY(Align::nw(Tui::fg_bg(fg, bg, Tui::bold(true, name)))), + ), + Fill::XY(When::new(self.selection().track() == Some(track_index) + && self.selection().scene() == Some(scene_index) + && self.is_editing(), self.editor()))))))); + }) + } +} +//take!(ClipCommand |state: Arrangement, iter|state.selected_clip().as_ref() + //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); diff --git a/device/clock.rs b/device/clock.rs new file mode 100644 index 00000000..17ab9d36 --- /dev/null +++ b/device/clock.rs @@ -0,0 +1,421 @@ +use crate::*; +use std::fmt::Write; + +pub trait HasClock: Send + Sync { + fn clock (&self) -> &Clock; + fn clock_mut (&mut self) -> &mut Clock; +} + +impl> HasClock for T { + fn clock (&self) -> &Clock { self.get() } + fn clock_mut (&mut self) -> &mut Clock { self.get_mut() } +} + +#[derive(Clone, Default)] +pub struct Clock { + /// JACK transport handle. + pub transport: Arc>, + /// Global temporal resolution (shared by [Moment] fields) + pub timebase: Arc, + /// Current global sample and usec (monotonic from JACK clock) + pub global: Arc, + /// Global sample and usec at which playback started + pub started: Arc>>, + /// Playback offset (when playing not from start) + pub offset: Arc, + /// Current playhead position + pub playhead: Arc, + /// Note quantization factor + pub quant: Arc, + /// Launch quantization factor + pub sync: Arc, + /// Size of buffer in samples + pub chunk: Arc, + // Cache of formatted strings + pub view_cache: Arc>, + /// For syncing the clock to an external source + #[cfg(feature = "port")] pub midi_in: Arc>>, + /// For syncing other devices to this clock + #[cfg(feature = "port")] pub midi_out: Arc>>, + /// For emitting a metronome + #[cfg(feature = "port")] pub click_out: Arc>>, +} + +impl std::fmt::Debug for Clock { + fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("Clock") + .field("timebase", &self.timebase) + .field("chunk", &self.chunk) + .field("quant", &self.quant) + .field("sync", &self.sync) + .field("global", &self.global) + .field("playhead", &self.playhead) + .field("started", &self.started) + .finish() + } +} + +impl Clock { + pub fn new (jack: &Jack<'static>, bpm: Option) -> Usually { + let (chunk, transport) = jack.with_client(|c|(c.buffer_size(), c.transport())); + let timebase = Arc::new(Timebase::default()); + let clock = Self { + quant: Arc::new(24.into()), + sync: Arc::new(384.into()), + transport: Arc::new(Some(transport)), + chunk: Arc::new((chunk as usize).into()), + global: Arc::new(Moment::zero(&timebase)), + playhead: Arc::new(Moment::zero(&timebase)), + offset: Arc::new(Moment::zero(&timebase)), + started: RwLock::new(None).into(), + timebase, + midi_in: Arc::new(RwLock::new(Some(MidiInput::new(jack, &"M/clock", &[])?))), + midi_out: Arc::new(RwLock::new(Some(MidiOutput::new(jack, &"clock/M", &[])?))), + click_out: Arc::new(RwLock::new(Some(AudioOutput::new(jack, &"click", &[])?))), + ..Default::default() + }; + if let Some(bpm) = bpm { + clock.timebase.bpm.set(bpm); + } + Ok(clock) + } + pub fn timebase (&self) -> &Arc { + &self.timebase + } + /// Current sample rate + pub fn sr (&self) -> &SampleRate { + &self.timebase.sr + } + /// Current tempo + pub fn bpm (&self) -> &BeatsPerMinute { + &self.timebase.bpm + } + /// Current MIDI resolution + pub fn ppq (&self) -> &PulsesPerQuaver { + &self.timebase.ppq + } + /// Next pulse that matches launch sync (for phrase switchover) + pub fn next_launch_pulse (&self) -> usize { + let sync = self.sync.get() as usize; + let pulse = self.playhead.pulse.get() as usize; + if pulse % sync == 0 { + pulse + } else { + (pulse / sync + 1) * sync + } + } + /// Start playing, optionally seeking to a given location beforehand + pub fn play_from (&self, start: Option) -> Usually<()> { + if let Some(transport) = self.transport.as_ref() { + if let Some(start) = start { + transport.locate(start)?; + } + transport.start()?; + } + Ok(()) + } + /// Pause, optionally seeking to a given location afterwards + pub fn pause_at (&self, pause: Option) -> Usually<()> { + if let Some(transport) = self.transport.as_ref() { + transport.stop()?; + if let Some(pause) = pause { + transport.locate(pause)?; + } + } + Ok(()) + } + /// Is currently paused? + pub fn is_stopped (&self) -> bool { + self.started.read().unwrap().is_none() + } + /// Is currently playing? + pub fn is_rolling (&self) -> bool { + self.started.read().unwrap().is_some() + } + /// Update chunk size + pub fn set_chunk (&self, n_frames: usize) { + self.chunk.store(n_frames, Relaxed); + } + pub fn update_from_scope (&self, scope: &ProcessScope) -> Usually<()> { + // Store buffer length + self.set_chunk(scope.n_frames() as usize); + + // Store reported global frame and usec + let CycleTimes { current_frames, current_usecs, .. } = scope.cycle_times()?; + self.global.sample.set(current_frames as f64); + self.global.usec.set(current_usecs as f64); + + let mut started = self.started.write().unwrap(); + + // If transport has just started or just stopped, + // update starting point: + if let Some(transport) = self.transport.as_ref() { + match (transport.query_state()?, started.as_ref()) { + (TransportState::Rolling, None) => { + let moment = Moment::zero(&self.timebase); + moment.sample.set(current_frames as f64); + moment.usec.set(current_usecs as f64); + *started = Some(moment); + }, + (TransportState::Stopped, Some(_)) => { + *started = None; + }, + _ => {} + }; + } + + self.playhead.update_from_sample(started.as_ref() + .map(|started|current_frames as f64 - started.sample.get()) + .unwrap_or(0.)); + + Ok(()) + } + + pub fn bbt (&self) -> PositionBBT { + let pulse = self.playhead.pulse.get() as i32; + let ppq = self.timebase.ppq.get() as i32; + let bpm = self.timebase.bpm.get(); + let bar = (pulse / ppq) / 4; + PositionBBT { + bar: 1 + bar, + beat: 1 + (pulse / ppq) % 4, + tick: (pulse % ppq), + bar_start_tick: (bar * 4 * ppq) as f64, + beat_type: 4., + beats_per_bar: 4., + beats_per_minute: bpm, + ticks_per_beat: ppq as f64 + } + } + + pub fn next_launch_instant (&self) -> Moment { + Moment::from_pulse(self.timebase(), self.next_launch_pulse() as f64) + } + + /// Get index of first sample to populate. + /// + /// Greater than 0 means that the first pulse of the clip + /// falls somewhere in the middle of the chunk. + pub fn get_sample_offset (&self, scope: &ProcessScope, started: &Moment) -> usize{ + (scope.last_frame_time() as usize).saturating_sub( + started.sample.get() as usize + + self.started.read().unwrap().as_ref().unwrap().sample.get() as usize + ) + } + + // Get iterator that emits sample paired with pulse. + // + // * Sample: index into output buffer at which to write MIDI event + // * Pulse: index into clip from which to take the MIDI event + // + // Emitted for each sample of the output buffer that corresponds to a MIDI pulse. + pub fn get_pulses (&self, scope: &ProcessScope, offset: usize) -> TicksIterator { + self.timebase().pulses_between_samples(offset, offset + scope.n_frames() as usize) + } +} + +impl Clock { + fn _todo_provide_u32 (&self) -> u32 { + todo!() + } + fn _todo_provide_opt_u32 (&self) -> Option { + todo!() + } + fn _todo_provide_f64 (&self) -> f64 { + todo!() + } +} + +impl Command for ClockCommand { + fn execute (&self, state: &mut T) -> Perhaps { + self.execute(state.clock_mut()) // awesome + } +} + +def_command!(ClockCommand: |clock: Clock| { + SeekUsec { usec: f64 } => { + clock.playhead.update_from_usec(*usec); Ok(None) }, + SeekSample { sample: f64 } => { + clock.playhead.update_from_sample(*sample); Ok(None) }, + SeekPulse { pulse: f64 } => { + clock.playhead.update_from_pulse(*pulse); Ok(None) }, + SetBpm { bpm: f64 } => Ok(Some( + Self::SetBpm { bpm: clock.timebase().bpm.set(*bpm) })), + SetQuant { quant: f64 } => Ok(Some( + Self::SetQuant { quant: clock.quant.set(*quant) })), + SetSync { sync: f64 } => Ok(Some( + Self::SetSync { sync: clock.sync.set(*sync) })), + + Play { position: Option } => { + clock.play_from(*position)?; Ok(None) /* TODO Some(Pause(previousPosition)) */ }, + Pause { position: Option } => { + clock.pause_at(*position)?; Ok(None) }, + + TogglePlayback { position: u32 } => Ok(if clock.is_rolling() { + clock.pause_at(Some(*position))?; None + } else { + clock.play_from(Some(*position))?; None + }), +}); + +pub fn view_transport ( + play: bool, + bpm: Arc>, + beat: Arc>, + time: Arc>, +) -> impl Content { + let theme = ItemTheme::G[96]; + Tui::bg(Black, row!(Bsp::a( + Fill::XY(Align::w(button_play_pause(play))), + Fill::XY(Align::e(row!( + FieldH(theme, "BPM", bpm), + FieldH(theme, "Beat", beat), + FieldH(theme, "Time", time), + ))) + ))) +} + +pub fn view_status ( + sel: Option>, + sr: Arc>, + buf: Arc>, + lat: Arc>, +) -> impl Content { + let theme = ItemTheme::G[96]; + Tui::bg(Black, row!(Bsp::a( + Fill::XY(Align::w(sel.map(|sel|FieldH(theme, "Selected", sel)))), + Fill::XY(Align::e(row!( + FieldH(theme, "SR", sr), + FieldH(theme, "Buf", buf), + FieldH(theme, "Lat", lat), + ))) + ))) +} + +pub(crate) fn button_play_pause (playing: bool) -> impl Content { + let compact = true;//self.is_editing(); + Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, + Either::new(compact, + Thunk::new(move|to: &mut TuiOut|to.place(&Fixed::X(9, Either::new(playing, + Tui::fg(Rgb(0, 255, 0), " PLAYING "), + Tui::fg(Rgb(255, 128, 0), " STOPPED "))) + )), + Thunk::new(move|to: &mut TuiOut|to.place(&Fixed::X(5, Either::new(playing, + Tui::fg(Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), + Tui::fg(Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",)))) + )) + ) + ) +} + +#[derive(Debug)] pub struct ViewCache { + pub sr: Memo, String>, + pub buf: Memo, String>, + pub lat: Memo, String>, + pub bpm: Memo, String>, + pub beat: Memo, String>, + pub time: Memo, String>, +} + +impl Default for ViewCache { + fn default () -> Self { + let mut beat = String::with_capacity(16); + let _ = write!(beat, "{}", Self::BEAT_EMPTY); + let mut time = String::with_capacity(16); + let _ = write!(time, "{}", Self::TIME_EMPTY); + let mut bpm = String::with_capacity(16); + let _ = write!(bpm, "{}", Self::BPM_EMPTY); + Self { + beat: Memo::new(None, beat), + time: Memo::new(None, time), + bpm: Memo::new(None, bpm), + sr: Memo::new(None, String::with_capacity(16)), + buf: Memo::new(None, String::with_capacity(16)), + lat: Memo::new(None, String::with_capacity(16)), + } + } +} + +impl ViewCache { + pub const BEAT_EMPTY: &'static str = "-.-.--"; + pub const TIME_EMPTY: &'static str = "-.---s"; + pub const BPM_EMPTY: &'static str = "---.---"; + + //pub fn track_counter (cache: &Arc>, track: usize, tracks: usize) + //-> Arc> + //{ + //let data = (track, tracks); + //cache.write().unwrap().trks.update(Some(data), rewrite!(buf, "{}/{}", data.0, data.1)); + //cache.read().unwrap().trks.view.clone() + //} + + //pub fn scene_add (cache: &Arc>, scene: usize, scenes: usize, is_editing: bool) + //-> impl Content + //{ + //let data = (scene, scenes); + //cache.write().unwrap().scns.update(Some(data), rewrite!(buf, "({}/{})", data.0, data.1)); + //button_3("S", "add scene", cache.read().unwrap().scns.view.clone(), is_editing) + //} + + pub fn update_clock (cache: &Arc>, clock: &Clock, compact: bool) { + let rate = clock.timebase.sr.get(); + let chunk = clock.chunk.load(Relaxed) as f64; + let lat = chunk / rate * 1000.; + let delta = |start: &Moment|clock.global.usec.get() - start.usec.get(); + let mut cache = cache.write().unwrap(); + cache.buf.update(Some(chunk), rewrite!(buf, "{chunk}")); + cache.lat.update(Some(lat), rewrite!(buf, "{lat:.1}ms")); + cache.sr.update(Some((compact, rate)), |buf,_,_|{ + buf.clear(); + if compact { + write!(buf, "{:.1}kHz", rate / 1000.) + } else { + write!(buf, "{:.0}Hz", rate) + } + }); + if let Some(now) = clock.started.read().unwrap().as_ref().map(delta) { + let pulse = clock.timebase.usecs_to_pulse(now); + let time = now/1000000.; + let bpm = clock.timebase.bpm.get(); + cache.beat.update(Some(pulse), |buf, _, _|{ + buf.clear(); + clock.timebase.format_beats_1_to(buf, pulse) + }); + cache.time.update(Some(time), rewrite!(buf, "{:.3}s", time)); + cache.bpm.update(Some(bpm), rewrite!(buf, "{:.3}", bpm)); + } else { + cache.beat.update(None, rewrite!(buf, "{}", ViewCache::BEAT_EMPTY)); + cache.time.update(None, rewrite!(buf, "{}", ViewCache::TIME_EMPTY)); + cache.bpm.update(None, rewrite!(buf, "{}", ViewCache::BPM_EMPTY)); + } + } + + //pub fn view_h2 (&self) -> impl Content { + //let cache = self.project.clock.view_cache.clone(); + //let cache = cache.read().unwrap(); + //add(&Fixed::x(15, Align::w(Bsp::s( + //FieldH(theme, "Beat", cache.beat.view.clone()), + //FieldH(theme, "Time", cache.time.view.clone()), + //)))); + //add(&Fixed::x(13, Align::w(Bsp::s( + //Fill::X(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))), + //Fill::X(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))), + //)))); + //add(&Fixed::x(12, Align::w(Bsp::s( + //Fill::X(Align::w(FieldH(theme, "Buf", cache.buf.view.clone()))), + //Fill::X(Align::w(FieldH(theme, "Lat", cache.lat.view.clone()))), + //)))); + //add(&Bsp::s( + //Fill::X(Align::w(FieldH(theme, "Selected", Align::w(self.selection().describe( + //self.tracks(), + //self.scenes() + //))))), + //Fill::X(Align::w(FieldH(theme, format!("History ({})", self.history.len()), + //self.history.last().map(|last|Fill::X(Align::w(format!("{:?}", last.0))))))) + //)); + ////if let Some(last) = self.history.last() { + ////add(&FieldV(theme, format!("History ({})", self.history.len()), + ////Fill::X(Align::w(format!("{:?}", last.0))))); + ////} + //} +} diff --git a/device/device.rs b/device/device.rs new file mode 100644 index 00000000..0c808131 --- /dev/null +++ b/device/device.rs @@ -0,0 +1,186 @@ +#![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 new file mode 100644 index 00000000..2ec4d37e --- /dev/null +++ b/device/editor.rs @@ -0,0 +1,550 @@ +use crate::*; + +#[macro_export] macro_rules! has_editor { + (|$self:ident: $Struct:ident|{ + editor = $e0:expr; + editor_w = $e1:expr; + editor_h = $e2:expr; + is_editing = $e3:expr; + }) => { + impl HasEditor for $Struct { + fn editor (&$self) -> Option<&MidiEditor> { $e0.as_ref() } + fn editor_mut (&mut $self) -> Option<&mut MidiEditor> { $e0.as_mut() } + fn editor_w (&$self) -> usize { $e1 } + fn editor_h (&$self) -> usize { $e2 } + fn is_editing (&$self) -> bool { $e3 } + } + }; +} +impl>> HasEditor for T {} +pub trait HasEditor: Has> { + fn editor (&self) -> Option<&MidiEditor> { self.get().as_ref() } + fn editor_mut (&mut self) -> Option<&mut MidiEditor> { self.get_mut().as_mut() } + fn is_editing (&self) -> bool { self.editor().is_some() } + fn editor_w (&self) -> usize { self.editor().map(|e|e.size.w()).unwrap_or(0) } + fn editor_h (&self) -> usize { self.editor().map(|e|e.size.h()).unwrap_or(0) } +} + +/// Contains state for viewing and editing a clip +pub struct MidiEditor { + /// Size of editor on screen + pub size: Measure, + /// View mode and state of editor + pub mode: PianoHorizontal, +} + +has!(Measure: |self: MidiEditor|self.size); +impl Default for MidiEditor { fn default () -> Self { Self { size: Measure::new(), mode: PianoHorizontal::new(None), } } } +impl std::fmt::Debug for MidiEditor { + fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("MidiEditor").field("mode", &self.mode).finish() + } +} +from!(|clip: &Arc>|MidiEditor = { let model = Self::from(Some(clip.clone())); model.redraw(); model }); +from!(|clip: Option>>|MidiEditor = { + let mut model = Self::default(); + *model.clip_mut() = clip; + model.redraw(); + model +}); + +impl MidiEditor { + /// Put note at current position + pub fn put_note (&mut self, advance: bool) { + let mut redraw = false; + if let Some(clip) = self.clip() { + let mut clip = clip.write().unwrap(); + let note_start = self.get_time_pos(); + let note_pos = self.get_note_pos(); + let note_len = self.get_note_len(); + let note_end = note_start + (note_len.saturating_sub(1)); + let key: u7 = u7::from(note_pos as u8); + let vel: u7 = 100.into(); + let length = clip.length; + let note_end = note_end % length; + let note_on = MidiMessage::NoteOn { key, vel }; + if !clip.notes[note_start].iter().any(|msg|*msg == note_on) { + clip.notes[note_start].push(note_on); + } + let note_off = MidiMessage::NoteOff { key, vel }; + if !clip.notes[note_end].iter().any(|msg|*msg == note_off) { + clip.notes[note_end].push(note_off); + } + if advance { + self.set_time_pos((note_end + 1) % clip.length); + } + redraw = true; + } + if redraw { + self.mode.redraw(); + } + } +} +impl TimeRange for MidiEditor { + fn time_len (&self) -> &AtomicUsize { self.mode.time_len() } + fn time_zoom (&self) -> &AtomicUsize { self.mode.time_zoom() } + fn time_lock (&self) -> &AtomicBool { self.mode.time_lock() } + fn time_start (&self) -> &AtomicUsize { self.mode.time_start() } + fn time_axis (&self) -> &AtomicUsize { self.mode.time_axis() } +} +impl NoteRange for MidiEditor { + fn note_lo (&self) -> &AtomicUsize { self.mode.note_lo() } + fn note_axis (&self) -> &AtomicUsize { self.mode.note_axis() } +} +impl NotePoint for MidiEditor { + fn note_len (&self) -> &AtomicUsize { self.mode.note_len() } + fn note_pos (&self) -> &AtomicUsize { self.mode.note_pos() } +} +impl TimePoint for MidiEditor { + fn time_pos (&self) -> &AtomicUsize { self.mode.time_pos() } +} +impl MidiViewer for MidiEditor { + fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { self.mode.buffer_size(clip) } + fn redraw (&self) { self.mode.redraw() } + fn clip (&self) -> &Option>> { self.mode.clip() } + fn clip_mut (&mut self) -> &mut Option>> { self.mode.clip_mut() } + fn set_clip (&mut self, p: Option<&Arc>>) { self.mode.set_clip(p) } +} + +def_command!(MidiEditCommand: |editor: MidiEditor| { + Show { clip: Option>> } => { + editor.set_clip(clip.as_ref()); editor.redraw(); Ok(None) }, + DeleteNote => { + editor.redraw(); todo!() }, + AppendNote { advance: bool } => { + editor.put_note(*advance); editor.redraw(); Ok(None) }, + SetNotePos { pos: usize } => { + editor.set_note_pos((*pos).min(127)); editor.redraw(); Ok(None) }, + SetNoteLen { len: usize } => { + editor.set_note_len(*len); editor.redraw(); Ok(None) }, + SetNoteScroll { scroll: usize } => { + editor.set_note_lo((*scroll).min(127)); editor.redraw(); Ok(None) }, + SetTimePos { pos: usize } => { + editor.set_time_pos(*pos); editor.redraw(); Ok(None) }, + SetTimeScroll { scroll: usize } => { + editor.set_time_start(*scroll); editor.redraw(); Ok(None) }, + SetTimeZoom { zoom: usize } => { + editor.set_time_zoom(*zoom); editor.redraw(); Ok(None) }, + SetTimeLock { lock: bool } => { + editor.set_time_lock(*lock); editor.redraw(); Ok(None) }, + // TODO: 1-9 seek markers that by default start every 8th of the clip +}); + +impl MidiEditor { + fn _todo_opt_clip_stub (&self) -> Option>> { todo!() } + fn clip_length (&self) -> usize { self.clip().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1) } + fn note_length (&self) -> usize { self.get_note_len() } + fn note_pos (&self) -> usize { self.get_note_pos() } + fn note_pos_next (&self) -> usize { self.get_note_pos() + 1 } + fn note_pos_next_octave (&self) -> usize { self.get_note_pos() + 12 } + fn note_pos_prev (&self) -> usize { self.get_note_pos().saturating_sub(1) } + fn note_pos_prev_octave (&self) -> usize { self.get_note_pos().saturating_sub(12) } + fn note_len (&self) -> usize { self.get_note_len() } + fn note_len_next (&self) -> usize { self.get_note_len() + 1 } + fn note_len_prev (&self) -> usize { self.get_note_len().saturating_sub(1) } + fn note_range (&self) -> usize { self.get_note_axis() } + fn note_range_next (&self) -> usize { self.get_note_axis() + 1 } + fn note_range_prev (&self) -> usize { self.get_note_axis().saturating_sub(1) } + fn time_zoom (&self) -> usize { self.get_time_zoom() } + fn time_zoom_next (&self) -> usize { self.get_time_zoom() + 1 } + fn time_zoom_next_fine (&self) -> usize { self.get_time_zoom() + 1 } + fn time_zoom_prev (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } + fn time_zoom_prev_fine (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) } + fn time_lock (&self) -> bool { self.get_time_lock() } + fn time_lock_toggled (&self) -> bool { !self.get_time_lock() } + fn time_pos (&self) -> usize { self.get_time_pos() } + fn time_pos_next (&self) -> usize { (self.get_time_pos() + self.get_note_len()) % self.clip_length() } + fn time_pos_next_fine (&self) -> usize { (self.get_time_pos() + 1) % self.clip_length() } + fn time_pos_prev (&self) -> usize { + let step = self.get_note_len(); + self.get_time_pos().overflowing_sub(step) + .0.min(self.clip_length().saturating_sub(step)) + } + fn time_pos_prev_fine (&self) -> usize { + self.get_time_pos().overflowing_sub(1) + .0.min(self.clip_length().saturating_sub(1)) + } +} + +impl Draw for MidiEditor { fn draw (&self, to: &mut TuiOut) { self.content().draw(to) } } +impl Layout for MidiEditor { fn layout (&self, to: [u16;4]) -> [u16;4] { self.content().layout(to) } } +impl HasContent for MidiEditor { + fn content (&self) -> impl Content { self.autoscroll(); /*self.autozoom();*/ self.size.of(&self.mode) } +} + +impl MidiEditor { + pub fn clip_status (&self) -> impl Content + '_ { + let (_color, name, length, looped) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { + (clip.color, clip.name.clone(), clip.length, clip.looped) + } else { (ItemTheme::G[64], String::new().into(), 0, false) }; + Fixed::X(20, col!( + Fill::X(Align::w(Bsp::e( + button_2("f2", "name ", false), + Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{name} "))))))), + Fill::X(Align::w(Bsp::e( + button_2("l", "ength ", false), + Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{length} "))))))), + Fill::X(Align::w(Bsp::e( + button_2("r", "epeat ", false), + Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), format!("{looped} "))))))), + )) + } + pub fn edit_status (&self) -> impl Content + '_ { + let (_color, length) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { + (clip.color, clip.length) + } else { (ItemTheme::G[64], 0) }; + let time_pos = self.get_time_pos(); + let time_zoom = self.get_time_zoom(); + let time_lock = if self.get_time_lock() { "[lock]" } else { " " }; + let note_pos = self.get_note_pos(); + let note_name = format!("{:4}", Note::pitch_to_name(note_pos)); + let note_pos = format!("{:>3}", note_pos); + let note_len = format!("{:>4}", self.get_note_len()); + Fixed::X(20, col!( + Fill::X(Align::w(Bsp::e( + button_2("t", "ime ", false), + Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), + format!("{length} /{time_zoom} +{time_pos} "))))))), + Fill::X(Align::w(Bsp::e( + button_2("z", "lock ", false), + Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), + format!("{time_lock}"))))))), + Fill::X(Align::w(Bsp::e( + button_2("x", "note ", false), + Fill::X(Align::e(Tui::fg(Rgb(255, 255, 255), + format!("{note_name} {note_pos} {note_len}"))))))), + )) + } +} + +/// A clip, rendered as a horizontal piano roll. +#[derive(Clone)] +pub struct PianoHorizontal { + pub clip: Option>>, + /// Buffer where the whole clip is rerendered on change + pub buffer: Arc>, + /// Size of actual notes area + pub size: Measure, + /// The display window + pub range: MidiRangeModel, + /// The note cursor + pub point: MidiPointModel, + /// The highlight color palette + pub color: ItemTheme, + /// Width of the keyboard + pub keys_width: u16, +} + +has!(Measure:|self:PianoHorizontal|self.size); + +impl PianoHorizontal { + pub fn new (clip: Option<&Arc>>) -> Self { + let size = Measure::new(); + let mut range = MidiRangeModel::from((12, true)); + range.time_axis = size.x.clone(); + range.note_axis = size.y.clone(); + let piano = Self { + keys_width: 5, + size, + range, + buffer: RwLock::new(Default::default()).into(), + point: MidiPointModel::default(), + clip: clip.cloned(), + color: clip.as_ref().map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]), + }; + piano.redraw(); + piano + } +} + +pub(crate) fn note_y_iter (note_lo: usize, note_hi: usize, y0: u16) + -> impl Iterator +{ + (note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n)) +} + +impl Draw for PianoHorizontal { fn draw (&self, to: &mut TuiOut) { self.content().draw(to) } } +impl Layout for PianoHorizontal { fn layout (&self, to: [u16;4]) -> [u16;4] { self.content().layout(to) } } +impl HasContent for PianoHorizontal { + fn content (&self) -> impl Content { + Bsp::s( + Bsp::e(Fixed::X(5, format!("{}x{}", self.size.w(), self.size.h())), self.timeline()), + Bsp::e(self.keys(), self.size.of(Bsp::b(Fill::XY(self.notes()), Fill::XY(self.cursor())))), + ) + } +} + +impl PianoHorizontal { + /// Draw the piano roll background. + /// + /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ + fn draw_bg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize, note_len: usize, note_point: usize, time_point: usize) { + for (y, note) in (0..=127).rev().enumerate() { + for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) { + let cell = buf.get_mut(x, y).unwrap(); + if note == (127-note_point) || time == time_point { + cell.set_bg(Rgb(0,0,0)); + } else { + cell.set_bg(clip.color.darkest.rgb); + } + if time % 384 == 0 { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('│'); + } else if time % 96 == 0 { + cell.set_fg(clip.color.dark.rgb); + cell.set_char('╎'); + } else if time % note_len == 0 { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('┊'); + } else if (127 - note) % 12 == 0 { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('='); + } else if (127 - note) % 6 == 0 { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('—'); + } else { + cell.set_fg(clip.color.darker.rgb); + cell.set_char('·'); + } + } + } + } + /// Draw the piano roll foreground. + /// + /// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄ + fn draw_fg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize) { + let style = Style::default().fg(clip.color.base.rgb);//.bg(Rgb(0, 0, 0)); + let mut notes_on = [false;128]; + for (x, time_start) in (0..clip.length).step_by(zoom).enumerate() { + for (_y, note) in (0..=127).rev().enumerate() { + if let Some(cell) = buf.get_mut(x, note) { + if notes_on[note] { + cell.set_char('▂'); + cell.set_style(style); + } + } + } + let time_end = time_start + zoom; + for time in time_start..time_end.min(clip.length) { + for event in clip.notes[time].iter() { + match event { + MidiMessage::NoteOn { key, .. } => { + let note = key.as_int() as usize; + if let Some(cell) = buf.get_mut(x, note) { + cell.set_char('█'); + cell.set_style(style); + } + notes_on[note] = true + }, + MidiMessage::NoteOff { key, .. } => { + notes_on[key.as_int() as usize] = false + }, + _ => {} + } + } + } + + } + } + fn notes (&self) -> impl Content { + let time_start = self.get_time_start(); + let note_lo = self.get_note_lo(); + let note_hi = self.get_note_hi(); + let buffer = self.buffer.clone(); + Thunk::new(move|to: &mut TuiOut|{ + let source = buffer.read().unwrap(); + let [x0, y0, w, _h] = to.area().xywh(); + //if h as usize != note_axis { + //panic!("area height mismatch: {h} <> {note_axis}"); + //} + for (area_x, screen_x) in (x0..x0+w).enumerate() { + for (area_y, screen_y, _note) in note_y_iter(note_lo, note_hi, y0) { + let source_x = time_start + area_x; + let source_y = note_hi - area_y; + // TODO: enable loop rollover: + //let source_x = (time_start + area_x) % source.width.max(1); + //let source_y = (note_hi - area_y) % source.height.max(1); + let is_in_x = source_x < source.width; + let is_in_y = source_y < source.height; + if is_in_x && is_in_y { + if let Some(source_cell) = source.get(source_x, source_y) { + if let Some(cell) = to.buffer.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) { + *cell = source_cell.clone(); + } + } + } + } + } + }) + } + fn cursor (&self) -> impl Content { + let note_hi = self.get_note_hi(); + let note_lo = self.get_note_lo(); + let note_pos = self.get_note_pos(); + let note_len = self.get_note_len(); + let time_pos = self.get_time_pos(); + let time_start = self.get_time_start(); + let time_zoom = self.get_time_zoom(); + let style = Some(Style::default().fg(self.color.lightest.rgb)); + Thunk::new(move|to: &mut TuiOut|{ + let [x0, y0, w, _] = to.area().xywh(); + for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { + if note == note_pos { + for x in 0..w { + let screen_x = x0 + x; + let time_1 = time_start + x as usize * time_zoom; + let time_2 = time_1 + time_zoom; + if time_1 <= time_pos && time_pos < time_2 { + to.blit(&"█", screen_x, screen_y, style); + let tail = note_len as u16 / time_zoom as u16; + for x_tail in (screen_x + 1)..(screen_x + tail) { + to.blit(&"▂", x_tail, screen_y, style); + } + break + } + } + break + } + } + }) + } + fn keys (&self) -> impl Content { + let state = self; + let color = state.color; + let note_lo = state.get_note_lo(); + let note_hi = state.get_note_hi(); + let note_pos = state.get_note_pos(); + let key_style = Some(Style::default().fg(Rgb(192, 192, 192)).bg(Rgb(0, 0, 0))); + let off_style = Some(Style::default().fg(Tui::g(255))); + let on_style = Some(Style::default().fg(Rgb(255,0,0)).bg(color.base.rgb).bold()); + Fill::Y(Fixed::X(self.keys_width, Thunk::new(move|to: &mut TuiOut|{ + let [x, y0, _w, _h] = to.area().xywh(); + for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { + to.blit(&to_key(note), x, screen_y, key_style); + if note > 127 { + continue + } + if note == note_pos { + to.blit(&format!("{:<5}", Note::pitch_to_name(note)), x, screen_y, on_style) + } else { + to.blit(&Note::pitch_to_name(note), x, screen_y, off_style) + }; + } + }))) + } + fn timeline (&self) -> impl Content + '_ { + Fill::X(Fixed::Y(1, Thunk::new(move|to: &mut TuiOut|{ + let [x, y, w, _h] = to.area(); + let style = Some(Style::default().dim()); + let length = self.clip.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); + for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) { + let t = area_x as usize * self.time_zoom().get(); + if t < length { + to.blit(&"|", screen_x, y, style); + } + } + }))) + } +} + +impl TimeRange for PianoHorizontal { + fn time_len (&self) -> &AtomicUsize { self.range.time_len() } + fn time_zoom (&self) -> &AtomicUsize { self.range.time_zoom() } + fn time_lock (&self) -> &AtomicBool { self.range.time_lock() } + fn time_start (&self) -> &AtomicUsize { self.range.time_start() } + fn time_axis (&self) -> &AtomicUsize { self.range.time_axis() } +} + +impl NoteRange for PianoHorizontal { + fn note_lo (&self) -> &AtomicUsize { self.range.note_lo() } + fn note_axis (&self) -> &AtomicUsize { self.range.note_axis() } +} + +impl NotePoint for PianoHorizontal { + fn note_len (&self) -> &AtomicUsize { self.point.note_len() } + fn note_pos (&self) -> &AtomicUsize { self.point.note_pos() } +} + +impl TimePoint for PianoHorizontal { + fn time_pos (&self) -> &AtomicUsize { self.point.time_pos() } +} + +impl MidiViewer for PianoHorizontal { + fn clip (&self) -> &Option>> { &self.clip } + fn clip_mut (&mut self) -> &mut Option>> { &mut self.clip } + /// Determine the required space to render the clip. + fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { (clip.length / self.range.time_zoom().get(), 128) } + fn redraw (&self) { + *self.buffer.write().unwrap() = if let Some(clip) = self.clip.as_ref() { + let clip = clip.read().unwrap(); + let buf_size = self.buffer_size(&clip); + let mut buffer = BigBuffer::from(buf_size); + let time_zoom = self.get_time_zoom(); + self.time_len().set(clip.length); + PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom,self.get_note_len(), self.get_note_pos(), self.get_time_pos()); + PianoHorizontal::draw_fg(&mut buffer, &clip, time_zoom); + buffer + } else { + Default::default() + } + } + fn set_clip (&mut self, clip: Option<&Arc>>) { + *self.clip_mut() = clip.cloned(); + self.color = clip.map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]); + self.redraw(); + } +} + +impl std::fmt::Debug for PianoHorizontal { + fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + let buffer = self.buffer.read().unwrap(); + f.debug_struct("PianoHorizontal") + .field("time_zoom", &self.range.time_zoom) + .field("buffer", &format!("{}x{}", buffer.width, buffer.height)) + .finish() + } +} + +fn to_key (note: usize) -> &'static str { + match note % 12 { + 11 | 9 | 7 | 5 | 4 | 2 | 0 => "████▌", + 10 | 8 | 6 | 3 | 1 => " ", + _ => unreachable!(), + } +} + +pub struct OctaveVertical { on: [bool; 12], colors: [Color; 3] } +impl Default for OctaveVertical { + fn default () -> Self { + Self { on: [false; 12], colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)] } + } +} +impl OctaveVertical { + fn color (&self, pitch: usize) -> Color { + let pitch = pitch % 12; + self.colors[if self.on[pitch] { 2 } else { match pitch { 0 | 2 | 4 | 5 | 6 | 8 | 10 => 0, _ => 1 } }] + } +} +impl HasContent for OctaveVertical { + fn content (&self) -> impl Content + '_ { + row!( + Tui::fg_bg(self.color(0), self.color(1), "▙"), + Tui::fg_bg(self.color(2), self.color(3), "▙"), + Tui::fg_bg(self.color(4), self.color(5), "▌"), + Tui::fg_bg(self.color(6), self.color(7), "▟"), + Tui::fg_bg(self.color(8), self.color(9), "▟"), + Tui::fg_bg(self.color(10), self.color(11), "▟"), + ) + } +} + // Update sequencer playhead indicator + //self.now().set(0.); + //if let Some((ref started_at, Some(ref playing))) = self.sequencer.play_clip { + //let clip = clip.read().unwrap(); + //if *playing.read().unwrap() == *clip { + //let pulse = self.current().pulse.get(); + //let start = started_at.pulse.get(); + //let now = (pulse - start) % clip.length as f64; + //self.now().set(now); + //} + //} diff --git a/device/lv2.rs b/device/lv2.rs new file mode 100644 index 00000000..2330ce38 --- /dev/null +++ b/device/lv2.rs @@ -0,0 +1,310 @@ +use crate::*; + +/// A LV2 plugin. +#[derive(Debug)] +pub struct Lv2 { + /// JACK client handle (needs to not be dropped for standalone mode to work). + pub jack: Jack<'static>, + pub name: Arc, + pub path: Option>, + pub selected: usize, + pub mapping: bool, + pub midi_ins: Vec>, + pub midi_outs: Vec>, + pub audio_ins: Vec>, + pub audio_outs: Vec>, + + pub lv2_world: livi::World, + pub lv2_instance: livi::Instance, + pub lv2_plugin: livi::Plugin, + pub lv2_features: Arc, + pub lv2_port_list: Vec, + pub lv2_input_buffer: Vec, + pub lv2_ui_thread: Option>, +} + +impl Lv2 { + + pub fn new ( + jack: &Jack<'static>, + name: &str, + uri: &str, + ) -> Usually { + let lv2_world = livi::World::with_load_bundle(&uri); + let lv2_features = lv2_world.build_features(livi::FeaturesBuilder { + min_block_length: 1, + max_block_length: 65536, + }); + let lv2_plugin = lv2_world.iter_plugins().nth(0) + .unwrap_or_else(||panic!("plugin not found: {uri}")); + Ok(Self { + jack: jack.clone(), + name: name.into(), + path: Some(String::from(uri).into()), + selected: 0, + mapping: false, + midi_ins: vec![], + midi_outs: vec![], + audio_ins: vec![], + audio_outs: vec![], + lv2_instance: unsafe { + lv2_plugin + .instantiate(lv2_features.clone(), 48000.0) + .expect(&format!("instantiate failed: {uri}")) + }, + lv2_port_list: lv2_plugin.ports().collect::>(), + lv2_input_buffer: Vec::with_capacity(Self::INPUT_BUFFER), + lv2_ui_thread: None, + lv2_world, + lv2_features, + lv2_plugin, + }) + } + + const INPUT_BUFFER: usize = 1024; + +} + + + //fn jack_from_lv2 (name: &str, plugin: &::livi::Plugin) -> Usually { + //let counts = plugin.port_counts(); + //let mut jack = Jack::new(name)?; + //for i in 0..counts.atom_sequence_inputs { + //jack = jack.midi_in(&format!("midi-in-{i}")) + //} + //for i in 0..counts.atom_sequence_outputs { + //jack = jack.midi_out(&format!("midi-out-{i}")); + //} + //for i in 0..counts.audio_inputs { + //jack = jack.audio_in(&format!("audio-in-{i}")); + //} + //for i in 0..counts.audio_outputs { + //jack = jack.audio_out(&format!("audio-out-{i}")); + //} + //Ok(jack) + //} + +audio!(|self: Lv2, _client, scope|{ + let Self { + midi_ins, + midi_outs, + audio_ins, + audio_outs, + lv2_features, + lv2_instance, + lv2_input_buffer, + .. + } = self; + let urid = lv2_features.midi_urid(); + lv2_input_buffer.clear(); + for port in midi_ins.iter() { + let mut atom = ::livi::event::LV2AtomSequence::new( + &lv2_features, + scope.n_frames() as usize + ); + for event in port.iter(scope) { + match event.bytes.len() { + 3 => atom.push_midi_event::<3>( + event.time as i64, + urid, + &event.bytes[0..3] + ).unwrap(), + _ => {} + } + } + lv2_input_buffer.push(atom); + } + let mut outputs = vec![]; + for _ in midi_outs.iter() { + outputs.push(::livi::event::LV2AtomSequence::new( + lv2_features, + scope.n_frames() as usize + )); + } + let ports = ::livi::EmptyPortConnections::new() + .with_atom_sequence_inputs(lv2_input_buffer.iter()) + .with_atom_sequence_outputs(outputs.iter_mut()) + .with_audio_inputs(audio_ins.iter().map(|o|o.as_slice(scope))) + .with_audio_outputs(audio_outs.iter_mut().map(|o|o.as_mut_slice(scope))); + unsafe { + lv2_instance.run(scope.n_frames() as usize, ports).unwrap() + }; + Control::Continue +}); + +impl Draw for Lv2 { + fn draw (&self, to: &mut TuiOut) { + let area = to.area(); + let [x, y, _, height] = area; + let mut width = 20u16; + let start = self.selected.saturating_sub((height as usize / 2).saturating_sub(1)); + let end = start + height as usize - 2; + //draw_box(buf, Rect { x, y, width, height }); + for i in start..end { + if let Some(port) = self.lv2_port_list.get(i) { + let value = if let Some(value) = self.lv2_instance.control_input(port.index) { + value + } else { + port.default_value + }; + //let label = &format!("C·· M·· {:25} = {value:.03}", port.name); + let label = &format!("{:25} = {value:.03}", port.name); + width = width.max(label.len() as u16 + 4); + let style = if i == self.selected { + Some(Style::default().green()) + } else { + None + } ; + to.blit(&label, x + 2, y + 1 + i as u16 - start as u16, style); + } else { + break + } + } + draw_header(self, to, x, y, width); + } +} + +fn draw_header (state: &Lv2, to: &mut TuiOut, x: u16, y: u16, w: u16) { + let style = Style::default().gray(); + let label1 = format!(" {}", state.name); + to.blit(&label1, x + 1, y, Some(style.white().bold())); + if let Some(ref path) = state.path { + let label2 = format!("{}…", &path[..((w as usize - 10).min(path.len()))]); + to.blit(&label2, x + 2 + label1.len() as u16, y, Some(style.not_dim())); + } + //Ok(Rect { x, y, width: w, height: 1 }) +} + +//handle!(TuiIn: |self:Plugin, from|{ + //match from.event() { + //kpat!(KeyCode::Up) => { + //self.selected = self.selected.saturating_sub(1); + //Ok(Some(true)) + //}, + //kpat!(KeyCode::Down) => { + //self.selected = (self.selected + 1).min(match &self.plugin { + //Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1, + //_ => unimplemented!() + //}); + //Ok(Some(true)) + //}, + //kpat!(KeyCode::PageUp) => { + //self.selected = self.selected.saturating_sub(8); + //Ok(Some(true)) + //}, + //kpat!(KeyCode::PageDown) => { + //self.selected = (self.selected + 10).min(match &self.plugin { + //Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1, + //_ => unimplemented!() + //}); + //Ok(Some(true)) + //}, + //kpat!(KeyCode::Char(',')) => { + //match self.plugin.as_mut() { + //Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => { + //let index = port_list[self.selected].index; + //if let Some(value) = instance.control_input(index) { + //instance.set_control_input(index, value - 0.01); + //} + //}, + //_ => {} + //} + //Ok(Some(true)) + //}, + //kpat!(KeyCode::Char('.')) => { + //match self.plugin.as_mut() { + //Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => { + //let index = port_list[self.selected].index; + //if let Some(value) = instance.control_input(index) { + //instance.set_control_input(index, value + 0.01); + //} + //}, + //_ => {} + //} + //Ok(Some(true)) + //}, + //kpat!(KeyCode::Char('g')) => { + //match self.plugin { + ////Some(PluginKind::LV2(ref mut plugin)) => { + ////plugin.ui_thread = Some(run_lv2_ui(LV2PluginUI::new()?)?); + ////}, + //Some(_) => unreachable!(), + //None => {} + //} + //Ok(Some(true)) + //}, + //_ => Ok(None) + //} +//}); + +//from_atom!("plugin/lv2" => |jack: &Jack, args| -> Plugin { + //let mut name = String::new(); + //let mut path = String::new(); + //atom!(atom in args { + //Atom::Map(map) => { + //if let Some(Atom::Str(n)) = map.get(&Atom::Key(":name")) { + //name = String::from(*n); + //} + //if let Some(Atom::Str(p)) = map.get(&Atom::Key(":path")) { + //path = String::from(*p); + //} + //}, + //_ => panic!("unexpected in lv2 '{name}'"), + //}); + //Plugin::new_lv2(jack, &name, &path) +//}); + +//pub struct LV2PluginUI { + //write: (), + //controller: (), + //widget: (), + //features: (), + //transfer: (), +//} + + +#[cfg(feature = "lv2_gui")] +pub fn run_lv2_ui (mut ui: LV2PluginUI) -> Usually> { + Ok(spawn(move||{ + let event_loop = EventLoop::builder().with_x11().with_any_thread(true).build().unwrap(); + event_loop.set_control_flow(ControlFlow::Wait); + event_loop.run_app(&mut ui).unwrap() + })) +} + +#[cfg(feature = "lv2_gui")] +/// A LV2 plugin's X11 UI. +pub struct LV2PluginUI { + pub window: Option +} + +#[cfg(feature = "lv2_gui")] +impl LV2PluginUI { + pub fn new () -> Usually { + Ok(Self { window: None }) + } +} + +#[cfg(feature = "lv2_gui")] +impl ApplicationHandler for LV2PluginUI { + fn resumed (&mut self, event_loop: &ActiveEventLoop) { + self.window = Some(event_loop.create_window(Window::default_attributes()).unwrap()); + } + fn window_event (&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) { + match event { + WindowEvent::CloseRequested => { + self.window.as_ref().unwrap().set_visible(false); + event_loop.exit(); + }, + WindowEvent::RedrawRequested => { + self.window.as_ref().unwrap().request_redraw(); + } + _ => (), + } + } +} + +#[cfg(feature = "lv2_gui")] +fn lv2_ui_instantiate (kind: &str) { + //let host = Suil +} diff --git a/device/meter.rs b/device/meter.rs new file mode 100644 index 00000000..8a7d9a50 --- /dev/null +++ b/device/meter.rs @@ -0,0 +1,85 @@ +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 new file mode 100644 index 00000000..99b2d74f --- /dev/null +++ b/device/mixer.rs @@ -0,0 +1,43 @@ +#[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 new file mode 100644 index 00000000..42bef43c --- /dev/null +++ b/device/pool.rs @@ -0,0 +1,411 @@ +use crate::*; +#[derive(Debug)] +pub struct Pool { + pub visible: bool, + /// Selected clip + pub clip: AtomicUsize, + /// Mode switch + pub mode: Option, + /// Collection of clips + #[cfg(feature = "clip")] pub clips: Arc>>>>, + /// Embedded file browse + #[cfg(feature = "browse")] pub browse: Option, +} +impl Default for Pool { + fn default () -> Self { + //use PoolMode::*; + Self { + visible: true, + clip: 0.into(), + mode: None, + clips: Arc::from(RwLock::from(vec![])), + browse: None, + } + } +} +impl Pool { + pub fn clip_index (&self) -> usize { + self.clip.load(Relaxed) + } + pub fn set_clip_index (&self, value: usize) { + self.clip.store(value, Relaxed); + } + pub fn mode (&self) -> &Option { + &self.mode + } + pub fn mode_mut (&mut self) -> &mut Option { + &mut self.mode + } + pub fn begin_clip_length (&mut self) { + let length = self.clips()[self.clip_index()].read().unwrap().length; + *self.mode_mut() = Some(PoolMode::Length( + self.clip_index(), + length, + ClipLengthFocus::Bar + )); + } + pub fn begin_clip_rename (&mut self) { + let name = self.clips()[self.clip_index()].read().unwrap().name.clone(); + *self.mode_mut() = Some(PoolMode::Rename( + self.clip_index(), + name + )); + } + pub fn begin_import (&mut self) -> Usually<()> { + *self.mode_mut() = Some(PoolMode::Import( + self.clip_index(), + Browse::new(None)? + )); + Ok(()) + } + pub fn begin_export (&mut self) -> Usually<()> { + *self.mode_mut() = Some(PoolMode::Export( + self.clip_index(), + Browse::new(None)? + )); + Ok(()) + } + pub fn new_clip (&self) -> MidiClip { + MidiClip::new("Clip", true, 4 * PPQ, None, Some(ItemTheme::random())) + } + pub fn cloned_clip (&self) -> MidiClip { + let index = self.clip_index(); + let mut clip = self.clips()[index].read().unwrap().duplicate(); + clip.color = ItemTheme::random_near(clip.color, 0.25); + clip + } + pub fn add_new_clip (&self) -> (usize, Arc>) { + let clip = Arc::new(RwLock::new(self.new_clip())); + let index = { + let mut clips = self.clips.write().unwrap(); + clips.push(clip.clone()); + clips.len().saturating_sub(1) + }; + self.clip.store(index, Relaxed); + (index, clip) + } + pub fn delete_clip (&mut self, clip: &MidiClip) -> bool { + let index = self.clips.read().unwrap().iter().position(|x|*x.read().unwrap()==*clip); + if let Some(index) = index { + self.clips.write().unwrap().remove(index); + return true + } + false + } +} +/// Modes for clip pool +#[derive(Debug, Clone)] +pub enum PoolMode { + /// Renaming a pattern + Rename(usize, Arc), + /// Editing the length of a pattern + Length(usize, usize, ClipLengthFocus), + /// Load clip from disk + Import(usize, Browse), + /// Save clip to disk + Export(usize, Browse), +} +/// Focused field of `ClipLength` +#[derive(Copy, Clone, Debug)] +pub enum ClipLengthFocus { + /// Editing the number of bars + Bar, + /// Editing the number of beats + Beat, + /// Editing the number of ticks + Tick, +} +impl ClipLengthFocus { + pub fn next (&mut self) { + use ClipLengthFocus::*; + *self = match self { Bar => Beat, Beat => Tick, Tick => Bar, } + } + pub fn prev (&mut self) { + use ClipLengthFocus::*; + *self = match self { Bar => Tick, Beat => Bar, Tick => Beat, } + } +} +/// Displays and edits clip length. +#[derive(Clone)] +pub struct ClipLength { + /// Pulses per beat (quaver) + ppq: usize, + /// Beats per bar + bpb: usize, + /// Length of clip in pulses + pulses: usize, + /// Selected subdivision + pub focus: Option, +} +impl ClipLength { + pub fn _new (pulses: usize, focus: Option) -> Self { + Self { ppq: PPQ, bpb: 4, pulses, focus } + } + pub fn bars (&self) -> usize { + self.pulses / (self.bpb * self.ppq) + } + pub fn beats (&self) -> usize { + (self.pulses % (self.bpb * self.ppq)) / self.ppq + } + pub fn ticks (&self) -> usize { + self.pulses % self.ppq + } + pub fn bars_string (&self) -> Arc { + format!("{}", self.bars()).into() + } + pub fn beats_string (&self) -> Arc { + format!("{}", self.beats()).into() + } + pub fn ticks_string (&self) -> Arc { + format!("{:>02}", self.ticks()).into() + } +} +pub type ClipPool = Vec>>; +pub trait HasClips { + fn clips <'a> (&'a self) -> std::sync::RwLockReadGuard<'a, ClipPool>; + fn clips_mut <'a> (&'a self) -> std::sync::RwLockWriteGuard<'a, ClipPool>; + fn add_clip (&self) -> (usize, Arc>) { + let clip = Arc::new(RwLock::new(MidiClip::new("Clip", true, 384, None, None))); + self.clips_mut().push(clip.clone()); + (self.clips().len() - 1, clip) + } +} +#[macro_export] macro_rules! has_clips { + (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? { + fn clips <'a> (&'a $self) -> std::sync::RwLockReadGuard<'a, ClipPool> { + $cb.read().unwrap() + } + fn clips_mut <'a> (&'a $self) -> std::sync::RwLockWriteGuard<'a, ClipPool> { + $cb.write().unwrap() + } + } + } +} +has_clips!(|self: Pool|self.clips); +has_clip!(|self: Pool|self.clips().get(self.clip_index()).map(|c|c.clone())); +from!(|clip:&Arc>|Pool = { + let model = Self::default(); model.clips.write().unwrap().push(clip.clone()); model.clip.store(1, Relaxed); model +}); +impl Pool { + fn _todo_usize_ (&self) -> usize { todo!() } + fn _todo_bool_ (&self) -> bool { todo!() } + fn _todo_clip_ (&self) -> MidiClip { todo!() } + fn _todo_path_ (&self) -> PathBuf { todo!() } + fn _todo_color_ (&self) -> ItemColor { todo!() } + fn _todo_str_ (&self) -> Arc { todo!() } + fn clip_new (&self) -> MidiClip { self.new_clip() } + fn clip_cloned (&self) -> MidiClip { self.cloned_clip() } + fn clip_index_current (&self) -> usize { 0 } + fn clip_index_after (&self) -> usize { 0 } + fn clip_index_previous (&self) -> usize { 0 } + fn clip_index_next (&self) -> usize { 0 } + fn color_random (&self) -> ItemColor { ItemColor::random() } +} +def_command!(PoolCommand: |pool: Pool| { + // Toggle visibility of pool + Show { visible: bool } => { pool.visible = *visible; Ok(Some(Self::Show { visible: !visible })) }, + // Select a clip from the clip pool + Select { index: usize } => { pool.set_clip_index(*index); Ok(None) }, + // Update the contents of the clip pool + Clip { command: PoolClipCommand } => Ok(command.execute(pool)?.map(|command|Self::Clip{command})), + // Rename a clip + Rename { command: RenameCommand } => Ok(command.delegate(pool, |command|Self::Rename{command})?), + // Change the length of a clip + Length { command: CropCommand } => Ok(command.delegate(pool, |command|Self::Length{command})?), + // Import from file + Import { command: BrowseCommand } => Ok(if let Some(browse) = pool.browse.as_mut() { + command.delegate(browse, |command|Self::Import{command})? + } else { + None + }), + // Export to file + Export { command: BrowseCommand } => Ok(if let Some(browse) = pool.browse.as_mut() { + command.delegate(browse, |command|Self::Export{command})? + } else { + None + }), +}); +def_command!(PoolClipCommand: |pool: Pool| { + Delete { index: usize } => { + let index = *index; + let clip = pool.clips_mut().remove(index).read().unwrap().clone(); + Ok(Some(Self::Add { index, clip })) + }, + Swap { index: usize, other: usize } => { + let index = *index; + let other = *other; + pool.clips_mut().swap(index, other); + Ok(Some(Self::Swap { index, other })) + }, + Export { index: usize, path: PathBuf } => { + todo!("export clip to midi file"); + }, + Add { index: usize, clip: MidiClip } => { + let index = *index; + let mut index = index; + let clip = Arc::new(RwLock::new(clip.clone())); + let mut clips = pool.clips_mut(); + if index >= clips.len() { + index = clips.len(); + clips.push(clip) + } else { + clips.insert(index, clip); + } + Ok(Some(Self::Delete { index })) + }, + Import { index: usize, path: PathBuf } => { + let index = *index; + let bytes = std::fs::read(&path)?; + let smf = Smf::parse(bytes.as_slice())?; + let mut t = 0u32; + let mut events = vec![]; + for track in smf.tracks.iter() { + for event in track.iter() { + t += event.delta.as_int(); + if let TrackEventKind::Midi { channel, message } = event.kind { + events.push((t, channel.as_int(), message)); + } + } + } + let mut clip = MidiClip::new("imported", true, t as usize + 1, None, None); + for event in events.iter() { + clip.notes[event.0 as usize].push(event.2); + } + Ok(Self::Add { index, clip }.execute(pool)?) + }, + SetName { index: usize, name: Arc } => { + let index = *index; + let clip = &mut pool.clips_mut()[index]; + let old_name = clip.read().unwrap().name.clone(); + clip.write().unwrap().name = name.clone(); + Ok(Some(Self::SetName { index, name: old_name })) + }, + SetLength { index: usize, length: usize } => { + let index = *index; + let clip = &mut pool.clips_mut()[index]; + let old_len = clip.read().unwrap().length; + clip.write().unwrap().length = *length; + Ok(Some(Self::SetLength { index, length: old_len })) + }, + SetColor { index: usize, color: ItemColor } => { + let index = *index; + let mut color = ItemTheme::from(*color); + std::mem::swap(&mut color, &mut pool.clips()[index].write().unwrap().color); + Ok(Some(Self::SetColor { index, color: color.base })) + }, +}); + +def_command!(RenameCommand: |pool: Pool| { + Begin => unreachable!(), + Cancel => { + if let Some(PoolMode::Rename(clip, ref mut old_name)) = pool.mode_mut().clone() { + pool.clips()[clip].write().unwrap().name = old_name.clone().into(); + } + Ok(None) + }, + Confirm => { + if let Some(PoolMode::Rename(_clip, ref mut old_name)) = pool.mode_mut().clone() { + let old_name = old_name.clone(); *pool.mode_mut() = None; return Ok(Some(Self::Set { value: old_name })) + } + Ok(None) + }, + Set { value: Arc } => { + if let Some(PoolMode::Rename(clip, ref mut _old_name)) = pool.mode_mut().clone() { + pool.clips()[clip].write().unwrap().name = value.clone(); + } + Ok(None) + }, +}); +def_command!(CropCommand: |pool: Pool| { + Begin => unreachable!(), + Cancel => { if let Some(PoolMode::Length(..)) = pool.mode_mut().clone() { *pool.mode_mut() = None; } Ok(None) }, + Set { length: usize } => { + if let Some(PoolMode::Length(clip, ref mut length, ref mut _focus)) + = pool.mode_mut().clone() + { + let old_length; + { + let clip = pool.clips()[clip].clone();//.write().unwrap(); + old_length = Some(clip.read().unwrap().length); + clip.write().unwrap().length = *length; + } + *pool.mode_mut() = None; + return Ok(old_length.map(|length|Self::Set { length })) + } + Ok(None) + }, + Next => { + if let Some(PoolMode::Length(_clip, ref mut _length, ref mut focus)) = pool.mode_mut().clone() { focus.next() }; Ok(None) + }, + Prev => { + if let Some(PoolMode::Length(_clip, ref mut _length, ref mut focus)) = pool.mode_mut().clone() { focus.prev() }; Ok(None) + }, + Inc => { + if let Some(PoolMode::Length(_clip, ref mut length, ref mut focus)) = pool.mode_mut().clone() { + match focus { + ClipLengthFocus::Bar => { *length += 4 * PPQ }, + ClipLengthFocus::Beat => { *length += PPQ }, + ClipLengthFocus::Tick => { *length += 1 }, + } + } + Ok(None) + }, + Dec => { + if let Some(PoolMode::Length(_clip, ref mut length, ref mut focus)) = pool.mode_mut().clone() { + match focus { + ClipLengthFocus::Bar => { *length = length.saturating_sub(4 * PPQ) }, + ClipLengthFocus::Beat => { *length = length.saturating_sub(PPQ) }, + ClipLengthFocus::Tick => { *length = length.saturating_sub(1) }, + } + } + Ok(None) + } +}); +pub struct PoolView<'a>(pub &'a Pool); +impl<'a> HasContent for PoolView<'a> { + fn content (&self) -> impl Content { + let Self(pool) = self; + //let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||Tui::g(32).into()); + //let on_bg = |x|x;//Bsp::b(Repeat(" "), Tui::bg(color.darkest.rgb, x)); + //let border = |x|x;//Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)).enclose(x); + //let height = pool.clips.read().unwrap().len() as u16; + Fixed::X(20, Fill::Y(Align::n(Map::new( + ||pool.clips().clone().into_iter(), + move|clip: Arc>, i: usize|{ + let item_height = 1; + let item_offset = i as u16 * item_height; + let selected = i == pool.clip_index(); + let MidiClip { ref name, color, length, .. } = *clip.read().unwrap(); + let bg = if selected { color.light.rgb } else { color.base.rgb }; + let fg = color.lightest.rgb; + let name = if false { format!(" {i:>3}") } else { format!(" {i:>3} {name}") }; + let length = if false { String::default() } else { format!("{length} ") }; + Fixed::Y(1, map_south(item_offset, item_height, Tui::bg(bg, lay!( + Fill::X(Align::w(Tui::fg(fg, Tui::bold(selected, name)))), + Fill::X(Align::e(Tui::fg(fg, Tui::bold(selected, length)))), + Fill::X(Align::w(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "▶"))))), + Fill::X(Align::e(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "◀"))))), + )))) + })))) + } +} +impl HasContent for ClipLength { + fn content (&self) -> impl Content + '_ { + use ClipLengthFocus::*; + let bars = ||self.bars_string(); + let beats = ||self.beats_string(); + let ticks = ||self.ticks_string(); + match self.focus { + None => row!(" ", bars(), ".", beats(), ".", ticks()), + Some(Bar) => row!("[", bars(), "]", beats(), ".", ticks()), + Some(Beat) => row!(" ", bars(), "[", beats(), "]", ticks()), + Some(Tick) => row!(" ", bars(), ".", beats(), "[", ticks()), + } + } +} +//take!(BrowseCommand |state: Pool, iter|Ok(state.browse.as_ref() + //.map(|p|Take::take(p, iter)) + //.transpose()? + //.flatten())); + diff --git a/device/port.rs b/device/port.rs new file mode 100644 index 00000000..0485f7c8 --- /dev/null +++ b/device/port.rs @@ -0,0 +1,577 @@ +use crate::*; + +def_sizes_iter!(InputsSizes => MidiInput); +def_sizes_iter!(OutputsSizes => MidiOutput); +def_sizes_iter!(PortsSizes => Arc, [Connect]); + +pub(crate) use ConnectName::*; +pub(crate) use ConnectScope::*; +pub(crate) use ConnectStatus::*; + +#[derive(Debug)] pub struct AudioInput { + /// Handle to JACK client, for receiving reconnect events. + jack: Jack<'static>, + /// Port name + name: Arc, + /// Port handle. + port: Port, + /// List of ports to connect to. + pub connections: Vec, +} + +#[derive(Debug)] pub struct AudioOutput { + /// Handle to JACK client, for receiving reconnect events. + jack: Jack<'static>, + /// Port name + name: Arc, + /// Port handle. + port: Port, + /// List of ports to connect to. + pub connections: Vec, +} + +#[derive(Debug)] pub struct MidiInput { + /// Handle to JACK client, for receiving reconnect events. + jack: Jack<'static>, + /// Port name + name: Arc, + /// Port handle. + port: Port, + /// List of currently held notes. + held: Arc>, + /// List of ports to connect to. + pub connections: Vec, +} + +#[derive(Debug)] pub struct MidiOutput { + /// Handle to JACK client, for receiving reconnect events. + jack: Jack<'static>, + /// Port name + name: Arc, + /// Port handle. + port: Port, + /// List of ports to connect to. + pub connections: Vec, + /// List of currently held notes. + held: Arc>, + /// Buffer + note_buffer: Vec, + /// Buffer + output_buffer: Vec>>, +} + +pub trait RegisterPorts: HasJack<'static> { + /// Register a MIDI input port. + fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; + /// Register a MIDI output port. + fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; + /// Register an audio input port. + fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; + /// Register an audio output port. + fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; +} + +impl> RegisterPorts for J { + fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + MidiInput::new(self.jack(), name, connect) + } + fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + MidiOutput::new(self.jack(), name, connect) + } + fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + AudioInput::new(self.jack(), name, connect) + } + fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + AudioOutput::new(self.jack(), name, connect) + } +} +//take!(MidiInputCommand |state: Arrangement, iter|state.selected_midi_in().as_ref() + //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); +//take!(MidiOutputCommand |state: Arrangement, iter|state.selected_midi_out().as_ref() + //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); + +pub trait JackPort: HasJack<'static> { + type Port: PortSpec + Default; + type Pair: PortSpec + Default; + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized; + fn register (jack: &Jack<'static>, name: &impl AsRef) -> Usually> { + jack.with_client(|c|c.register_port::(name.as_ref(), Default::default())) + .map_err(|e|e.into()) + } + fn port_name (&self) -> &Arc; + fn connections (&self) -> &[Connect]; + fn port (&self) -> &Port; + fn port_mut (&mut self) -> &mut Port; + fn into_port (self) -> Port where Self: Sized; + fn close (self) -> Usually<()> where Self: Sized { + let jack = self.jack().clone(); + Ok(jack.with_client(|c|c.unregister_port(self.into_port()))?) + } + fn ports (&self, re_name: Option<&str>, re_type: Option<&str>, flags: PortFlags) -> Vec { + self.with_client(|c|c.ports(re_name, re_type, flags)) + } + fn port_by_id (&self, id: u32) -> Option> { + self.with_client(|c|c.port_by_id(id)) + } + fn port_by_name (&self, name: impl AsRef) -> Option> { + self.with_client(|c|c.port_by_name(name.as_ref())) + } + fn connect_to_matching <'k> (&'k self) -> Usually<()> { + for connect in self.connections().iter() { + //panic!("{connect:?}"); + let status = match &connect.name { + Exact(name) => self.connect_exact(name), + RegExp(re) => self.connect_regexp(re, connect.scope), + }?; + *connect.status.write().unwrap() = status; + } + Ok(()) + } + fn connect_exact <'k> (&'k self, name: &str) -> + Usually, Arc, ConnectStatus)>> + { + self.with_client(move|c|{ + let mut status = vec![]; + for port in c.ports(None, None, PortFlags::empty()).iter() { + if port.as_str() == &*name { + if let Some(port) = c.port_by_name(port.as_str()) { + let port_status = self.connect_to_unowned(&port)?; + let name = port.name()?.into(); + status.push((port, name, port_status)); + if port_status == Connected { + break + } + } + } + } + Ok(status) + }) + } + fn connect_regexp <'k> ( + &'k self, re: &str, scope: ConnectScope + ) -> Usually, Arc, ConnectStatus)>> { + self.with_client(move|c|{ + let mut status = vec![]; + let ports = c.ports(Some(&re), None, PortFlags::empty()); + for port in ports.iter() { + if let Some(port) = c.port_by_name(port.as_str()) { + let port_status = self.connect_to_unowned(&port)?; + let name = port.name()?.into(); + status.push((port, name, port_status)); + if port_status == Connected && scope == One { + break + } + } + } + Ok(status) + }) + } + /** Connect to a matching port by name. */ + fn connect_to_name (&self, name: impl AsRef) -> Usually { + self.with_client(|c|if let Some(ref port) = c.port_by_name(name.as_ref()) { + self.connect_to_unowned(port) + } else { + Ok(Missing) + }) + } + /** Connect to a matching port by reference. */ + fn connect_to_unowned (&self, port: &Port) -> Usually { + self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { + Connected + } else if let Ok(_) = c.connect_ports(port, self.port()) { + Connected + } else { + Mismatch + })) + } + /** Connect to an owned matching port by reference. */ + fn connect_to_owned (&self, port: &Port) -> Usually { + self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { + Connected + } else if let Ok(_) = c.connect_ports(port, self.port()) { + Connected + } else { + Mismatch + })) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum ConnectName { + /** Exact match */ + Exact(Arc), + /** Match regular expression */ + RegExp(Arc), +} + +#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectScope { + One, + All +} + +#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectStatus { + Missing, + Disconnected, + Connected, + Mismatch, +} + +#[derive(Clone, Debug)] pub struct Connect { + pub name: ConnectName, + pub scope: ConnectScope, + pub status: Arc, Arc, ConnectStatus)>>>, + pub info: Arc, +} + +impl Connect { + pub fn collect (exact: &[impl AsRef], re: &[impl AsRef], re_all: &[impl AsRef]) + -> Vec + { + let mut connections = vec![]; + for port in exact.iter() { connections.push(Self::exact(port)) } + for port in re.iter() { connections.push(Self::regexp(port)) } + for port in re_all.iter() { connections.push(Self::regexp_all(port)) } + connections + } + /// Connect to this exact port + pub fn exact (name: impl AsRef) -> Self { + let info = format!("=:{}", name.as_ref()).into(); + let name = Exact(name.as_ref().into()); + Self { name, scope: One, status: Arc::new(RwLock::new(vec![])), info } + } + pub fn regexp (name: impl AsRef) -> Self { + let info = format!("~:{}", name.as_ref()).into(); + let name = RegExp(name.as_ref().into()); + Self { name, scope: One, status: Arc::new(RwLock::new(vec![])), info } + } + pub fn regexp_all (name: impl AsRef) -> Self { + let info = format!("+:{}", name.as_ref()).into(); + let name = RegExp(name.as_ref().into()); + Self { name, scope: All, status: Arc::new(RwLock::new(vec![])), info } + } + pub fn info (&self) -> Arc { + let status = { + let status = self.status.read().unwrap(); + let mut ok = 0; + for (_, _, state) in status.iter() { + if *state == Connected { + ok += 1 + } + } + format!("{ok}/{}", status.len()) + }; + let scope = match self.scope { + One => " ", All => "*", + }; + let name = match &self.name { + Exact(name) => format!("= {name}"), RegExp(name) => format!("~ {name}"), + }; + format!(" ({}) {} {}", status, scope, name).into() + } +} + +def_command!(MidiInputCommand: |port: MidiInput| { + Close => todo!(), + Connect { midi_out: Arc } => todo!(), +}); +def_command!(MidiOutputCommand: |port: MidiOutput| { + Close => todo!(), + Connect { midi_in: Arc } => todo!(), +}); +def_command!(AudioInputCommand: |port: AudioInput| { + Close => todo!(), + Connect { audio_out: Arc } => todo!(), +}); +def_command!(AudioOutputCommand: |port: AudioOutput| { + Close => todo!(), + Connect { audio_in: Arc } => todo!(), +}); + +impl HasJack<'static> for MidiInput { fn jack (&self) -> &Jack<'static> { &self.jack } } + +impl JackPort for MidiInput { + type Port = MidiIn; + type Pair = MidiOut; + fn port_name (&self) -> &Arc { + &self.name + } + fn port (&self) -> &Port { + &self.port + } + fn port_mut (&mut self) -> &mut Port { + &mut self.port + } + fn into_port (self) -> Port { + self.port + } + fn connections (&self) -> &[Connect] { + self.connections.as_slice() + } + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized + { + let port = Self { + port: Self::register(jack, name)?, + jack: jack.clone(), + name: name.as_ref().into(), + connections: connect.to_vec(), + held: Arc::new(RwLock::new([false;128])) + }; + port.connect_to_matching()?; + Ok(port) + } +} + +impl MidiInput { + pub fn parsed <'a> (&'a self, scope: &'a ProcessScope) -> impl Iterator, &'a [u8])> { + parse_midi_input(self.port().iter(scope)) + } +} + +impl>> HasMidiIns for T { + fn midi_ins (&self) -> &Vec { + self.get() + } + fn midi_ins_mut (&mut self) -> &mut Vec { + self.get_mut() + } +} + +/// Trait for thing that may receive MIDI. +pub trait HasMidiIns { + fn midi_ins (&self) -> &Vec; + fn midi_ins_mut (&mut self) -> &mut Vec; + /// Collect MIDI input from app ports (TODO preallocate large buffers) + fn midi_input_collect <'a> (&'a self, scope: &'a ProcessScope) -> CollectedMidiInput<'a> { + self.midi_ins().iter() + .map(|port|port.port().iter(scope) + .map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes))) + .collect::>()) + .collect::>() + } + fn midi_ins_with_sizes <'a> (&'a self) -> + impl Iterator, &'a [Connect], usize, usize)> + Send + Sync + 'a + { + let mut y = 0; + self.midi_ins().iter().enumerate().map(move|(i, input)|{ + let height = 1 + input.connections().len(); + let data = (i, input.port_name(), input.connections(), y, y + height); + y += height; + data + }) + } +} + +pub type CollectedMidiInput<'a> = Vec, MidiError>)>>; + +impl> AddMidiIn for T { + fn midi_in_add (&mut self) -> Usually<()> { + let index = self.midi_ins().len(); + let port = MidiInput::new(self.jack(), &format!("M/{index}"), &[])?; + self.midi_ins_mut().push(port); + Ok(()) + } +} + +/// May create new MIDI input ports. +pub trait AddMidiIn { + fn midi_in_add (&mut self) -> Usually<()>; +} + +impl HasJack<'static> for MidiOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } + +impl JackPort for MidiOutput { + type Port = MidiOut; + type Pair = MidiIn; + fn port_name (&self) -> &Arc { + &self.name + } + fn port (&self) -> &Port { + &self.port + } + fn port_mut (&mut self) -> &mut Port { + &mut self.port + } + fn into_port (self) -> Port { + self.port + } + fn connections (&self) -> &[Connect] { + self.connections.as_slice() + } + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized + { + let port = Self::register(jack, name)?; + let jack = jack.clone(); + let name = name.as_ref().into(); + let connections = connect.to_vec(); + let port = Self { + jack, + port, + name, + connections, + held: Arc::new([false;128].into()), + note_buffer: vec![0;8], + output_buffer: vec![vec![];65536], + }; + port.connect_to_matching()?; + Ok(port) + } +} + +impl MidiOutput { + /// Clear the section of the output buffer that we will be using, + /// emitting "all notes off" at start of buffer if requested. + pub fn buffer_clear (&mut self, scope: &ProcessScope, reset: bool) { + let n_frames = (scope.n_frames() as usize).min(self.output_buffer.len()); + for frame in &mut self.output_buffer[0..n_frames] { + frame.clear(); + } + if reset { + all_notes_off(&mut self.output_buffer); + } + } + /// Write a note to the output buffer + pub fn buffer_write <'a> ( + &'a mut self, + sample: usize, + event: LiveEvent, + ) { + self.note_buffer.fill(0); + event.write(&mut self.note_buffer).expect("failed to serialize MIDI event"); + self.output_buffer[sample].push(self.note_buffer.clone()); + // Update the list of currently held notes. + if let LiveEvent::Midi { ref message, .. } = event { + update_keys(&mut*self.held.write().unwrap(), message); + } + } + /// Write a chunk of MIDI data from the output buffer to the output port. + pub fn buffer_emit (&mut self, scope: &ProcessScope) { + let samples = scope.n_frames() as usize; + let mut writer = self.port.writer(scope); + for (time, events) in self.output_buffer.iter().enumerate().take(samples) { + for bytes in events.iter() { + writer.write(&RawMidi { time: time as u32, bytes }).unwrap_or_else(|_|{ + panic!("Failed to write MIDI data: {bytes:?}"); + }); + } + } + } +} + + +impl>> HasMidiOuts for T { + fn midi_outs (&self) -> &Vec { + self.get() + } + fn midi_outs_mut (&mut self) -> &mut Vec { + self.get_mut() + } +} + + +/// Trait for thing that may output MIDI. +pub trait HasMidiOuts { + fn midi_outs (&self) -> &Vec; + fn midi_outs_mut (&mut self) -> &mut Vec; + fn midi_outs_with_sizes <'a> (&'a self) -> + impl Iterator, &'a [Connect], usize, usize)> + Send + Sync + 'a + { + let mut y = 0; + self.midi_outs().iter().enumerate().map(move|(i, output)|{ + let height = 1 + output.connections().len(); + let data = (i, output.port_name(), output.connections(), y, y + height); + y += height; + data + }) + } + fn midi_outs_emit (&mut self, scope: &ProcessScope) { + for port in self.midi_outs_mut().iter_mut() { + port.buffer_emit(scope) + } + } +} + +/// Trail for thing that may gain new MIDI ports. +impl> AddMidiOut for T { + fn midi_out_add (&mut self) -> Usually<()> { + let index = self.midi_outs().len(); + let port = MidiOutput::new(self.jack(), &format!("{index}/M"), &[])?; + self.midi_outs_mut().push(port); + Ok(()) + } +} + +/// May create new MIDI output ports. +pub trait AddMidiOut { + fn midi_out_add (&mut self) -> Usually<()>; +} + +impl HasJack<'static> for AudioInput { fn jack (&self) -> &Jack<'static> { &self.jack } } + +impl JackPort for AudioInput { + type Port = AudioIn; + type Pair = AudioOut; + fn port_name (&self) -> &Arc { + &self.name + } + fn port (&self) -> &Port { + &self.port + } + fn port_mut (&mut self) -> &mut Port { + &mut self.port + } + fn into_port (self) -> Port { + self.port + } + fn connections (&self) -> &[Connect] { + self.connections.as_slice() + } + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized + { + let port = Self { + port: Self::register(jack, name)?, + jack: jack.clone(), + name: name.as_ref().into(), + connections: connect.to_vec() + }; + port.connect_to_matching()?; + Ok(port) + } +} + +impl HasJack<'static> for AudioOutput { fn jack (&self) -> &Jack<'static> { &self.jack } } + +impl JackPort for AudioOutput { + type Port = AudioOut; + type Pair = AudioIn; + fn port_name (&self) -> &Arc { + &self.name + } + fn port (&self) -> &Port { + &self.port + } + fn port_mut (&mut self) -> &mut Port { + &mut self.port + } + fn into_port (self) -> Port { + self.port + } + fn connections (&self) -> &[Connect] { + self.connections.as_slice() + } + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized + { + let port = Self { + port: Self::register(jack, name)?, + jack: jack.clone(), + name: name.as_ref().into(), + connections: connect.to_vec() + }; + port.connect_to_matching()?; + Ok(port) + } +} diff --git a/device/sampler.rs b/device/sampler.rs new file mode 100644 index 00000000..77aea1b2 --- /dev/null +++ b/device/sampler.rs @@ -0,0 +1,1116 @@ +//! ``` +//! let sample = Sample::new("test", 0, 0, vec![]); +//! ``` + +use crate::*; + +/// The sampler device plays sounds in response to MIDI notes. +#[derive(Debug)] +pub struct Sampler { + /// Name of sampler. + pub name: Arc, + /// Device color. + pub color: ItemTheme, + /// Audio input ports. Samples get recorded here. + #[cfg(feature = "port")] pub audio_ins: Vec, + /// Audio input meters. + #[cfg(feature = "meter")] pub input_meters: Vec, + /// Sample currently being recorded. + pub recording: Option<(usize, Option>>)>, + /// Recording buffer. + pub buffer: Vec>, + /// Samples mapped to MIDI notes. + pub mapped: [Option>>;128], + /// Samples that are not mapped to MIDI notes. + pub unmapped: Vec>>, + /// Sample currently being edited. + pub editing: Option>>, + /// MIDI input port. Triggers sample playback. + #[cfg(feature = "port")] pub midi_in: MidiInput, + /// Collection of currently playing instances of samples. + pub voices: Arc>>, + /// Audio output ports. Voices get played here. + #[cfg(feature = "port")] pub audio_outs: Vec, + /// Audio output meters. + #[cfg(feature = "meter")] pub output_meters: Vec, + /// How to mix the voices. + pub mixing_mode: MixingMode, + /// How to meter the inputs and outputs. + pub metering_mode: MeteringMode, + /// Fixed gain applied to all output. + pub output_gain: f32, + /// Currently active modal, if any. + pub mode: Option, + /// Size of rendered sampler. + pub size: Measure, + /// Lowest note displayed. + pub note_lo: AtomicUsize, + /// Currently selected note. + pub note_pt: AtomicUsize, + /// Selected note as row/col. + pub cursor: (AtomicUsize, AtomicUsize), +} + +impl Sampler { + pub fn new ( + jack: &Jack<'static>, + name: impl AsRef, + #[cfg(feature = "port")] midi_from: &[Connect], + #[cfg(feature = "port")] audio_from: &[&[Connect];2], + #[cfg(feature = "port")] audio_to: &[&[Connect];2], + ) -> Usually { + let name = name.as_ref(); + Ok(Self { + name: name.into(), + #[cfg(feature = "port")] midi_in: MidiInput::new(jack, &format!("M/{name}"), midi_from)?, + #[cfg(feature = "port")] audio_ins: vec![ + AudioInput::new(jack, &format!("L/{name}"), audio_from[0])?, + AudioInput::new(jack, &format!("R/{name}"), audio_from[1])?, + ], + #[cfg(feature = "port")] audio_outs: vec![ + AudioOutput::new(jack, &format!("{name}/L"), audio_to[0])?, + AudioOutput::new(jack, &format!("{name}/R"), audio_to[1])?, + ], + input_meters: vec![0.0;2], + output_meters: vec![0.0;2], + mapped: [const { None };128], + unmapped: vec![], + voices: Arc::new(RwLock::new(vec![])), + buffer: vec![vec![0.0;16384];2], + output_gain: 1., + recording: None, + mode: None, + editing: None, + size: Default::default(), + note_lo: 0.into(), + note_pt: 0.into(), + cursor: (0.into(), 0.into()), + color: Default::default(), + mixing_mode: Default::default(), + metering_mode: Default::default(), + }) + } + /// Value of cursor + pub fn cursor (&self) -> (usize, usize) { + (self.cursor.0.load(Relaxed), self.cursor.1.load(Relaxed)) + } +} + +impl NoteRange for Sampler { + fn note_lo (&self) -> &AtomicUsize { + &self.note_lo + } + fn note_axis (&self) -> &AtomicUsize { + &self.size.y + } +} + +impl NotePoint for Sampler { + fn note_len (&self) -> &AtomicUsize { + unreachable!(); + } + fn get_note_len (&self) -> usize { + 0 + } + fn set_note_len (&self, x: usize) -> usize { + 0 /*TODO?*/ + } + fn note_pos (&self) -> &AtomicUsize { + &self.note_pt + } + fn get_note_pos (&self) -> usize { + self.note_pt.load(Relaxed) + } + fn set_note_pos (&self, x: usize) -> usize { + let old = self.note_pt.swap(x, Relaxed); + self.cursor.0.store(x % 8, Relaxed); + self.cursor.1.store(x / 8, Relaxed); + old + } +} + +/// A sound sample. +#[derive(Default, Debug)] +pub struct Sample { + pub name: Arc, + pub start: usize, + pub end: usize, + pub channels: Vec>, + pub rate: Option, + pub gain: f32, + pub color: ItemTheme, +} + +impl Sample { + pub fn new (name: impl AsRef, start: usize, end: usize, channels: Vec>) -> Self { + Self { + name: name.as_ref().into(), + start, + end, + channels, + rate: None, + gain: 1.0, + color: ItemTheme::random(), + } + } + pub fn play (sample: &Arc>, after: usize, velocity: &u7) -> Voice { + Voice { + sample: sample.clone(), + after, + position: sample.read().unwrap().start, + velocity: velocity.as_int() as f32 / 127.0, + } + } +} + +/// A currently playing instance of a sample. +#[derive(Default, Debug, Clone)] +pub struct Voice { + pub sample: Arc>, + pub after: usize, + pub position: usize, + pub velocity: f32, +} + +#[derive(Debug)] +pub enum SamplerMode { + // Load sample from path + Import(usize, Browse), +} + +impl Sample { + + /// Read WAV from file + pub fn read_data (src: &str) -> Usually<(usize, Vec>)> { + let mut channels: Vec> = vec![]; + for channel in wavers::Wav::from_path(src)?.channels() { + channels.push(channel); + } + let mut end = 0; + let mut data: Vec> = vec![]; + for samples in channels.iter() { + let channel = Vec::from(samples.as_ref()); + end = end.max(channel.len()); + data.push(channel); + } + Ok((end, data)) + } + + pub fn from_file (path: &PathBuf) -> Usually { + let name = path.file_name().unwrap().to_string_lossy().into(); + let mut sample = Self { name, ..Default::default() }; + // Use file extension if present + let mut hint = Hint::new(); + if let Some(ext) = path.extension() { + hint.with_extension(&ext.to_string_lossy()); + } + let probed = symphonia::default::get_probe().format( + &hint, + MediaSourceStream::new( + Box::new(File::open(path)?), + Default::default(), + ), + &Default::default(), + &Default::default() + )?; + let mut format = probed.format; + let params = &format.tracks().iter() + .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) + .expect("no tracks found") + .codec_params; + let mut decoder = get_codecs().make(params, &Default::default())?; + loop { + match format.next_packet() { + Ok(packet) => sample.decode_packet(&mut decoder, packet)?, + Err(symphonia::core::errors::Error::IoError(_)) => break decoder.last_decoded(), + Err(err) => return Err(err.into()), + }; + }; + sample.end = sample.channels.iter().fold(0, |l, c|l + c.len()); + Ok(sample) + } + + fn decode_packet ( + &mut self, decoder: &mut Box, packet: Packet + ) -> Usually<()> { + // Decode a packet + let decoded = decoder + .decode(&packet) + .map_err(|e|Box::::from(e))?; + // Determine sample rate + let spec = *decoded.spec(); + if let Some(rate) = self.rate { + if rate != spec.rate as usize { + panic!("sample rate changed"); + } + } else { + self.rate = Some(spec.rate as usize); + } + // Determine channel count + while self.channels.len() < spec.channels.count() { + self.channels.push(vec![]); + } + // Load sample + let mut samples = SampleBuffer::new( + decoded.frames() as u64, + spec + ); + if samples.capacity() > 0 { + samples.copy_interleaved_ref(decoded); + for frame in samples.samples().chunks(spec.channels.count()) { + for (chan, frame) in frame.iter().enumerate() { + self.channels[chan].push(*frame) + } + } + } + Ok(()) + } + +} + +pub type MidiSample = (Option, Arc>); + +impl Sampler { + + /// Create [Voice]s from [Sample]s in response to MIDI input. + pub fn process_midi_in (&mut self, scope: &ProcessScope) { + let Sampler { midi_in, mapped, voices, .. } = self; + for RawMidi { time, bytes } in midi_in.port().iter(scope) { + if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() { + match message { + MidiMessage::NoteOn { ref key, ref vel } => { + if let Some(ref sample) = mapped[key.as_int() as usize] { + voices.write().unwrap().push(Sample::play(sample, time as usize, vel)); + } + }, + MidiMessage::Controller { controller: _, value: _ } => { + // TODO + } + _ => {} + } + } + } + } + +} + +impl Sample { + pub fn handle_cc (&mut self, controller: u7, value: u7) { + let percentage = value.as_int() as f64 / 127.; + match controller.as_int() { + 20 => { + self.start = (percentage * self.end as f64) as usize; + }, + 21 => { + let length = self.channels[0].len(); + self.end = length.min( + self.start + (percentage * (length as f64 - self.start as f64)) as usize + ); + }, + 22 => { /*attack*/ }, + 23 => { /*decay*/ }, + 24 => { + self.gain = percentage as f32 * 2.0; + }, + 26 => { /* pan */ } + 25 => { /* pitch */ } + _ => {} + } + } +} + +// TODO: +//for port in midi_in.iter() { + //for event in port.iter() { + //match event { + //(time, Ok(LiveEvent::Midi {message, ..})) => match message { + //MidiMessage::NoteOn {ref key, ..} if let Some(editor) = self.editor.as_ref() => { + //editor.set_note_pos(key.as_int() as usize); + //}, + //MidiMessage::Controller {controller, value} if let (Some(editor), Some(sampler)) = ( + //self.editor.as_ref(), + //self.sampler.as_ref(), + //) => { + //// TODO: give sampler its own cursor + //if let Some(sample) = &sampler.mapped[editor.note_pos()] { + //sample.write().unwrap().handle_cc(*controller, *value) + //} + //} + //_ =>{} + //}, + //_ =>{} + //} + //} +//} + +audio!(|self: Sampler, _client, scope|{ + self.process_midi_in(scope); + self.process_audio_out(scope); + self.process_audio_in(scope); + Control::Continue +}); + +impl Sampler { + + pub fn process_audio_in (&mut self, scope: &ProcessScope) { + self.reset_input_meters(); + if self.recording.is_some() { + self.record_into(scope); + } else { + self.update_input_meters(scope); + } + } + + /// Make sure that input meter count corresponds to input channel count + fn reset_input_meters (&mut self) { + let channels = self.audio_ins.len(); + if self.input_meters.len() != channels { + self.input_meters = vec![f32::MIN;channels]; + } + } + + /// Record from inputs to sample + fn record_into (&mut self, scope: &ProcessScope) { + if let Some(ref sample) = self.recording.as_ref().expect("no recording sample").1 { + let mut sample = sample.write().unwrap(); + if sample.channels.len() != self.audio_ins.len() { + panic!("channel count mismatch"); + } + let samples_with_meters = self.audio_ins.iter() + .zip(self.input_meters.iter_mut()) + .zip(sample.channels.iter_mut()); + let mut length = 0; + for ((input, meter), channel) in samples_with_meters { + let slice = input.port().as_slice(scope); + length = length.max(slice.len()); + *meter = to_rms(slice); + channel.extend_from_slice(slice); + } + sample.end += length; + } else { + panic!("tried to record into the void") + } + } + + /// Update input meters + fn update_input_meters (&mut self, scope: &ProcessScope) { + for (input, meter) in self.audio_ins.iter().zip(self.input_meters.iter_mut()) { + let slice = input.port().as_slice(scope); + *meter = to_rms(slice); + } + } + + /// Make sure that output meter count corresponds to input channel count + fn reset_output_meters (&mut self) { + let channels = self.audio_outs.len(); + if self.output_meters.len() != channels { + self.output_meters = vec![f32::MIN;channels]; + } + } + + /// Mix all currently playing samples into the output. + pub fn process_audio_out (&mut self, scope: &ProcessScope) { + self.clear_output_buffer(); + self.populate_output_buffer(scope.n_frames() as usize); + self.write_output_buffer(scope); + } + + /// Zero the output buffer. + fn clear_output_buffer (&mut self) { + for buffer in self.buffer.iter_mut() { + buffer.fill(0.0); + } + } + + /// Write playing voices to output buffer + fn populate_output_buffer (&mut self, frames: usize) { + let Sampler { buffer, voices, output_gain, mixing_mode, .. } = self; + let channel_count = buffer.len(); + match mixing_mode { + MixingMode::Summing => voices.write().unwrap().retain_mut(|voice|{ + mix_summing(buffer.as_mut_slice(), *output_gain, frames, ||voice.next()) + }), + MixingMode::Average => voices.write().unwrap().retain_mut(|voice|{ + mix_average(buffer.as_mut_slice(), *output_gain, frames, ||voice.next()) + }), + } + } + + /// Write output buffer to output ports. + fn write_output_buffer (&mut self, scope: &ProcessScope) { + let Sampler { audio_outs, buffer, .. } = self; + for (i, port) in audio_outs.iter_mut().enumerate() { + let buffer = &buffer[i]; + for (i, value) in port.port_mut().as_mut_slice(scope).iter_mut().enumerate() { + *value = *buffer.get(i).unwrap_or(&0.0); + } + } + } + +} + +impl Iterator for Voice { + type Item = [f32;2]; + fn next (&mut self) -> Option { + if self.after > 0 { + self.after -= 1; + return Some([0.0, 0.0]) + } + let sample = self.sample.read().unwrap(); + if self.position < sample.end { + let position = self.position; + self.position += 1; + return sample.channels[0].get(position).map(|_amplitude|[ + sample.channels[0][position] * self.velocity * sample.gain, + sample.channels[0][position] * self.velocity * sample.gain, + ]) + } + None + } +} + +def_command!(SamplerCommand: |sampler: Sampler| { + RecordToggle { slot: usize } => { + let slot = *slot; + let recording = sampler.recording.as_ref().map(|x|x.0); + let _ = Self::RecordFinish.execute(sampler)?; + // autoslice: continue recording at next slot + if recording != Some(slot) { + Self::RecordBegin { slot }.execute(sampler) + } else { + Ok(None) + } + }, + RecordBegin { slot: usize } => { + let slot = *slot; + sampler.recording = Some(( + slot, + Some(Arc::new(RwLock::new(Sample::new( + "Sample", 0, 0, vec![vec![];sampler.audio_ins.len()] + )))) + )); + Ok(None) + }, + RecordFinish => { + let _prev_sample = sampler.recording.as_mut().map(|(index, sample)|{ + std::mem::swap(sample, &mut sampler.mapped[*index]); + sample + }); // TODO: undo + Ok(None) + }, + RecordCancel => { + sampler.recording = None; + Ok(None) + }, + PlaySample { slot: usize } => { + let slot = *slot; + if let Some(ref sample) = sampler.mapped[slot] { + sampler.voices.write().unwrap().push(Sample::play(sample, 0, &u7::from(128))); + } + Ok(None) + }, + StopSample { slot: usize } => { + let slot = *slot; + todo!(); + Ok(None) + }, +}); + +def_command!(FileBrowserCommand: |sampler: Sampler|{ + //("begin" [] Some(Self::Begin)) + //("cancel" [] Some(Self::Cancel)) + //("confirm" [] Some(Self::Confirm)) + //("select" [i: usize] Some(Self::Select(i.expect("no index")))) + //("chdir" [p: PathBuf] Some(Self::Chdir(p.expect("no path")))) + //("filter" [f: Arc] Some(Self::Filter(f.expect("no filter"))))) +}); + +impl Sampler { + fn sample_selected (&self) -> usize { + (self.get_note_pos() as u8).into() + } + fn sample_selected_pitch (&self) -> u7 { + (self.get_note_pos() as u8).into() + } +} + +pub struct AddSampleModal { + exited: bool, + dir: PathBuf, + subdirs: Vec, + files: Vec, + cursor: usize, + offset: usize, + sample: Arc>, + voices: Arc>>, + _search: Option, +} + +impl AddSampleModal { + fn exited (&self) -> bool { + self.exited + } + fn exit (&mut self) { + self.exited = true + } +} + +impl AddSampleModal { + pub fn new ( + sample: &Arc>, + voices: &Arc>> + ) -> Usually { + let dir = std::env::current_dir()?; + let (subdirs, files) = scan(&dir)?; + Ok(Self { + exited: false, + dir, + subdirs, + files, + cursor: 0, + offset: 0, + sample: sample.clone(), + voices: voices.clone(), + _search: None + }) + } + fn rescan (&mut self) -> Usually<()> { + scan(&self.dir).map(|(subdirs, files)|{ + self.subdirs = subdirs; + self.files = files; + }) + } + fn prev (&mut self) { + self.cursor = self.cursor.saturating_sub(1); + } + fn next (&mut self) { + self.cursor = self.cursor + 1; + } + fn try_preview (&mut self) -> Usually<()> { + if let Some(path) = self.cursor_file() { + if let Ok(sample) = Sample::from_file(&path) { + *self.sample.write().unwrap() = sample; + self.voices.write().unwrap().push( + Sample::play(&self.sample, 0, &u7::from(100u8)) + ); + } + //load_sample(&path)?; + //let src = std::fs::File::open(&path)?; + //let mss = MediaSourceStream::new(Box::new(src), Default::default()); + //let mut hint = Hint::new(); + //if let Some(ext) = path.extension() { + //hint.with_extension(&ext.to_string_lossy()); + //} + //let meta_opts: MetadataOptions = Default::default(); + //let fmt_opts: FormatOptions = Default::default(); + //if let Ok(mut probed) = symphonia::default::get_probe() + //.format(&hint, mss, &fmt_opts, &meta_opts) + //{ + //panic!("{:?}", probed.format.metadata()); + //}; + } + Ok(()) + } + fn cursor_dir (&self) -> Option { + if self.cursor < self.subdirs.len() { + Some(self.dir.join(&self.subdirs[self.cursor])) + } else { + None + } + } + fn cursor_file (&self) -> Option { + if self.cursor < self.subdirs.len() { + return None + } + let index = self.cursor.saturating_sub(self.subdirs.len()); + if index < self.files.len() { + Some(self.dir.join(&self.files[index])) + } else { + None + } + } + fn pick (&mut self) -> Usually { + if self.cursor == 0 { + if let Some(parent) = self.dir.parent() { + self.dir = parent.into(); + self.rescan()?; + self.cursor = 0; + return Ok(false) + } + } + if let Some(dir) = self.cursor_dir() { + self.dir = dir; + self.rescan()?; + self.cursor = 0; + return Ok(false) + } + if let Some(path) = self.cursor_file() { + let (end, channels) = read_sample_data(&path.to_string_lossy())?; + let mut sample = self.sample.write().unwrap(); + sample.name = path.file_name().unwrap().to_string_lossy().into(); + sample.end = end; + sample.channels = channels; + return Ok(true) + } + return Ok(false) + } +} + +fn read_sample_data (_: &str) -> Usually<(usize, Vec>)> { + todo!(); +} + +fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { + let (mut subdirs, mut files) = std::fs::read_dir(dir)? + .fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{ + let entry = entry.expect("failed to read drectory entry"); + let meta = entry.metadata().expect("failed to read entry metadata"); + if meta.is_file() { + files.push(entry.file_name()); + } else if meta.is_dir() { + subdirs.push(entry.file_name()); + } + (subdirs, files) + }); + subdirs.sort(); + files.sort(); + Ok((subdirs, files)) +} + +impl Draw for AddSampleModal { + fn draw (&self, _to: &mut TuiOut) { + todo!() + //let area = to.area(); + //to.make_dim(); + //let area = center_box( + //area, + //64.max(area.w().saturating_sub(8)), + //20.max(area.w().saturating_sub(8)), + //); + //to.fill_fg(area, Color::Reset); + //to.fill_bg(area, Nord::bg_lo(true, true)); + //to.fill_char(area, ' '); + //to.blit(&format!("{}", &self.dir.to_string_lossy()), area.x()+2, area.y()+1, Some(Style::default().bold()))?; + //to.blit(&"Select sample:", area.x()+2, area.y()+2, Some(Style::default().bold()))?; + //for (i, (is_dir, name)) in self.subdirs.iter() + //.map(|path|(true, path)) + //.chain(self.files.iter().map(|path|(false, path))) + //.enumerate() + //.skip(self.offset) + //{ + //if i >= area.h() as usize - 4 { + //break + //} + //let t = if is_dir { "" } else { "" }; + //let line = format!("{t} {}", name.to_string_lossy()); + //let line = &line[..line.len().min(area.w() as usize - 4)]; + //to.blit(&line, area.x() + 2, area.y() + 3 + i as u16, Some(if i == self.cursor { + //Style::default().green() + //} else { + //Style::default().white() + //}))?; + //} + //Lozenge(Style::default()).draw(to) + } +} + +impl Sampler { + + pub fn view_grid (&self) -> impl Content + use<'_> { + //let cells_x = 8u16; + //let cells_y = 8u16; + //let cell_width = 10u16; + //let cell_height = 2u16; + //let width = cells_x * cell_width; + //let height = cells_y * cell_height; + //let cols = Map::east( + //cell_width, + //move||0..cells_x, + //move|x, _|Map::south( + //cell_height, + //move||0..cells_y, + //move|y, _|self.view_grid_cell("........", x, y, cell_width, cell_height) + //) + //); + //cols + //Thunk::new(|to: &mut TuiOut|{ + //}) + "TODO" + } + + pub fn view_grid_cell <'a> ( + &'a self, name: &'a str, x: u16, y: u16, w: u16, h: u16 + ) -> impl Content + use<'a> { + let cursor = self.cursor(); + let hi_fg = Color::Rgb(64, 64, 64); + let hi_bg = if y == 0 { Color::Reset } else { Color::Rgb(64, 64, 64) /*prev*/ }; + let tx_fg = if let Some((index, _)) = self.recording + && index % 8 == x as usize + && index / 8 == y as usize + { + Color::Rgb(255, 64, 0) + } else { + Color::Rgb(255, 255, 255) + }; + let tx_bg = if x as usize == cursor.0 && y as usize == cursor.1 { + Color::Rgb(96, 96, 96) + } else { + Color::Rgb(64, 64, 64) + }; + let lo_fg = Color::Rgb(64, 64, 64); + let lo_bg = if y == 7 { Color::Reset } else { tx_bg }; + Fixed::XY(w, h, Bsp::s( + Fixed::Y(1, Tui::fg_bg(hi_fg, hi_bg, RepeatH(Phat::<()>::LO))), + Bsp::n( + Fixed::Y(1, Tui::fg_bg(lo_fg, lo_bg, RepeatH(Phat::<()>::HI))), + Fill::X(Fixed::Y(1, Tui::fg_bg(tx_fg, tx_bg, name))), + ), + )) + } + + const _EMPTY: &[(f64, f64)] = &[(0., 0.), (1., 1.), (2., 2.), (0., 2.), (2., 0.)]; + + pub fn view_list <'a, T: NotePoint + NoteRange> ( + &'a self, compact: bool, editor: &T + ) -> impl Content + 'a { + let note_lo = editor.get_note_lo(); + let note_pt = editor.get_note_pos(); + let note_hi = editor.get_note_hi(); + Fixed::X(if compact { 4 } else { 12 }, Map::south( + 1, + move||(note_lo..=note_hi).rev(), + move|note, _index| { + //let offset = |a|Push::y(i as u16, Align::n(Fixed::Y(1, Fill::X(a)))); + let mut bg = if note == note_pt { Tui::g(64) } else { Color::Reset }; + let mut fg = Tui::g(160); + if let Some(mapped) = &self.mapped[note] { + let sample = mapped.read().unwrap(); + fg = if note == note_pt { + sample.color.lightest.rgb + } else { + Tui::g(224) + }; + bg = if note == note_pt { + sample.color.light.rgb + } else { + sample.color.base.rgb + }; + } + if let Some((index, _)) = self.recording { + if note == index { + bg = if note == note_pt { Color::Rgb(96,24,0) } else { Color::Rgb(64,16,0) }; + fg = Color::Rgb(224,64,32) + } + } + Tui::fg_bg(fg, bg, format!("{note:3} {}", self.view_list_item(note, compact))) + })) + } + + pub fn view_list_item (&self, note: usize, compact: bool) -> String { + if compact { + String::default() + } else { + draw_list_item(&self.mapped[note]) + } + } + + pub fn view_sample (&self, note_pt: usize) -> impl Content + use<'_> { + Outer(true, Style::default().fg(Tui::g(96))) + .enclose(Fill::XY(draw_viewer(if let Some((_, Some(sample))) = &self.recording { + Some(sample) + } else if let Some(sample) = &self.mapped[note_pt] { + Some(sample) + } else { + None + }))) + } + + pub fn view_sample_info (&self, note_pt: usize) -> impl Content + use<'_> { + Fill::X(Fixed::Y(1, draw_info(if let Some((_, Some(sample))) = &self.recording { + Some(sample) + } else if let Some(sample) = &self.mapped[note_pt] { + Some(sample) + } else { + None + }))) + } + + pub fn view_sample_status (&self, note_pt: usize) -> impl Content + use<'_> { + Fixed::X(20, draw_info_v(if let Some((_, Some(sample))) = &self.recording { + Some(sample) + } else if let Some(sample) = &self.mapped[note_pt] { + Some(sample) + } else { + None + })) + } + + pub fn view_status (&self, index: usize) -> impl Content { + draw_status(self.mapped[index].as_ref()) + } + + pub fn view_meters_input (&self) -> impl Content + use<'_> { + draw_meters(&self.input_meters) + } + + pub fn view_meters_output (&self) -> impl Content + use<'_> { + draw_meters(&self.output_meters) + } +} + +fn draw_meters (meters: &[f32]) -> impl Content + use<'_> { + Tui::bg(Black, Fixed::X(2, Map::east(1, ||meters.iter(), |value, _index|{ + Fill::Y(RmsMeter(*value)) + }))) +} + +fn draw_list_item (sample: &Option>>) -> String { + if let Some(sample) = sample { + let sample = sample.read().unwrap(); + format!("{:8}", sample.name) + //format!("{:8} {:3} {:6}-{:6}/{:6}", + //sample.name, + //sample.gain, + //sample.start, + //sample.end, + //sample.channels[0].len() + //) + } else { + String::from("........") + } +} + +fn draw_viewer (sample: Option<&Arc>>) -> impl Content + use<'_> { + let min_db = -64.0; + Thunk::new(move|to: &mut TuiOut|{ + let [x, y, width, height] = to.area(); + let area = Rect { x, y, width, height }; + if let Some(sample) = &sample { + let sample = sample.read().unwrap(); + let start = sample.start as f64; + let end = sample.end as f64; + let length = end - start; + let step = length / width as f64; + let mut t = start; + let mut lines = vec![]; + while t < end { + let chunk = &sample.channels[0][t as usize..((t + step) as usize).min(sample.end)]; + let total: f32 = chunk.iter().map(|x|x.abs()).sum(); + let count = chunk.len() as f32; + let meter = 10. * (total / count).log10(); + let x = t as f64; + let y = meter as f64; + lines.push(Line::new(x, min_db, x, y, Color::Green)); + t += step / 2.; + } + Canvas::default() + .x_bounds([sample.start as f64, sample.end as f64]) + .y_bounds([min_db, 0.]) + .paint(|ctx| { + for line in lines.iter() { + ctx.draw(line); + } + //FIXME: proportions + //let text = "press record to finish sampling"; + //ctx.print( + //(width - text.len() as u16) as f64 / 2.0, + //height as f64 / 2.0, + //text.red() + //); + }).render(area, &mut to.buffer); + } else { + Canvas::default() + .x_bounds([0.0, width as f64]) + .y_bounds([0.0, height as f64]) + .paint(|_ctx| { + //let text = "press record to begin sampling"; + //ctx.print( + //(width - text.len() as u16) as f64 / 2.0, + //height as f64 / 2.0, + //text.red() + //); + }) + .render(area, &mut to.buffer); + } + }) +} + +fn draw_info (sample: Option<&Arc>>) -> impl Content + use<'_> { + When::new(sample.is_some(), Thunk::new(move|to: &mut TuiOut|{ + let sample = sample.unwrap().read().unwrap(); + let theme = sample.color; + to.place(&row!( + FieldH(theme, "Name", format!("{:<10}", sample.name.clone())), + FieldH(theme, "Length", format!("{:<8}", sample.channels[0].len())), + FieldH(theme, "Start", format!("{:<8}", sample.start)), + FieldH(theme, "End", format!("{:<8}", sample.end)), + FieldH(theme, "Trans", "0"), + FieldH(theme, "Gain", format!("{}", sample.gain)), + )) + })) +} + +fn draw_info_v (sample: Option<&Arc>>) -> impl Content + use<'_> { + Either::new(sample.is_some(), Thunk::new(move|to: &mut TuiOut|{ + let sample = sample.unwrap().read().unwrap(); + let theme = sample.color; + to.place(&Fixed::X(20, col!( + Fill::X(Align::w(FieldH(theme, "Name ", format!("{:<10}", sample.name.clone())))), + Fill::X(Align::w(FieldH(theme, "Length", format!("{:<8}", sample.channels[0].len())))), + Fill::X(Align::w(FieldH(theme, "Start ", format!("{:<8}", sample.start)))), + Fill::X(Align::w(FieldH(theme, "End ", format!("{:<8}", sample.end)))), + Fill::X(Align::w(FieldH(theme, "Trans ", "0"))), + Fill::X(Align::w(FieldH(theme, "Gain ", format!("{}", sample.gain)))), + ))) + }), Thunk::new(|to: &mut TuiOut|to.place(&Tui::fg(Red, col!( + Tui::bold(true, "× No sample."), + "[r] record", + "[Shift-F9] import", + ))))) +} + +fn draw_status (sample: Option<&Arc>>) -> impl Content { + Tui::bold(true, Tui::fg(Tui::g(224), sample + .map(|sample|{ + let sample = sample.read().unwrap(); + format!("Sample {}-{}", sample.start, sample.end) + }) + .unwrap_or_else(||"No sample".to_string()))) +} + +fn draw_sample ( + to: &mut TuiOut, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool +) -> Usually { + let style = if focus { Style::default().green() } else { Style::default() }; + if focus { + to.blit(&"🬴", x+1, y, Some(style.bold())); + } + let label1 = format!("{:3} {:12}", + note.map(|n|n.to_string()).unwrap_or(String::default()), + sample.name); + let label2 = format!("{:>6} {:>6} +0.0", + sample.start, + sample.end); + to.blit(&label1, x+2, y, Some(style.bold())); + to.blit(&label2, x+3+label1.len()as u16, y, Some(style)); + Ok(label1.len() + label2.len() + 4) +} + //fn file_browser_filter (&self) -> Arc { + //todo!() + //} + //fn file_browser_path (&self) -> PathBuf { + //todo!(); + //} + ///// Immutable reference to sample at cursor. + //fn sample_selected (&self) -> Option>> { + //for (i, sample) in self.mapped.iter().enumerate() { + //if i == self.cursor().0 { + //return sample.as_ref() + //} + //} + //for (i, sample) in self.unmapped.iter().enumerate() { + //if i + self.mapped.len() == self.cursor().0 { + //return Some(sample) + //} + //} + //None + //} + //fn sample_gain (&self) -> f32 { + //todo!() + //} + //fn sample_above () -> usize { + //self.note_pos().min(119) + 8 + //} + //fn sample_below () -> usize { + //self.note_pos().max(8) - 8 + //} + //fn sample_to_left () -> usize { + //self.note_pos().min(126) + 1 + //} + //fn sample_to_right () -> usize { + //self.note_pos().max(1) - 1 + //} + //fn selected_pitch () -> u7 { + //(self.note_pos() as u8).into() // TODO + //} + + //select (&self, state: &mut Sampler, i: usize) -> Option { + //Self::Select(state.set_note_pos(i)) + //} + ///// Assign sample to slot + //set (&self, slot: u7, sample: Option>>) -> Option { + //let i = slot.as_int() as usize; + //let old = self.mapped[i].clone(); + //self.mapped[i] = sample; + //Some(Self::Set(old)) + //} + //set_start (&self, state: &mut Sampler, slot: u7, frame: usize) -> Option { + //todo!() + //} + //set_gain (&self, state: &mut Sampler, slot: u7, g: f32) -> Option { + //todo!() + //} + //note_on (&self, state: &mut Sampler, slot: u7, v: u7) -> Option { + //todo!() + //} + //note_off (&self, state: &mut Sampler, slot: u7) -> Option { + //todo!() + //} + //set_sample (&self, state: &mut Sampler, slot: u7, s: Option>>) -> Option { + //Some(Self::SetSample(p, state.set_sample(p, s))) + //} + //import (&self, state: &mut Sampler, c: FileBrowserCommand) -> Option { + //match c { + //FileBrowserCommand::Begin => { + ////let voices = &state.state.voices; + ////let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); + //state.mode = Some(SamplerMode::Import(0, FileBrowser::new(None)?)); + //None + //}, + //_ => { + //println!("\n\rtodo: import: filebrowser: {c:?}"); + //None + //} + //} + //} + ////(Select [i: usize] Some(Self::Select(state.set_note_pos(i)))) + ////(RecordBegin [p: u7] cmd!(state.begin_recording(p.as_int() as usize))) + ////(RecordCancel [] cmd!(state.cancel_recording())) + ////(RecordFinish [] cmd!(state.finish_recording())) + ////(SetStart [p: u7, frame: usize] cmd_todo!("\n\rtodo: {self:?}")) + ////(SetGain [p: u7, gain: f32] cmd_todo!("\n\rtodo: {self:?}")) + ////(NoteOn [p: u7, velocity: u7] cmd_todo!("\n\rtodo: {self:?}")) + ////(NoteOff [p: u7] cmd_todo!("\n\rtodo: {self:?}")) + ////(SetSample [p: u7, s: Option>>] Some(Self::SetSample(p, state.set_sample(p, s)))) + ////(Import [c: FileBrowserCommand] match c { + ////FileBrowserCommand::Begin => { + //////let voices = &state.state.voices; + //////let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); + ////state.mode = Some(SamplerMode::Import(0, FileBrowser::new(None)?)); + ////None + ////}, + ////_ => { + ////println!("\n\rtodo: import: filebrowser: {c:?}"); + ////None + ////} + ////}))); + ////("import" [,..a] + ////FileBrowserCommand::try_from_expr(state, a).map(Self::Import)) + ////("select" [i: usize] + ////Some(Self::Select(i.expect("no index")))) + ////("record/begin" [i: u7] + ////Some(Self::RecordBegin(i.expect("no index")))) + ////("record/cancel" [] + ////Some(Self::RecordCancel)) + ////("record/finish" [] + ////Some(Self::RecordFinish)) + ////("set/sample" [i: u7, s: Option>>] + ////Some(Self::SetSample(i.expect("no index"), s.expect("no sampler")))) + ////("set/start" [i: u7, s: usize] + ////Some(Self::SetStart(i.expect("no index"), s.expect("no start")))) + ////("set/gain" [i: u7, g: f32] + ////Some(Self::SetGain(i.expect("no index"), g.expect("no gain")))) + ////("note/on" [p: u7, v: u7] + ////Some(Self::NoteOn(p.expect("no slot"), v.expect("no velocity")))) + ////("note/off" [p: u7] + ////Some(Self::NoteOff(p.expect("no slot")))))); diff --git a/device/scene.rs b/device/scene.rs new file mode 100644 index 00000000..271daa5d --- /dev/null +++ b/device/scene.rs @@ -0,0 +1,191 @@ +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 new file mode 100644 index 00000000..4868692b --- /dev/null +++ b/device/select.rs @@ -0,0 +1,140 @@ +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 new file mode 100644 index 00000000..46c93cd3 --- /dev/null +++ b/device/sequencer.rs @@ -0,0 +1,542 @@ +//! MIDI sequencer +//! ``` +//! use crate::*; +//! +//! let clip = MidiClip::default(); +//! println!("Empty clip: {clip:?}"); +//! +//! let clip = MidiClip::stop_all(); +//! println!("Panic clip: {clip:?}"); +//! +//! let mut clip = MidiClip::new("clip", true, 1, None, None); +//! clip.set_length(96); +//! clip.toggle_loop(); +//! clip.record_event(12, MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }); +//! assert!(clip.contains_note_on(36.into(), 6, 18)); +//! assert_eq!(&clip.notes, &clip.duplicate().notes); +//! +//! let clip = std::sync::Arc::new(clip); +//! assert_eq!(clip.clone(), clip); +//! +//! let sequencer = Sequencer::default(); +//! println!("{sequencer:?}"); +//! ``` + +use crate::*; + +impl> HasSequencer for T { + fn sequencer (&self) -> &Sequencer { + self.get() + } + fn sequencer_mut (&mut self) -> &mut Sequencer { + self.get_mut() + } +} + +pub trait HasSequencer { + fn sequencer (&self) -> &Sequencer; + fn sequencer_mut (&mut self) -> &mut Sequencer; +} + +/// Contains state for playing a clip +pub struct Sequencer { + /// State of clock and playhead + #[cfg(feature = "clock")] pub clock: Clock, + /// Start time and clip being played + #[cfg(feature = "clip")] pub play_clip: Option<(Moment, Option>>)>, + /// Start time and next clip + #[cfg(feature = "clip")] pub next_clip: Option<(Moment, Option>>)>, + /// Record from MIDI ports to current sequence. + #[cfg(feature = "port")] pub midi_ins: Vec, + /// Play from current sequence to MIDI ports + #[cfg(feature = "port")] pub midi_outs: Vec, + /// Play input through output. + pub monitoring: bool, + /// Write input to sequence. + pub recording: bool, + /// Overdub input to sequence. + pub overdub: bool, + /// Send all notes off + pub reset: bool, // TODO?: after Some(nframes) + /// Notes currently held at input + pub notes_in: Arc>, + /// Notes currently held at output + pub notes_out: Arc>, + /// MIDI output buffer + pub note_buf: Vec, + /// MIDI output buffer + pub midi_buf: Vec>>, +} + +impl Default for Sequencer { + fn default () -> Self { + Self { + #[cfg(feature = "clock")] clock: Clock::default(), + #[cfg(feature = "clip")] play_clip: None, + #[cfg(feature = "clip")] next_clip: None, + #[cfg(feature = "port")] midi_ins: vec![], + #[cfg(feature = "port")] midi_outs: vec![], + + recording: false, + monitoring: true, + overdub: false, + notes_in: RwLock::new([false;128]).into(), + notes_out: RwLock::new([false;128]).into(), + note_buf: vec![0;8], + midi_buf: vec![], + reset: true, + } + } +} + +impl Sequencer { + pub fn new ( + name: impl AsRef, + jack: &Jack<'static>, + #[cfg(feature = "clock")] clock: Option<&Clock>, + #[cfg(feature = "clip")] clip: Option<&Arc>>, + #[cfg(feature = "port")] midi_from: &[Connect], + #[cfg(feature = "port")] midi_to: &[Connect], + ) -> Usually { + let _name = name.as_ref(); + #[cfg(feature = "clock")] let clock = clock.cloned().unwrap_or_default(); + Ok(Self { + reset: true, + notes_in: RwLock::new([false;128]).into(), + notes_out: RwLock::new([false;128]).into(), + #[cfg(feature = "port")] midi_ins: vec![MidiInput::new(jack, &format!("M/{}", name.as_ref()), midi_from)?,], + #[cfg(feature = "port")] midi_outs: vec![MidiOutput::new(jack, &format!("{}/M", name.as_ref()), midi_to)?, ], + #[cfg(feature = "clip")] play_clip: clip.map(|clip|(Moment::zero(&clock.timebase), Some(clip.clone()))), + #[cfg(feature = "clock")] clock, + ..Default::default() + }) + } +} + +impl std::fmt::Debug for Sequencer { + fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("Sequencer") + .field("clock", &self.clock) + .field("play_clip", &self.play_clip) + .field("next_clip", &self.next_clip) + .finish() + } +} + +#[cfg(feature = "clock")] has!(Clock: |self:Sequencer|self.clock); +#[cfg(feature = "port")] has!(Vec: |self:Sequencer|self.midi_ins); +#[cfg(feature = "port")] has!(Vec: |self:Sequencer|self.midi_outs); + +impl MidiMonitor for Sequencer { + fn notes_in (&self) -> &Arc> { + &self.notes_in + } + fn monitoring (&self) -> bool { + self.monitoring + } + fn monitoring_mut (&mut self) -> &mut bool { + &mut self.monitoring + } +} + +impl MidiRecord for Sequencer { + fn recording (&self) -> bool { + self.recording + } + fn recording_mut (&mut self) -> &mut bool { + &mut self.recording + } + fn overdub (&self) -> bool { + self.overdub + } + fn overdub_mut (&mut self) -> &mut bool { + &mut self.overdub + } +} + +#[cfg(feature="clip")] impl HasPlayClip for Sequencer { + fn reset (&self) -> bool { + self.reset + } + fn reset_mut (&mut self) -> &mut bool { + &mut self.reset + } + fn play_clip (&self) -> &Option<(Moment, Option>>)> { + &self.play_clip + } + fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)> { + &mut self.play_clip + } + fn next_clip (&self) -> &Option<(Moment, Option>>)> { + &self.next_clip + } + fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)> { + &mut self.next_clip + } +} + +/// JACK process callback for a sequencer's clip sequencer/recorder. +impl Audio for Sequencer { + fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { + if self.clock().is_rolling() { + self.process_rolling(scope) + } else { + self.process_stopped(scope) + } + } +} + +impl Sequencer { + fn process_rolling (&mut self, scope: &ProcessScope) -> Control { + self.process_clear(scope, false); + // Write chunk of clip to output, handle switchover + if self.process_playback(scope) { + self.process_switchover(scope); + } + // Monitor input to output + self.process_monitoring(scope); + // Record and/or monitor input + self.process_recording(scope); + // Emit contents of MIDI buffers to JACK MIDI output ports. + self.midi_outs_emit(scope); + Control::Continue + } + fn process_stopped (&mut self, scope: &ProcessScope) -> Control { + if self.monitoring() && self.midi_ins().len() > 0 && self.midi_outs().len() > 0 { + self.process_monitoring(scope) + } + Control::Continue + } + fn process_monitoring (&mut self, scope: &ProcessScope) { + let notes_in = self.notes_in().clone(); // For highlighting keys and note repeat + let monitoring = self.monitoring(); + for input in self.midi_ins.iter() { + for (sample, event, bytes) in input.parsed(scope) { + if let LiveEvent::Midi { message, .. } = event { + if monitoring { + self.midi_buf[sample].push(bytes.to_vec()); + } + // FIXME: don't lock on every event! + update_keys(&mut notes_in.write().unwrap(), &message); + } + } + } + } + /// Clear the section of the output buffer that we will be using, + /// emitting "all notes off" at start of buffer if requested. + fn process_clear (&mut self, scope: &ProcessScope, reset: bool) { + let n_frames = (scope.n_frames() as usize).min(self.midi_buf_mut().len()); + for frame in &mut self.midi_buf_mut()[0..n_frames] { + frame.clear(); + } + if reset { + all_notes_off(self.midi_buf_mut()); + } + for port in self.midi_outs_mut().iter_mut() { + // Clear output buffer(s) + port.buffer_clear(scope, false); + } + } + fn process_recording (&mut self, scope: &ProcessScope) { + if self.monitoring() { + self.monitor(scope); + } + if let Some((started, ref clip)) = self.play_clip.clone() { + self.record_clip(scope, started, clip); + } + if let Some((_start_at, _clip)) = &self.next_clip() { + self.record_next(); + } + } + fn process_playback (&mut self, scope: &ProcessScope) -> bool { + // If a clip is playing, write a chunk of MIDI events from it to the output buffer. + // If no clip is playing, prepare for switchover immediately. + if let Some((started, clip)) = &self.play_clip { + // Length of clip, to repeat or stop on end. + let length = clip.as_ref().map_or(0, |p|p.read().unwrap().length); + // Index of first sample to populate. + let offset = self.clock().get_sample_offset(scope, &started); + // Write MIDI events from clip at sample offsets corresponding to pulses. + for (sample, pulse) in self.clock().get_pulses(scope, offset) { + // If a next clip is enqueued, and we're past the end of the current one, + // break the loop here (FIXME count pulse correctly) + let past_end = if clip.is_some() { pulse >= length } else { true }; + // Is it time for switchover? + if self.next_clip().is_some() && past_end { + return true + } + // If there's a currently playing clip, output notes from it to buffer: + if let Some(clip) = clip { + // Source clip from which the MIDI events will be taken. + let clip = clip.read().unwrap(); + // Clip with zero length is not processed + if clip.length > 0 { + // Current pulse index in source clip + let pulse = pulse % clip.length; + // Output each MIDI event from clip at appropriate frames of output buffer: + for message in clip.notes[pulse].iter() { + for port in self.midi_outs.iter_mut() { + port.buffer_write(sample, LiveEvent::Midi { + channel: 0.into(), /* TODO */ + message: *message + }); + } + } + } + } + } + false + } else { + true + } + } + /// Handle switchover from current to next playing clip. + fn process_switchover (&mut self, scope: &ProcessScope) { + let midi_buf = self.midi_buf_mut(); + let sample0 = scope.last_frame_time() as usize; + //let samples = scope.n_frames() as usize; + if let Some((start_at, clip)) = &self.next_clip() { + let start = start_at.sample.get() as usize; + let sample = self.clock().started.read().unwrap() + .as_ref().unwrap().sample.get() as usize; + // If it's time to switch to the next clip: + if start <= sample0.saturating_sub(sample) { + // Samples elapsed since clip was supposed to start + let _skipped = sample0 - start; + // Switch over to enqueued clip + let started = Moment::from_sample(self.clock().timebase(), start as f64); + // Launch enqueued clip + *self.play_clip_mut() = Some((started, clip.clone())); + // Unset enqueuement (TODO: where to implement looping?) + *self.next_clip_mut() = None; + // Fill in remaining ticks of chunk from next clip. + self.process_playback(scope); + } + } + } +} + +pub trait HasMidiBuffers { + fn note_buf_mut (&mut self) -> &mut Vec; + fn midi_buf_mut (&mut self) -> &mut Vec>>; +} + +impl HasMidiBuffers for Sequencer { + fn note_buf_mut (&mut self) -> &mut Vec { + &mut self.note_buf + } + fn midi_buf_mut (&mut self) -> &mut Vec>> { + &mut self.midi_buf + } +} + +pub trait MidiMonitor: HasMidiIns + HasMidiBuffers { + fn notes_in (&self) -> &Arc>; + fn monitoring (&self) -> bool; + fn monitoring_mut (&mut self) -> &mut bool; + fn toggle_monitor (&mut self) { + *self.monitoring_mut() = !self.monitoring(); + } + fn monitor (&mut self, scope: &ProcessScope) { + } +} + +pub trait MidiRecord: MidiMonitor + HasClock + HasPlayClip { + fn recording (&self) -> bool; + fn recording_mut (&mut self) -> &mut bool; + fn toggle_record (&mut self) { + *self.recording_mut() = !self.recording(); + } + + fn overdub (&self) -> bool; + fn overdub_mut (&mut self) -> &mut bool; + fn toggle_overdub (&mut self) { + *self.overdub_mut() = !self.overdub(); + } + + fn record_clip ( + &mut self, + scope: &ProcessScope, + started: Moment, + clip: &Option>>, + ) { + if let Some(clip) = clip { + let sample0 = scope.last_frame_time() as usize; + let start = started.sample.get() as usize; + let _recording = self.recording(); + let timebase = self.clock().timebase().clone(); + let quant = self.clock().quant.get(); + let mut clip = clip.write().unwrap(); + let length = clip.length; + for input in self.midi_ins_mut().iter() { + for (sample, event, _bytes) in parse_midi_input(input.port().iter(scope)) { + if let LiveEvent::Midi { message, .. } = event { + clip.record_event({ + let sample = (sample0 + sample - start) as f64; + let pulse = timebase.samples_to_pulse(sample); + let quantized = (pulse / quant).round() * quant; + quantized as usize % length + }, message); + } + } + } + } + } + + fn record_next (&mut self) { + // TODO switch to next clip and record into it + } +} + +pub trait MidiViewer: HasSize + MidiRange + MidiPoint + Debug + Send + Sync { + fn buffer_size (&self, clip: &MidiClip) -> (usize, usize); + fn redraw (&self); + fn clip (&self) -> &Option>>; + fn clip_mut (&mut self) -> &mut Option>>; + fn set_clip (&mut self, clip: Option<&Arc>>) { + *self.clip_mut() = clip.cloned(); + self.redraw(); + } + /// Make sure cursor is within note range + fn autoscroll (&self) { + let note_pos = self.get_note_pos().min(127); + let note_lo = self.get_note_lo(); + let note_hi = self.get_note_hi(); + if note_pos < note_lo { + self.note_lo().set(note_pos); + } else if note_pos > note_hi { + self.note_lo().set((note_lo + note_pos).saturating_sub(note_hi)); + } + } + /// Make sure time range is within display + fn autozoom (&self) { + if self.time_lock().get() { + let time_len = self.get_time_len(); + let time_axis = self.get_time_axis(); + let time_zoom = self.get_time_zoom(); + loop { + let time_zoom = self.time_zoom().get(); + let time_area = time_axis * time_zoom; + if time_area > time_len { + let next_time_zoom = NoteDuration::prev(time_zoom); + if next_time_zoom <= 1 { + break + } + let next_time_area = time_axis * next_time_zoom; + if next_time_area >= time_len { + self.time_zoom().set(next_time_zoom); + } else { + break + } + } else if time_area < time_len { + let prev_time_zoom = NoteDuration::next(time_zoom); + if prev_time_zoom > 384 { + break + } + let prev_time_area = time_axis * prev_time_zoom; + if prev_time_area <= time_len { + self.time_zoom().set(prev_time_zoom); + } else { + break + } + } + } + if time_zoom != self.time_zoom().get() { + self.redraw() + } + } + //while time_len.div_ceil(time_zoom) > time_axis { + //println!("\r{time_len} {time_zoom} {time_axis}"); + //time_zoom = Note::next(time_zoom); + //} + //self.time_zoom().set(time_zoom); + } +} + +pub trait HasPlayClip: HasClock { + + fn reset (&self) -> bool; + + fn reset_mut (&mut self) -> &mut bool; + + fn play_clip (&self) -> &Option<(Moment, Option>>)>; + + fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)>; + + fn next_clip (&self) -> &Option<(Moment, Option>>)>; + + fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)>; + + fn pulses_since_start (&self) -> Option { + if let Some((started, Some(_))) = self.play_clip().as_ref() { + let elapsed = self.clock().playhead.pulse.get() - started.pulse.get(); + return Some(elapsed) + } + None + } + + fn pulses_since_start_looped (&self) -> Option<(f64, f64)> { + if let Some((started, Some(clip))) = self.play_clip().as_ref() { + let elapsed = self.clock().playhead.pulse.get() - started.pulse.get(); + let length = clip.read().unwrap().length.max(1); // prevent div0 on empty clip + let times = (elapsed as usize / length) as f64; + let elapsed = (elapsed as usize % length) as f64; + return Some((times, elapsed)) + } + None + } + + fn enqueue_next (&mut self, clip: Option<&Arc>>) { + *self.next_clip_mut() = Some((self.clock().next_launch_instant(), clip.cloned())); + *self.reset_mut() = true; + } + + fn play_status (&self) -> impl Content { + let (name, color): (Arc, ItemTheme) = if let Some((_, Some(clip))) = self.play_clip() { + let MidiClip { ref name, color, .. } = *clip.read().unwrap(); + (name.clone(), color) + } else { + ("".into(), Tui::g(64).into()) + }; + let time: String = self.pulses_since_start_looped() + .map(|(times, time)|format!("{:>3}x {:>}", times+1.0, self.clock().timebase.format_beats_1(time))) + .unwrap_or_else(||String::from(" ")).into(); + FieldV(color, "Now:", format!("{} {}", time, name)) + } + + fn next_status (&self) -> impl Content { + let mut time: Arc = String::from("--.-.--").into(); + let mut name: Arc = String::from("").into(); + let mut color = ItemTheme::G[64]; + let clock = self.clock(); + if let Some((t, Some(clip))) = self.next_clip() { + let clip = clip.read().unwrap(); + name = clip.name.clone(); + color = clip.color.clone(); + time = { + let target = t.pulse.get(); + let current = clock.playhead.pulse.get(); + if target > current { + let remaining = target - current; + format!("-{:>}", clock.timebase.format_beats_1(remaining)) + } else { + String::new() + } + }.into() + } else if let Some((t, Some(clip))) = self.play_clip() { + let clip = clip.read().unwrap(); + if clip.looped { + name = clip.name.clone(); + color = clip.color.clone(); + let target = t.pulse.get() + clip.length as f64; + let current = clock.playhead.pulse.get(); + if target > current { + time = format!("-{:>}", clock.timebase.format_beats_0(target - current)).into() + } + } else { + name = "Stop".to_string().into(); + } + }; + FieldV(color, "Next:", format!("{} {}", time, name)) + } +} diff --git a/device/sf2.rs b/device/sf2.rs new file mode 100644 index 00000000..e69de29b diff --git a/device/track.rs b/device/track.rs new file mode 100644 index 00000000..c583381c --- /dev/null +++ b/device/track.rs @@ -0,0 +1,365 @@ +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/src/plugin/vst2_tui.rs b/device/vst2.rs similarity index 100% rename from src/plugin/vst2_tui.rs rename to device/vst2.rs diff --git a/device/vst3.rs b/device/vst3.rs new file mode 100644 index 00000000..e69de29b diff --git a/engine/Cargo.lock b/engine/Cargo.lock deleted file mode 100644 index 5bf6354f..00000000 --- a/engine/Cargo.lock +++ /dev/null @@ -1,722 +0,0 @@ -# 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 b72a07cb..23988f80 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -1,9 +1,22 @@ [package] -name = "tek_engine" -edition = "2021" -version = "0.2.0" +name = "tek_engine" +edition = { workspace = true } +version = { workspace = true } + +[lib] +path = "engine.rs" + +[target.'cfg(target_os = "linux")'] +rustflags = ["-C", "link-arg=-fuse-ld=mold"] [dependencies] -crossterm = "0.28.1" -ratatui = { version = "0.29.0", features = [ "unstable-widget-ref", "underline-color" ] } -better-panic = "0.3.0" +atomic_float = { workspace = true } +jack = { workspace = true } +midly = { workspace = true } +tengri = { workspace = true } + +[dev-dependencies] +proptest = { workspace = true } +proptest-derive = { workspace = true } + +[features] diff --git a/engine/README.md b/engine/README.md deleted file mode 100644 index 03d7e53d..00000000 --- a/engine/README.md +++ /dev/null @@ -1,65 +0,0 @@ -# `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 new file mode 100644 index 00000000..f7beba37 --- /dev/null +++ b/engine/engine.rs @@ -0,0 +1,155 @@ +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 new file mode 100644 index 00000000..1284b0f2 --- /dev/null +++ b/engine/engine_deps.rs @@ -0,0 +1,11 @@ +pub use ::tengri; +pub(crate) use ::{ + tengri::{Usually, from}, + atomic_float::AtomicF64, + std::{ + cmp::Ord, + fmt::Debug, + ops::{Add, Sub, Mul, Div, Rem}, + sync::{Arc, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}}, + }, +}; diff --git a/engine/jack.rs b/engine/jack.rs new file mode 100644 index 00000000..9b611752 --- /dev/null +++ b/engine/jack.rs @@ -0,0 +1,222 @@ +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 new file mode 100644 index 00000000..55835404 --- /dev/null +++ b/engine/midi.rs @@ -0,0 +1,35 @@ +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 new file mode 100644 index 00000000..e7e35888 --- /dev/null +++ b/engine/note.rs @@ -0,0 +1,3 @@ +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 new file mode 100644 index 00000000..06e455ce --- /dev/null +++ b/engine/note/note_pitch.rs @@ -0,0 +1,23 @@ +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 new file mode 100644 index 00000000..310cef87 --- /dev/null +++ b/engine/note/note_point.rs @@ -0,0 +1,79 @@ +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/src/midi/midi_range.rs b/engine/note/note_range.rs similarity index 54% rename from src/midi/midi_range.rs rename to engine/note/note_range.rs index 308a4ae2..40e7acc0 100644 --- a/src/midi/midi_range.rs +++ b/engine/note/note_range.rs @@ -1,4 +1,5 @@ use crate::*; +use std::sync::atomic::Ordering; #[derive(Debug, Clone)] pub struct MidiRangeModel { @@ -28,20 +29,53 @@ from!(|data:(usize, bool)|MidiRangeModel = Self { }); pub trait TimeRange { - fn time_len (&self) -> &AtomicUsize; - fn time_zoom (&self) -> &AtomicUsize; - fn time_lock (&self) -> &AtomicBool; + fn time_len (&self) -> &AtomicUsize; + fn get_time_len (&self) -> usize { + self.time_len().load(Ordering::Relaxed) + } + fn time_zoom (&self) -> &AtomicUsize; + fn get_time_zoom (&self) -> usize { + self.time_zoom().load(Ordering::Relaxed) + } + fn set_time_zoom (&self, value: usize) -> usize { + self.time_zoom().swap(value, Ordering::Relaxed) + } + fn time_lock (&self) -> &AtomicBool; + fn get_time_lock (&self) -> bool { + self.time_lock().load(Ordering::Relaxed) + } + fn set_time_lock (&self, value: bool) -> bool { + self.time_lock().swap(value, Ordering::Relaxed) + } fn time_start (&self) -> &AtomicUsize; - fn time_axis (&self) -> &AtomicUsize; - fn time_end (&self) -> usize { + fn get_time_start (&self) -> usize { + self.time_start().load(Ordering::Relaxed) + } + fn set_time_start (&self, value: usize) -> usize { + self.time_start().swap(value, Ordering::Relaxed) + } + fn time_axis (&self) -> &AtomicUsize; + fn get_time_axis (&self) -> usize { + self.time_axis().load(Ordering::Relaxed) + } + fn get_time_end (&self) -> usize { self.time_start().get() + self.time_axis().get() * self.time_zoom().get() } } pub trait NoteRange { - fn note_lo (&self) -> &AtomicUsize; - fn note_axis (&self) -> &AtomicUsize; - fn note_hi (&self) -> usize { + fn note_lo (&self) -> &AtomicUsize; + fn get_note_lo (&self) -> usize { + self.note_lo().load(Ordering::Relaxed) + } + fn set_note_lo (&self, x: usize) -> usize { + self.note_lo().swap(x, Ordering::Relaxed) + } + fn note_axis (&self) -> &AtomicUsize; + fn get_note_axis (&self) -> usize { + self.note_axis().load(Ordering::Relaxed) + } + fn get_note_hi (&self) -> usize { (self.note_lo().get() + self.note_axis().get().saturating_sub(1)).min(127) } } diff --git a/engine/src/engine.rs b/engine/src/engine.rs deleted file mode 100644 index 886f9670..00000000 --- a/engine/src/engine.rs +++ /dev/null @@ -1,161 +0,0 @@ -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 deleted file mode 100644 index 6a77db37..00000000 --- a/engine/src/input.rs +++ /dev/null @@ -1,69 +0,0 @@ -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 deleted file mode 100644 index 9a89169b..00000000 --- a/engine/src/lib.rs +++ /dev/null @@ -1,85 +0,0 @@ -//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 deleted file mode 100644 index 2433140d..00000000 --- a/engine/src/output.rs +++ /dev/null @@ -1,118 +0,0 @@ -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 deleted file mode 100644 index 7158e9e6..00000000 --- a/engine/src/tui.rs +++ /dev/null @@ -1,164 +0,0 @@ -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 deleted file mode 100644 index a15feeb3..00000000 --- a/engine/src/tui/tui_input.rs +++ /dev/null @@ -1,113 +0,0 @@ -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 deleted file mode 100644 index bb11a0f2..00000000 --- a/engine/src/tui/tui_output.rs +++ /dev/null @@ -1,116 +0,0 @@ -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