mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-19 10:26:41 +01:00
Compare commits
No commits in common. "main" and "0.2.0-rc.3" have entirely different histories.
main
...
0.2.0-rc.3
221 changed files with 11581 additions and 21976 deletions
|
|
@ -1 +0,0 @@
|
|||
*
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
root = true
|
||||
[*]
|
||||
max_line_length = 132
|
||||
1
.envrc
1
.envrc
|
|
@ -1 +0,0 @@
|
|||
use nix
|
||||
6
.forgejo/workflows/build.yaml
Normal file
6
.forgejo/workflows/build.yaml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
on: [push]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: rust
|
||||
stepS:
|
||||
- run: cargo build
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
on:
|
||||
push:
|
||||
tags: '*'
|
||||
jobs:
|
||||
build:
|
||||
runs-on: codeberg-small-lazy
|
||||
container: { image: "alpine:edge" }
|
||||
steps:
|
||||
|
||||
- name: install deps
|
||||
run: apk add --no-cache bash nodejs tree rustup git just cloc build-base clang20-dev pipewire-jack-dev lilv-dev serd-dev
|
||||
|
||||
- run: git clone --depth 1 --recursive $GITHUB_SERVER_URL/$GITHUB_REPOSITORY .
|
||||
- run: whoami && pwd && tree && cloc src/ && cloc .
|
||||
|
||||
- run: rustup-init -y
|
||||
- run: source "$HOME/.cargo/env" && rustup install nightly && rustup default nightly && cargo version -vv
|
||||
#- run: source "$HOME/.cargo/env" && RUSTFLAGS="-Ctarget-feature=-crt-static" just doc
|
||||
- run: source "$HOME/.cargo/env" && RUSTFLAGS="-Ctarget-feature=-crt-static" just test
|
||||
- run: source "$HOME/.cargo/env" && RUSTFLAGS="-Ctarget-feature=-crt-static" just build-release
|
||||
|
||||
- run: tree && mkdir -p .release && mv -v target/release/tek .release
|
||||
|
||||
- name: publish release
|
||||
uses: https://data.forgejo.org/actions/forgejo-release@v2.6.0
|
||||
with:
|
||||
url: "https://codeberg.org"
|
||||
direction: upload
|
||||
tag: "${{ github.ref_name }}"
|
||||
sha: "${{ github.sha }}"
|
||||
release-dir: .release
|
||||
override: true
|
||||
verbose: true
|
||||
#hide-archive-link: true
|
||||
#token: ${{ secrets.TOKEN }}
|
||||
#release-notes-assistant: true
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
on:
|
||||
push:
|
||||
branches: '*'
|
||||
jobs:
|
||||
build:
|
||||
container: { image: "alpine:edge" }
|
||||
steps:
|
||||
|
||||
- name: install deps
|
||||
run: apk add --no-cache nodejs tree rustup git just cloc build-base clang20-dev pipewire-jack-dev lilv-dev serd-dev
|
||||
|
||||
- run: git clone --depth 1 --recursive $GITHUB_SERVER_URL/$GITHUB_REPOSITORY .
|
||||
- run: whoami && pwd && tree && cloc src/ && cloc .
|
||||
|
||||
#- id: cache
|
||||
#name: cache restore
|
||||
#uses: https://data.forgejo.org/actions/cache/restore@v4
|
||||
#with:
|
||||
#key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
#path: |
|
||||
#~/.cargo/bin/
|
||||
#~/.cargo/registry/index/
|
||||
#~/.cargo/registry/cache/
|
||||
#~/.cargo/git/db/
|
||||
#target/
|
||||
|
||||
#- name: cache hit
|
||||
#if: steps.cache.outputs.cache-hit == 'true'
|
||||
#run: echo "cache hit! :)"
|
||||
#- name: cache miss
|
||||
#if: steps.cache.outputs.cache-miss != 'true'
|
||||
#run: echo "cache miss! :("
|
||||
|
||||
- run: cloc src/ && cloc .
|
||||
- run: rustup-init -y
|
||||
- run: source "$HOME/.cargo/env" && rustup install nightly && rustup default nightly && cargo version -vv
|
||||
- run: source "$HOME/.cargo/env" && RUSTFLAGS="-Ctarget-feature=-crt-static" just test
|
||||
- run: tree
|
||||
|
||||
#- name: cache save
|
||||
#uses: https://data.forgejo.org/actions/cache/save@v4
|
||||
#with:
|
||||
#key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
#path: |
|
||||
#~/.cargo/bin/
|
||||
#~/.cargo/registry/index/
|
||||
#~/.cargo/registry/cache/
|
||||
#~/.cargo/git/db/
|
||||
#target/
|
||||
12
.gitignore
vendored
12
.gitignore
vendored
|
|
@ -1,15 +1,5 @@
|
|||
target/*
|
||||
!target/.gitkeep
|
||||
target
|
||||
perf.data*
|
||||
flamegraph*.svg
|
||||
vgcore*
|
||||
example.mid
|
||||
cov
|
||||
*/cov
|
||||
*.profraw
|
||||
build/*
|
||||
!build/README.md
|
||||
!build/*.sh
|
||||
!build/Dockerfile.*
|
||||
.misc
|
||||
.direnv
|
||||
|
|
|
|||
10
.gitmodules
vendored
10
.gitmodules
vendored
|
|
@ -1,10 +0,0 @@
|
|||
[submodule "rust-jack"]
|
||||
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
|
||||
|
|
@ -1,188 +0,0 @@
|
|||
|
||||
|
||||
//pub struct ArrangerVCursor {
|
||||
//cols: Vec<(usize, usize)>,
|
||||
//rows: Vec<(usize, usize)>,
|
||||
//color: ItemPalette,
|
||||
//reticle: Reticle,
|
||||
//selected: ArrangerSelection,
|
||||
//scenes_w: u16,
|
||||
//}
|
||||
|
||||
//pub(crate) const HEADER_H: u16 = 0; // 5
|
||||
//pub(crate) const SCENES_W_OFFSET: u16 = 0;
|
||||
//from!(|args:(&Arranger, usize)|ArrangerVCursor = Self {
|
||||
//cols: Arranger::track_widths(&args.0.tracks),
|
||||
//rows: Arranger::scene_heights(&args.0.scenes, args.1),
|
||||
//selected: args.0.selected(),
|
||||
//scenes_w: SCENES_W_OFFSET + ArrangerScene::longest_name(&args.0.scenes) as u16,
|
||||
//color: args.0.color,
|
||||
//reticle: Reticle(Style {
|
||||
//fg: Some(args.0.color.lighter.rgb),
|
||||
//bg: None,
|
||||
//underline_color: None,
|
||||
//add_modifier: Modifier::empty(),
|
||||
//sub_modifier: Modifier::DIM
|
||||
//}),
|
||||
//});
|
||||
//impl Content<TuiOut> 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<TuiOut> + 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::<usize>() {
|
||||
//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::<usize>() {
|
||||
//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}")
|
||||
//}
|
||||
//}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//keymap!(KEYS_MIDI_EDITOR = |s: MidiEditor, _input: Event| MidiEditCommand {
|
||||
//key(Up) => SetNoteCursor(s.note_pos() + 1),
|
||||
//key(Char('w')) => SetNoteCursor(s.note_pos() + 1),
|
||||
//key(Down) => SetNoteCursor(s.note_pos().saturating_sub(1)),
|
||||
//key(Char('s')) => SetNoteCursor(s.note_pos().saturating_sub(1)),
|
||||
//key(Left) => SetTimeCursor(s.time_pos().saturating_sub(s.note_len())),
|
||||
//key(Char('a')) => SetTimeCursor(s.time_pos().saturating_sub(s.note_len())),
|
||||
//key(Right) => SetTimeCursor((s.time_pos() + s.note_len()) % s.clip_length()),
|
||||
//ctrl(alt(key(Up))) => SetNoteScroll(s.note_pos() + 3),
|
||||
//ctrl(alt(key(Down))) => SetNoteScroll(s.note_pos().saturating_sub(3)),
|
||||
//ctrl(alt(key(Left))) => SetTimeScroll(s.time_pos().saturating_sub(s.time_zoom().get())),
|
||||
//ctrl(alt(key(Right))) => SetTimeScroll((s.time_pos() + s.time_zoom().get()) % s.clip_length()),
|
||||
//ctrl(key(Up)) => SetNoteScroll(s.note_lo().get() + 1),
|
||||
//ctrl(key(Down)) => SetNoteScroll(s.note_lo().get().saturating_sub(1)),
|
||||
//ctrl(key(Left)) => SetTimeScroll(s.time_start().get().saturating_sub(s.note_len())),
|
||||
//ctrl(key(Right)) => SetTimeScroll(s.time_start().get() + s.note_len()),
|
||||
//alt(key(Up)) => SetNoteCursor(s.note_pos() + 3),
|
||||
//alt(key(Down)) => SetNoteCursor(s.note_pos().saturating_sub(3)),
|
||||
//alt(key(Left)) => SetTimeCursor(s.time_pos().saturating_sub(s.time_zoom().get())),
|
||||
//alt(key(Right)) => SetTimeCursor((s.time_pos() + s.time_zoom().get()) % s.clip_length()),
|
||||
//key(Char('d')) => SetTimeCursor((s.time_pos() + s.note_len()) % s.clip_length()),
|
||||
//key(Char('z')) => SetTimeLock(!s.time_lock().get()),
|
||||
//key(Char('-')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::next(s.time_zoom().get()) }),
|
||||
//key(Char('_')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::next(s.time_zoom().get()) }),
|
||||
//key(Char('=')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::prev(s.time_zoom().get()) }),
|
||||
//key(Char('+')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::prev(s.time_zoom().get()) }),
|
||||
////// TODO: kpat!(Char('/')) => // toggle 3plet
|
||||
////// TODO: kpat!(Char('?')) => // toggle dotted
|
||||
//});
|
||||
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
use tek::*;
|
||||
use tengri::input::*;
|
||||
use std::sync::*;
|
||||
struct ExampleClips(Arc<RwLock<Vec<Arc<RwLock<MidiClip>>>>>);
|
||||
impl HasClips for ExampleClips {
|
||||
fn clips (&self) -> RwLockReadGuard<'_, Vec<Arc<RwLock<MidiClip>>>> {
|
||||
self.0.read().unwrap()
|
||||
}
|
||||
fn clips_mut (&self) -> RwLockWriteGuard<'_, Vec<Arc<RwLock<MidiClip>>>> {
|
||||
self.0.write().unwrap()
|
||||
}
|
||||
}
|
||||
fn main () -> Result<(), Box<dyn std::error::Error>> {
|
||||
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(())
|
||||
}
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
//handle!(TuiIn: |self: Sampler, input|SamplerCommand::execute_with_state(self, input.event()));
|
||||
//input_to_command!(SamplerCommand: |state: Sampler, input: Event|match state.mode{
|
||||
//Some(SamplerMode::Import(..)) => Self::Import(
|
||||
//FileBrowserCommand::input_to_command(state, input)?
|
||||
//),
|
||||
//_ => match input {
|
||||
//// load sample
|
||||
//kpat!(Shift-Char('L')) => Self::Import(FileBrowserCommand::Begin),
|
||||
//kpat!(KeyCode::Up) => Self::Select(state.note_pos().overflowing_add(1).0.min(127)),
|
||||
//kpat!(KeyCode::Down) => Self::Select(state.note_pos().overflowing_sub(1).0.min(127)),
|
||||
//_ => return None
|
||||
//}
|
||||
//});
|
||||
//impl Handle<TuiIn> for AddSampleModal {
|
||||
//fn handle (&mut self, from: &TuiIn) -> Perhaps<bool> {
|
||||
//if from.handle_keymap(self, KEYMAP_ADD_SAMPLE)? {
|
||||
//return Ok(Some(true))
|
||||
//}
|
||||
//Ok(Some(true))
|
||||
//}
|
||||
//}
|
||||
//pub const KEYMAP_ADD_SAMPLE: &'static [KeyBinding<AddSampleModal>] = 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
|
||||
//}))))
|
||||
//});
|
||||
375
.old/scratch.rs
375
.old/scratch.rs
|
|
@ -1,375 +0,0 @@
|
|||
//impl Bar for ArrangerStatus {
|
||||
//type State = (ArrangerFocus, ArrangerSelection, bool);
|
||||
//fn hotkey_fg () -> Color where Self: Sized {
|
||||
//TuiTheme::HOTKEY_FG
|
||||
//}
|
||||
//fn update (&mut self, (focused, selected, entered): &Self::State) {
|
||||
//*self = match focused {
|
||||
////ArrangerFocus::Menu => { todo!() },
|
||||
//ArrangerFocus::Transport(_) => ArrangerStatus::Transport,
|
||||
//ArrangerFocus::Arranger => match selected {
|
||||
//ArrangerSelection::Mix => ArrangerStatus::ArrangerMix,
|
||||
//ArrangerSelection::Track(_) => ArrangerStatus::ArrangerTrack,
|
||||
//ArrangerSelection::Scene(_) => ArrangerStatus::ArrangerScene,
|
||||
//ArrangerSelection::Clip(_, _) => ArrangerStatus::ArrangerClip,
|
||||
//},
|
||||
//ArrangerFocus::Phrases => ArrangerStatus::PhrasePool,
|
||||
//ArrangerFocus::PhraseEditor => match entered {
|
||||
//true => ArrangerStatus::PhraseEdit,
|
||||
//false => ArrangerStatus::PhraseView,
|
||||
//},
|
||||
//}
|
||||
//}
|
||||
//}
|
||||
|
||||
//render!(<Tui>|self: ArrangerStatus|{
|
||||
|
||||
//let label = match self {
|
||||
//Self::Transport => "TRANSPORT",
|
||||
//Self::ArrangerMix => "PROJECT",
|
||||
//Self::ArrangerTrack => "TRACK",
|
||||
//Self::ArrangerScene => "SCENE",
|
||||
//Self::ArrangerClip => "CLIP",
|
||||
//Self::PhrasePool => "SEQ LIST",
|
||||
//Self::PhraseView => "VIEW SEQ",
|
||||
//Self::PhraseEdit => "EDIT SEQ",
|
||||
//};
|
||||
|
||||
//let status_bar_bg = TuiTheme::status_bar_bg();
|
||||
|
||||
//let mode_bg = TuiTheme::mode_bg();
|
||||
//let mode_fg = TuiTheme::mode_fg();
|
||||
//let mode = Tui::fg(mode_fg, Tui::bg(mode_bg, Tui::bold(true, format!(" {label} "))));
|
||||
|
||||
//let commands = match self {
|
||||
//Self::ArrangerMix => Self::command(&[
|
||||
//["", "c", "olor"],
|
||||
//["", "<>", "resize"],
|
||||
//["", "+-", "zoom"],
|
||||
//["", "n", "ame/number"],
|
||||
//["", "Enter", " stop all"],
|
||||
//]),
|
||||
//Self::ArrangerClip => Self::command(&[
|
||||
//["", "g", "et"],
|
||||
//["", "s", "et"],
|
||||
//["", "a", "dd"],
|
||||
//["", "i", "ns"],
|
||||
//["", "d", "up"],
|
||||
//["", "e", "dit"],
|
||||
//["", "c", "olor"],
|
||||
//["re", "n", "ame"],
|
||||
//["", ",.", "select"],
|
||||
//["", "Enter", " launch"],
|
||||
//]),
|
||||
//Self::ArrangerTrack => Self::command(&[
|
||||
//["re", "n", "ame"],
|
||||
//["", ",.", "resize"],
|
||||
//["", "<>", "move"],
|
||||
//["", "i", "nput"],
|
||||
//["", "o", "utput"],
|
||||
//["", "m", "ute"],
|
||||
//["", "s", "olo"],
|
||||
//["", "Del", "ete"],
|
||||
//["", "Enter", " stop"],
|
||||
//]),
|
||||
//Self::ArrangerScene => Self::command(&[
|
||||
//["re", "n", "ame"],
|
||||
//["", "Del", "ete"],
|
||||
//["", "Enter", " launch"],
|
||||
//]),
|
||||
//Self::PhrasePool => Self::command(&[
|
||||
//["", "a", "ppend"],
|
||||
//["", "i", "nsert"],
|
||||
//["", "d", "uplicate"],
|
||||
//["", "Del", "ete"],
|
||||
//["", "c", "olor"],
|
||||
//["re", "n", "ame"],
|
||||
//["leng", "t", "h"],
|
||||
//["", ",.", "move"],
|
||||
//["", "+-", "resize view"],
|
||||
//]),
|
||||
//Self::PhraseView => Self::command(&[
|
||||
//["", "enter", " edit"],
|
||||
//["", "arrows/pgup/pgdn", " scroll"],
|
||||
//["", "+=", "zoom"],
|
||||
//]),
|
||||
//Self::PhraseEdit => Self::command(&[
|
||||
//["", "esc", " exit"],
|
||||
//["", "a", "ppend"],
|
||||
//["", "s", "et"],
|
||||
//["", "][", "length"],
|
||||
//["", "+-", "zoom"],
|
||||
//]),
|
||||
//_ => Self::command(&[])
|
||||
//};
|
||||
|
||||
////let commands = commands.iter().reduce(String::new(), |s, (a, b, c)| format!("{s} {a}{b}{c}"));
|
||||
//Tui::bg(status_bar_bg, Fill::w(row!([mode, commands])))
|
||||
|
||||
//});
|
||||
|
||||
///// Status bar for arranger app
|
||||
//#[derive(Copy, Clone, Debug)]
|
||||
//pub enum ArrangerStatus {
|
||||
//Transport,
|
||||
//ArrangerMix,
|
||||
//ArrangerTrack,
|
||||
//ArrangerScene,
|
||||
//ArrangerClip,
|
||||
//PhrasePool,
|
||||
//PhraseView,
|
||||
//PhraseEdit,
|
||||
//}
|
||||
|
||||
//let focused = true;
|
||||
//let _tracks = view.tracks();
|
||||
//lay!(
|
||||
//focused.then_some(Background(TuiTheme::border_bg())),
|
||||
//row!(
|
||||
//// name
|
||||
//Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{
|
||||
//todo!()
|
||||
////let Self(tracks, selected) = self;
|
||||
////let yellow = Some(Style::default().yellow().bold().not_dim());
|
||||
////let white = Some(Style::default().white().bold().not_dim());
|
||||
////let area = to.area();
|
||||
////let area = [area.x(), area.y(), 3 + 5.max(track_name_max_len(tracks)) as u16, area.h()];
|
||||
////let offset = 0; // track scroll offset
|
||||
////for y in 0..area.h() {
|
||||
////if y == 0 {
|
||||
////to.blit(&"Mixer", area.x() + 1, area.y() + y, Some(DIM))?;
|
||||
////} else if y % 2 == 0 {
|
||||
////let index = (y as usize - 2) / 2 + offset;
|
||||
////if let Some(track) = tracks.get(index) {
|
||||
////let selected = selected.track() == Some(index);
|
||||
////let style = if selected { yellow } else { white };
|
||||
////to.blit(&format!(" {index:>02} "), area.x(), area.y() + y, style)?;
|
||||
////to.blit(&*track.name.read().unwrap(), area.x() + 4, area.y() + y, style)?;
|
||||
////}
|
||||
////}
|
||||
////}
|
||||
////Ok(Some(area))
|
||||
//}),
|
||||
//// monitor
|
||||
//Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{
|
||||
//todo!()
|
||||
////let Self(tracks) = self;
|
||||
////let mut area = to.area();
|
||||
////let on = Some(Style::default().not_dim().green().bold());
|
||||
////let off = Some(DIM);
|
||||
////area.x += 1;
|
||||
////for y in 0..area.h() {
|
||||
////if y == 0 {
|
||||
//////" MON ".blit(to.buffer, area.x, area.y + y, style2)?;
|
||||
////} else if y % 2 == 0 {
|
||||
////let index = (y as usize - 2) / 2;
|
||||
////if let Some(track) = tracks.get(index) {
|
||||
////let style = if track.monitoring { on } else { off };
|
||||
////to.blit(&" MON ", area.x(), area.y() + y, style)?;
|
||||
////} else {
|
||||
////area.height = y;
|
||||
////break
|
||||
////}
|
||||
////}
|
||||
////}
|
||||
////area.width = 4;
|
||||
////Ok(Some(area))
|
||||
//}),
|
||||
//// record
|
||||
//Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{
|
||||
//todo!()
|
||||
////let Self(tracks) = self;
|
||||
////let mut area = to.area();
|
||||
////let on = Some(Style::default().not_dim().red().bold());
|
||||
////let off = Some(Style::default().dim());
|
||||
////area.x += 1;
|
||||
////for y in 0..area.h() {
|
||||
////if y == 0 {
|
||||
//////" REC ".blit(to.buffer, area.x, area.y + y, style2)?;
|
||||
////} else if y % 2 == 0 {
|
||||
////let index = (y as usize - 2) / 2;
|
||||
////if let Some(track) = tracks.get(index) {
|
||||
////let style = if track.recording { on } else { off };
|
||||
////to.blit(&" REC ", area.x(), area.y() + y, style)?;
|
||||
////} else {
|
||||
////area.height = y;
|
||||
////break
|
||||
////}
|
||||
////}
|
||||
////}
|
||||
////area.width = 4;
|
||||
////Ok(Some(area))
|
||||
//}),
|
||||
//// overdub
|
||||
//Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{
|
||||
//todo!()
|
||||
////let Self(tracks) = self;
|
||||
////let mut area = to.area();
|
||||
////let on = Some(Style::default().not_dim().yellow().bold());
|
||||
////let off = Some(Style::default().dim());
|
||||
////area.x = area.x + 1;
|
||||
////for y in 0..area.h() {
|
||||
////if y == 0 {
|
||||
//////" OVR ".blit(to.buffer, area.x, area.y + y, style2)?;
|
||||
////} else if y % 2 == 0 {
|
||||
////let index = (y as usize - 2) / 2;
|
||||
////if let Some(track) = tracks.get(index) {
|
||||
////to.blit(&" OVR ", area.x(), area.y() + y, if track.overdub {
|
||||
////on
|
||||
////} else {
|
||||
////off
|
||||
////})?;
|
||||
////} else {
|
||||
////area.height = y;
|
||||
////break
|
||||
////}
|
||||
////}
|
||||
////}
|
||||
////area.width = 4;
|
||||
////Ok(Some(area))
|
||||
//}),
|
||||
//// erase
|
||||
//Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{
|
||||
//todo!()
|
||||
////let Self(tracks) = self;
|
||||
////let mut area = to.area();
|
||||
////let off = Some(Style::default().dim());
|
||||
////area.x = area.x + 1;
|
||||
////for y in 0..area.h() {
|
||||
////if y == 0 {
|
||||
//////" DEL ".blit(to.buffer, area.x, area.y + y, style2)?;
|
||||
////} else if y % 2 == 0 {
|
||||
////let index = (y as usize - 2) / 2;
|
||||
////if let Some(_) = tracks.get(index) {
|
||||
////to.blit(&" DEL ", area.x(), area.y() + y, off)?;
|
||||
////} else {
|
||||
////area.height = y;
|
||||
////break
|
||||
////}
|
||||
////}
|
||||
////}
|
||||
////area.width = 4;
|
||||
////Ok(Some(area))
|
||||
//}),
|
||||
//// gain
|
||||
//Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{
|
||||
//todo!()
|
||||
////let Self(tracks) = self;
|
||||
////let mut area = to.area();
|
||||
////let off = Some(Style::default().dim());
|
||||
////area.x = area.x() + 1;
|
||||
////for y in 0..area.h() {
|
||||
////if y == 0 {
|
||||
//////" GAIN ".blit(to.buffer, area.x, area.y + y, style2)?;
|
||||
////} else if y % 2 == 0 {
|
||||
////let index = (y as usize - 2) / 2;
|
||||
////if let Some(_) = tracks.get(index) {
|
||||
////to.blit(&" +0.0 ", area.x(), area.y() + y, off)?;
|
||||
////} else {
|
||||
////area.height = y;
|
||||
////break
|
||||
////}
|
||||
////}
|
||||
////}
|
||||
////area.width = 7;
|
||||
////Ok(Some(area))
|
||||
//}),
|
||||
//// scenes
|
||||
//Widget::new(|_|{todo!()}, |to: &mut TuiOutput|{
|
||||
//let [x, y, _, height] = to.area();
|
||||
//let mut x2 = 0;
|
||||
//Ok(for (scene_index, scene) in view.scenes().iter().enumerate() {
|
||||
//let active_scene = view.selected.scene() == Some(scene_index);
|
||||
//let sep = Some(if active_scene {
|
||||
//Style::default().yellow().not_dim()
|
||||
//} else {
|
||||
//Style::default().dim()
|
||||
//});
|
||||
//for y in y+1..y+height {
|
||||
//to.blit(&"│", x + x2, y, sep);
|
||||
//}
|
||||
//let name = scene.name.read().unwrap();
|
||||
//let mut x3 = name.len() as u16;
|
||||
//to.blit(&*name, x + x2, y, sep);
|
||||
//for (i, clip) in scene.clips.iter().enumerate() {
|
||||
//let active_track = view.selected.track() == Some(i);
|
||||
//if let Some(clip) = clip {
|
||||
//let y2 = y + 2 + i as u16 * 2;
|
||||
//let label = format!("{}", clip.read().unwrap().name);
|
||||
//to.blit(&label, x + x2, y2, Some(if active_track && active_scene {
|
||||
//Style::default().not_dim().yellow().bold()
|
||||
//} else {
|
||||
//Style::default().not_dim()
|
||||
//}));
|
||||
//x3 = x3.max(label.len() as u16)
|
||||
//}
|
||||
//}
|
||||
//x2 = x2 + x3 + 1;
|
||||
//})
|
||||
//}),
|
||||
//)
|
||||
//)
|
||||
//}
|
||||
|
||||
//impl Command<ArrangerModel> for ArrangerSceneCommand {
|
||||
//}
|
||||
//Edit(phrase) => { state.state.phrase = phrase.clone() },
|
||||
//ToggleViewMode => { state.state.mode.to_next(); },
|
||||
//Delete => { state.state.delete(); },
|
||||
//Activate => { state.state.activate(); },
|
||||
//ZoomIn => { state.state.zoom_in(); },
|
||||
//ZoomOut => { state.state.zoom_out(); },
|
||||
//MoveBack => { state.state.move_back(); },
|
||||
//MoveForward => { state.state.move_forward(); },
|
||||
//RandomColor => { state.state.randomize_color(); },
|
||||
//Put => { state.state.phrase_put(); },
|
||||
//Get => { state.state.phrase_get(); },
|
||||
//AddScene => { state.state.scene_add(None, None)?; },
|
||||
//AddTrack => { state.state.track_add(None, None)?; },
|
||||
//ToggleLoop => { state.state.toggle_loop() },
|
||||
//pub fn zoom_in (&mut self) {
|
||||
//if let ArrangerEditorMode::V(factor) = self.mode {
|
||||
//self.mode = ArrangerEditorMode::V(factor + 1)
|
||||
//}
|
||||
//}
|
||||
//pub fn zoom_out (&mut self) {
|
||||
//if let ArrangerEditorMode::V(factor) = self.mode {
|
||||
//self.mode = ArrangerEditorMode::V(factor.saturating_sub(1))
|
||||
//}
|
||||
//}
|
||||
//pub fn move_back (&mut self) {
|
||||
//match self.selected {
|
||||
//ArrangerEditorFocus::Scene(s) => {
|
||||
//if s > 0 {
|
||||
//self.scenes.swap(s, s - 1);
|
||||
//self.selected = ArrangerEditorFocus::Scene(s - 1);
|
||||
//}
|
||||
//},
|
||||
//ArrangerEditorFocus::Track(t) => {
|
||||
//if t > 0 {
|
||||
//self.tracks.swap(t, t - 1);
|
||||
//self.selected = ArrangerEditorFocus::Track(t - 1);
|
||||
//// FIXME: also swap clip order in scenes
|
||||
//}
|
||||
//},
|
||||
//_ => todo!("arrangement: move forward")
|
||||
//}
|
||||
//}
|
||||
//pub fn move_forward (&mut self) {
|
||||
//match self.selected {
|
||||
//ArrangerEditorFocus::Scene(s) => {
|
||||
//if s < self.scenes.len().saturating_sub(1) {
|
||||
//self.scenes.swap(s, s + 1);
|
||||
//self.selected = ArrangerEditorFocus::Scene(s + 1);
|
||||
//}
|
||||
//},
|
||||
//ArrangerEditorFocus::Track(t) => {
|
||||
//if t < self.tracks.len().saturating_sub(1) {
|
||||
//self.tracks.swap(t, t + 1);
|
||||
//self.selected = ArrangerEditorFocus::Track(t + 1);
|
||||
//// FIXME: also swap clip order in scenes
|
||||
//}
|
||||
//},
|
||||
//_ => todo!("arrangement: move forward")
|
||||
//}
|
||||
//}
|
||||
2113
.old/tek.rs.old
2113
.old/tek.rs.old
File diff suppressed because it is too large
Load diff
|
|
@ -1,83 +0,0 @@
|
|||
This is the unified Tek Arranger.
|
||||
|
||||
Its appearance is defined by the following view definition:
|
||||
|
||||
{def :view (bsp/s (fixed/y 2 :toolbar)
|
||||
(fill/x (align/c (bsp/w (fixed/x :pool-w :pool)
|
||||
(bsp/n (fixed/y 3 :outputs)
|
||||
(bsp/n (fixed/y 3 :inputs)
|
||||
(bsp/n (fixed/y 3 :tracks) :scenes)))))))}
|
||||
|
||||
The arranger's behavior is controlled by the
|
||||
following keymaps:
|
||||
|
||||
{def :keys
|
||||
(@u undo 1)
|
||||
(@shift-u redo 1)
|
||||
(@space clock toggle)
|
||||
(@shift-space clock toggle 0)
|
||||
(@ctrl-a scene add)
|
||||
(@ctrl-t track add)
|
||||
(@tab edit :clip)
|
||||
(@c color)}
|
||||
|
||||
{def :keys-mix
|
||||
(@down select 0 1)
|
||||
(@s select 0 1)
|
||||
|
||||
(@right select 1 0)
|
||||
(@d select 1 0)}
|
||||
|
||||
{def :keys-track
|
||||
(@left select :track-prev :scene)
|
||||
(@a select :track-prev :scene)
|
||||
(@right select :track-next :scene)
|
||||
(@d select :track-next :scene)
|
||||
(@down select :track :scene-next)
|
||||
(@s select :track :scene-next)
|
||||
|
||||
(@q track launch)
|
||||
(@c track color :track)
|
||||
(@comma track swap-prev)
|
||||
(@period track swap-next)
|
||||
(@lt track size-dec)
|
||||
(@gt track size-inc)
|
||||
(@delete track delete)}
|
||||
|
||||
{def :keys-scene
|
||||
(@up select :track :scene-prev)
|
||||
(@w select :track :scene-prev)
|
||||
(@down select :track :scene-next)
|
||||
(@s select :track :scene-next)
|
||||
(@right select :track-next :scene)
|
||||
(@d select :track-next :scene)
|
||||
|
||||
(@q scene launch)
|
||||
(@c scene color :scene)
|
||||
(@comma scene swap-prev)
|
||||
(@period scene swap-next)
|
||||
(@lt scene size-dec)
|
||||
(@gt scene size-inc)
|
||||
(@delete scene delete)}
|
||||
|
||||
{def :keys-clip
|
||||
(@up select :track :scene-prev)
|
||||
(@w select :track :scene-prev)
|
||||
(@down select :track :scene-next)
|
||||
(@s select :track :scene-next)
|
||||
(@left select :track-prev :scene)
|
||||
(@a select :track-prev :scene)
|
||||
(@right select :track-next :scene)
|
||||
(@d select :track-next :scene)
|
||||
|
||||
(@q enqueue :clip)
|
||||
(@c clip color :track :scene)
|
||||
(@g clip get)
|
||||
(@p clip put)
|
||||
(@delete clip del)
|
||||
(@comma clip prev)
|
||||
(@period clip next)
|
||||
(@lt clip swap-prev)
|
||||
(@gt clip swap-next)
|
||||
(@l clip loop-toggle)}
|
||||
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
## development
|
||||
|
||||
you'll need a Rust toolchain and various system libraries.
|
||||
|
||||
you can obtain the former using `rustup` and the latter using `nix-shell`.
|
||||
there's a `shell.nix` provided with the project.
|
||||
|
||||
from there, use the commands in the `Justfile`, e.g.:
|
||||
|
||||
```sh
|
||||
just arranger
|
||||
```
|
||||
|
||||
note that `tek > 0.2.0-rc.7` will require rust nightly
|
||||
for the unstable features `type_alias_impl_trait` and
|
||||
`impl_trait_in_assoc_type`. make some noise for lucky
|
||||
[**rust rfc2515**](https://github.com/rust-lang/rust/issues/63063)
|
||||
if you want to see this buildable with stable/beta.
|
||||
2311
Cargo.lock
generated
2311
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
59
Cargo.toml
59
Cargo.toml
|
|
@ -1,53 +1,10 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
members = [ "./app", "./engine", "./device" ]
|
||||
exclude = [ "./deps/tengri" ]
|
||||
|
||||
[workspace.package]
|
||||
edition = "2024"
|
||||
version = "0.3.0"
|
||||
|
||||
[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"
|
||||
proptest = { version = "^1" }
|
||||
proptest-derive = { version = "^0.5.1" }
|
||||
members = [
|
||||
"crates/tek",
|
||||
#"crates/tek_core",
|
||||
#"crates/tek_api",
|
||||
#"crates/tek_tui",
|
||||
#"crates/tek_cli",
|
||||
#"crates/tek_layout"
|
||||
]
|
||||
|
|
|
|||
127
Justfile
127
Justfile
|
|
@ -1,117 +1,34 @@
|
|||
export RUSTFLAGS := "--cfg procmacro2_semver_exempt -Zmacro-backtrace -Clink-arg=-fuse-ld=mold"
|
||||
export RUST_BACKTRACE := "1"
|
||||
|
||||
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
|
||||
just -l
|
||||
status:
|
||||
cargo c
|
||||
cloc --by-file src/
|
||||
git status
|
||||
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
|
||||
|
||||
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*'"
|
||||
git push --tags -fu codeberg
|
||||
git push --tags -fu origin
|
||||
transport:
|
||||
cargo run --bin tek_transport
|
||||
arranger:
|
||||
{{debug}} {{name}} {{bpm}} arranger
|
||||
arranger-ext:
|
||||
{{debug}} {{name}} {{bpm}} {{midi-in}} {{midi-out}} arranger
|
||||
arranger-release:
|
||||
{{release}} {{name}} {{bpm}} arranger
|
||||
arranger-release-ext:
|
||||
{{release}} {{name}} {{bpm}} {{midi-in}} {{firefox-in}} {{midi-out}} arranger
|
||||
|
||||
groovebox:
|
||||
{{debug}} {{name}} {{bpm}} groovebox
|
||||
groovebox-ext:
|
||||
reset
|
||||
{{debug}} {{name}} {{bpm}} {{midi-in}} {{midi-out}} {{audio-in}} {{audio-out}} groovebox
|
||||
groovebox-browser:
|
||||
{{debug}} {{name}} {{bpm}} {{audio-in}} groovebox
|
||||
groovebox-release:
|
||||
{{release}} {{name}} {{bpm}} groovebox
|
||||
groovebox-release-ext:
|
||||
{{release}} {{name}} {{bpm}} {{midi-in}} {{midi-out}} {{audio-in}} {{audio-out}} groovebox
|
||||
groovebox-release-ext-browser:
|
||||
{{release}} {{name}} {{bpm}} {{midi-in}} {{firefox-in}} {{audio-out}} groovebox
|
||||
|
||||
cargo run --bin tek_arranger
|
||||
sequencer:
|
||||
{{debug}} {{name}} {{bpm}} sequencer
|
||||
sequencer-ext:
|
||||
{{debug}} {{name}} {{bpm}} {{midi-in}} {{midi-out}} sequencer
|
||||
cargo run --bin tek_sequencer
|
||||
sequencer-release:
|
||||
{{release}} {{name}} {{bpm}} sequencer
|
||||
sequencer-release-ext:
|
||||
{{release}} {{name}} {{bpm}} {{midi-in}} {{midi-out}} sequencer
|
||||
|
||||
cargo run --release --bin tek_sequencer
|
||||
mixer:
|
||||
{{debug}} mixer
|
||||
cargo run --bin tek_mixer
|
||||
track:
|
||||
{{debug}} track
|
||||
cargo run --bin tek_track
|
||||
sampler:
|
||||
{{debug}} sampler
|
||||
cargo run --bin tek_sampler
|
||||
plugin:
|
||||
{{debug}} plugin
|
||||
cargo run --bin tek_plugin
|
||||
|
|
|
|||
669
LICENSE
669
LICENSE
|
|
@ -1,661 +1,8 @@
|
|||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
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.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
<https://www.gnu.org/licenses/>.
|
||||
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.
|
||||
|
|
|
|||
182
README.md
182
README.md
|
|
@ -1,90 +1,116 @@
|
|||
# tek [](https://nogithub.codeberg.page)
|
||||
# tek
|
||||
|
||||
a music making program for [24-bit unicode terminals](https://sw.kovidgoyal.net/kitty/).
|
||||
[](https://nogithub.codeberg.page)
|
||||
|
||||
written in [rust](https://www.rust-lang.org/)
|
||||
with [ratatui](https://ratatui.rs/) on [crossterm](https://docs.rs/crossterm/latest/crossterm/)
|
||||
for [jack](https://jackaudio.org/) and [pipewire](https://www.pipewire.org/).
|
||||
a music making program for your terminal
|
||||
|
||||
**tek** is available as [source](https://codeberg.org/unspeaker/tek#building-from-source),
|
||||
[statically linked binaries](https://codeberg.org/unspeaker/tek/releases), and on the
|
||||
[aur](https://codeberg.org/unspeaker/tek#arch-linux).
|
||||
## project status
|
||||
|
||||
author is reachable via [**mastodon** `@unspeaker@mastodon.social`](https://mastodon.social/@unspeaker)
|
||||
or [**matrix** `@unspeaker:matrix.org`](https://matrix.to/#/@unspeaker:matrix.org)
|
||||
for roadmap, see https://codeberg.org/unspeaker/tek/milestones
|
||||
|
||||
| | |
|
||||
|-|-|
|
||||
||<br>|
|
||||
> [!WARNING]
|
||||
>
|
||||
> As of 2024-10-25, I'm on track to release `tek 0.2.0` sometime in December 2024.
|
||||
> I plan to tag the previous working prototype (as seen in the demos published in the
|
||||
> [tek channel at basspistol's peertube](https://v.basspistol.org/c/tek/videos)) as `0.1.0`—
|
||||
> once I've identified the appropriate commit!
|
||||
>
|
||||
> I've been dreaming of this project for a decade, and finally had the experience and peace of mind
|
||||
> to start building it in late May 2024. I quickly reached the limit of how much of the UI I can
|
||||
> write imperatively, so I started refactoring it in a more declarative style. The new interface
|
||||
> logic is holding out pretty well, though it's not presently without its warts.
|
||||
>
|
||||
> Your moral support means a lot to me. Feel free to [contact me on Mastodon](https://mastodon.social/@unspeaker)!
|
||||
> (Especially if you know how to host LV2 plugin UIs in `winit`; or how to relink abandoned Win32
|
||||
> VST2s into LV2 or CLAP monoliths 😁)
|
||||
>
|
||||
> Love,
|
||||
>
|
||||
> (a rogue knowledge worker in a cyberpunk dystopia)
|
||||
|
||||
## usage
|
||||
## what it does
|
||||
|
||||
* **requirements:** linux; jack or pipewire; 24-bit terminal (i use `kitty`)
|
||||
* **recommended:** midi controller; samples in wav format; lv2 plugins.
|
||||
|
||||
## keymaps
|
||||
|
||||
* 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
|
||||
|
||||
[](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
|
||||
```
|
||||
|
||||
### building from source
|
||||
|
||||
requires docker.
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
Tek is a [MIDI](https://en.wikipedia.org/wiki/MIDI) sequencer, sampler, and plugin host
|
||||
for the Linux terminal. It's written in [Rust](https://www.rust-lang.org/), and targets
|
||||
[JACK](https://jackaudio.org/) (or [Pipewire](https://www.pipewire.org/)'s JACK implementation).
|
||||
|
||||
## design goals
|
||||
|
||||
* inspired by trackers and hardware sequencers,
|
||||
but with the critical feature that 90s samplers lack:
|
||||
able to **resample, i.e. record while playing!**
|
||||
### lightweight
|
||||
|
||||
* **pop-up scratchpad for musical ideas.**
|
||||
low resource consumption, can stay open in background.
|
||||
but flexible enough to allow expanding on compositions
|
||||
My goal is to have a pop-up scratchpad for musical ideas that doesn't get in the way
|
||||
of building upon them. Kind of like [Ableton](https://www.ableton.com/) — but for free systems,
|
||||
and without all the bloat!
|
||||
|
||||
* **human- and machine- readable project format**
|
||||
simple representation for project data
|
||||
enable scripting and remapping.
|
||||
### flexible
|
||||
|
||||
Besides Ableton, I'm also inspired by the workflow of trackers and various old-school hardware
|
||||
sequencers (of which I've broken several). I've found that every existing music-making tool
|
||||
takes me about 80% of the way to the music I want to make. And so, after a decade of fucking
|
||||
around, I've decided it's finally time to make good on my old dream to build the instrument
|
||||
that will take me 100% there.
|
||||
|
||||
### programmable
|
||||
|
||||
A secondary goal is to make my music making environment extensible, programmable, and
|
||||
interoperable; the intended project format is an
|
||||
[S-expression](https://en.wikipedia.org/wiki/S-expression)-based notation
|
||||
([EDN](https://en.wikipedia.org/wiki/Clojure#Extensible_Data_Notation),
|
||||
[Steel](https://github.com/mattwparas/steel), or similar... though I've also been
|
||||
looking for an excuse to embed a
|
||||
[Forth](https://en.wikipedia.org/wiki/Forth_(programming_language)) 😏)
|
||||
|
||||
## getting started
|
||||
|
||||
### requirements
|
||||
|
||||
* Linux
|
||||
* JACK or Pipewire
|
||||
* a terminal supporting 24-bit colors (I use `kitty`)
|
||||
|
||||
### recommended
|
||||
|
||||
* MIDI controller
|
||||
* Samples
|
||||
* LV2 plugins
|
||||
|
||||
### downloads
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> Binaries are currently unavailable. Right now your only option is to build from source.
|
||||
> In the future I plan to integrate Forgejo Actions / Codeberg CI.
|
||||
|
||||
### building from source
|
||||
|
||||
You need a Rust toolchain and various system libraries. You can obtain the former
|
||||
using `rustup` and the latter using `nix-shell`. From there, use the commands in the
|
||||
`Justfile`, e.g.:
|
||||
|
||||
```sh
|
||||
just arranger
|
||||
```
|
||||
|
||||
## usage
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> The following applies to `tek 0.1.0`. I will update it as part of the `0.2.0` release.
|
||||
|
||||
### Overview
|
||||
|
||||
Tek is inspired by "clip launching" workflows as exemplified by Ableton Live, Bitwig Studio,
|
||||
Ardour, and probably others. The main view consists of three sections:
|
||||
|
||||
* The **arranger view** corresponds to Ableton's Session and Arrangement views.
|
||||
It allows you to put together a musical composition as a sequence of **phrases**,
|
||||
playing simultaneously across multiple **tracks**.
|
||||
* The **sequencer view** allows you to edit phrases, which consist of MIDI events.
|
||||
* The **chain view** allows you to add **devices** to each track. Devices determine
|
||||
how a given phrase will sound. Currently, there are two devices implemented:
|
||||
**sampler** and **plugin**.
|
||||
|
||||
> [!NOTE]
|
||||
> Use `Tab` to switch focus between views. Use `Enter` to exclusively focus the highlighted view,
|
||||
> and `Esc` to unfocus it. When a view is focused, use the `Arrow Keys` and `Enter` to navigate.
|
||||
> Use `;` (semicolon) to open the command palette, which will list the remaining keybindings.
|
||||
|
|
|
|||
|
|
@ -1,58 +0,0 @@
|
|||
[package]
|
||||
name = "tek"
|
||||
edition = { workspace = true }
|
||||
version = { workspace = true }
|
||||
|
||||
[lib]
|
||||
path = "tek.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "tek"
|
||||
path = "tek_cli.rs"
|
||||
|
||||
[target.'cfg(target_os = "linux")']
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
|
||||
[dependencies]
|
||||
tek_device = { path = "../device" }
|
||||
|
||||
atomic_float = { workspace = true }
|
||||
backtrace = { workspace = true }
|
||||
clap = { workspace = true, optional = true }
|
||||
jack = { workspace = true }
|
||||
konst = { workspace = true }
|
||||
livi = { workspace = true, optional = true }
|
||||
midly = { workspace = true }
|
||||
palette = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
symphonia = { workspace = true, optional = true }
|
||||
tengri = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
uuid = { workspace = true, optional = true }
|
||||
wavers = { workspace = true, optional = true }
|
||||
winit = { workspace = true, optional = true }
|
||||
xdg = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
proptest = { workspace = true }
|
||||
proptest-derive = { workspace = true }
|
||||
|
||||
[features]
|
||||
arranger = ["port", "editor", "sequencer", "editor"]
|
||||
browse = []
|
||||
clap = []
|
||||
cli = ["dep:clap"]
|
||||
clock = []
|
||||
default = ["cli", "arranger", "sampler", "lv2"]
|
||||
editor = []
|
||||
host = ["lv2"]
|
||||
lv2 = ["port", "livi", "winit"]
|
||||
meter = []
|
||||
mixer = []
|
||||
pool = []
|
||||
port = []
|
||||
sampler = ["port", "meter", "mixer", "browse", "symphonia", "wavers"]
|
||||
sequencer = ["port", "clock", "uuid", "pool"]
|
||||
sf2 = []
|
||||
vst2 = []
|
||||
vst3 = []
|
||||
224
app/tek.edn
224
app/tek.edn
|
|
@ -1,224 +0,0 @@
|
|||
(keys :axis/x
|
||||
(@left x/dec)
|
||||
(@right x/inc))
|
||||
(keys :axis/x2
|
||||
(@shift/left x2/dec)
|
||||
(@shift/right x2/inc))
|
||||
(keys :axis/y
|
||||
(@up y/dec)
|
||||
(@down y/inc))
|
||||
(keys :axis/y2
|
||||
(@shift/up y2/dec)
|
||||
(@shift/down y2/inc))
|
||||
(keys :axis/z
|
||||
(@minus z/dec)
|
||||
(@equal z/inc))
|
||||
(keys :axis/z2
|
||||
(@underscore z2/dec)
|
||||
(@plus z2/inc))
|
||||
(keys :axis/i
|
||||
(@comma i/dec)
|
||||
(@period z/inc))
|
||||
(keys :axis/i2
|
||||
(@lt i2/dec)
|
||||
(@gt z2/inc))
|
||||
(keys :axis/w
|
||||
(@openbracket w/dec)
|
||||
(@closebracket w/inc))
|
||||
(keys :axis/w2
|
||||
(@openbrace w2/dec)
|
||||
(@closebrace w2/inc))
|
||||
|
||||
(mode :menu (keys :axis/y :confirm) :menu)
|
||||
|
||||
(keys :confirm
|
||||
(@enter confirm))
|
||||
|
||||
(view :menu (bg (g 0) (bsp/s
|
||||
:ports/out
|
||||
(bsp/n :ports/in (bg (g 30) (bsp/s (fixed/y 7 :logo) (fill :dialog/menu)))))))
|
||||
|
||||
(view :menu (bsp/s
|
||||
(push/y 4 (fixed/xy 20 2 (bg (g 0) :debug)))
|
||||
(fixed 20 2 (bg (g 20) (push/x 2 :debug)))))
|
||||
|
||||
(view :menu (bsp/s (fixed/y 4 :debug) :debug))
|
||||
|
||||
(view :ports/out (fill/x (fixed/y 3 (bsp/a
|
||||
(fill/x (align/w (text L-AUDIO-OUT)))
|
||||
(bsp/a (text MIDI-OUT) (fill/x (align/e (text AUDIO-OUT-R))))))))
|
||||
|
||||
(view :ports/in (fill/x (fixed/y 3 (bsp/a
|
||||
(fill/x (align/w (text L-AUDIO-IN)))
|
||||
(bsp/a (text MIDI-IN) (fill/x (align/e (text AUDIO-IN-R))))))))
|
||||
|
||||
(view :browse (bsp/s
|
||||
(padding 3 1 :browse-title)
|
||||
(enclose (fg (g 96)) browser)))
|
||||
|
||||
(keys :help
|
||||
(@f1 dialog :help))
|
||||
|
||||
(keys :back
|
||||
(@escape back))
|
||||
|
||||
(keys :page
|
||||
(@pgup page/up)
|
||||
(@pgdn page/down))
|
||||
|
||||
(keys :delete
|
||||
(@delete delete)
|
||||
(@backspace delete/back))
|
||||
|
||||
(keys :input (see :axis/x :delete)
|
||||
(:char input))
|
||||
|
||||
(keys :list (see :axis/y :confirm))
|
||||
|
||||
(keys :length (see :axis/x :axis/y :confirm))
|
||||
|
||||
(keys :browse (see :list :input :focus))
|
||||
|
||||
(keys :history
|
||||
(@u undo 1)
|
||||
(@r redo 1))
|
||||
|
||||
(keys :clock
|
||||
(@space clock/toggle 0)
|
||||
(@shift/space clock/toggle 0))
|
||||
|
||||
(keys :color
|
||||
(@c color))
|
||||
|
||||
(keys :launch
|
||||
(@q launch))
|
||||
|
||||
(keys :saveload
|
||||
(@f6 dialog :save)
|
||||
(@f9 dialog :load))
|
||||
|
||||
(keys :global (see :history :saveload)
|
||||
(@f8 dialog :options)
|
||||
(@f10 dialog :quit))
|
||||
|
||||
(keys :focus)
|
||||
|
||||
(mode :transport (name Transport) (info A JACK transport controller.) (keys :clock :global)
|
||||
(view :transport))
|
||||
|
||||
(mode :arranger (name Arranger) (info A grid of launchable clips arranged by track and scene.)
|
||||
(mode :editor (keys :editor)) (mode :dialog (keys :dialog)) (mode :message (keys :message))
|
||||
(mode :add-device (keys :add-device)) (mode :browse (keys :browse)) (mode :rename (keys :input))
|
||||
(mode :length (keys :rename)) (mode :clip (keys :clip)) (mode :track (keys :track))
|
||||
(mode :scene (keys :scene)) (mode :mix (keys :mix))
|
||||
(keys :clock :arranger :global) :arranger)
|
||||
|
||||
(view :arranger (bsp/n
|
||||
:status
|
||||
(bsp/w :meters/output (bsp/e :meters/input :arrangement))))
|
||||
|
||||
(view :arrangement (bsp/n
|
||||
:tracks/inputs
|
||||
(bsp/s :tracks/outputs (bsp/s :tracks/names (bsp/s :tracks/devices
|
||||
(fill (either :mode/editor (bsp/e :scenes/names :editor) :scenes)))))))
|
||||
|
||||
(keys :arranger (see :color :launch :scenes :tracks)
|
||||
(@tab project/edit) (@enter project/edit)
|
||||
(@shift/I project/input/add) (@shift/O project/output/add)
|
||||
(@shift/S project/scene/add) (@shift/T project/track/add)
|
||||
(@shift/D dialog/show :dialog/device))
|
||||
|
||||
(keys :tracks
|
||||
(@t select :select/track)
|
||||
(@left select :select/track/dec)
|
||||
(@right select :select/track/inc))
|
||||
|
||||
(keys :scenes
|
||||
(@s select :select/scene)
|
||||
(@up select :select/scene/dec)
|
||||
(@down select :select/scene/inc))
|
||||
|
||||
(keys :track (see :color :launch :axis/z :axis/z2 :delete)
|
||||
(@r toggle :rec)
|
||||
(@m toggle :mon)
|
||||
(@p toggle :play)
|
||||
(@P toggle :solo))
|
||||
(keys :scene (see :color :launch :axis/z :axis/z2 :delete))
|
||||
|
||||
(keys :clip (see :color :launch :axis/z :axis/z2 :delete)
|
||||
(@l toggle :loop))
|
||||
|
||||
(mode :groovebox (name Groovebox) (info A sequencer with built-in sampler.)
|
||||
(mode browse (keys :browse))
|
||||
(mode rename (keys :pool-rename))
|
||||
(mode length (keys :pool-length))
|
||||
(keys :clock :editor :sampler :global) (view :groovebox))
|
||||
|
||||
(view :groovebox (bsp/w
|
||||
:meters/output
|
||||
(bsp/e :meters/input (bsp/w :groove/meta :groove/editor))))
|
||||
|
||||
(view :groove/meta (fill/y (align/n (stack/s
|
||||
:midi-ins/status :midi-outs/status :audio-ins/status :audio-outs/status :pool))))
|
||||
|
||||
(view :groove/editor (bsp/n
|
||||
:groove/sample
|
||||
:groove/sequence))
|
||||
|
||||
(view :groove/sample (fixed/y :h-sample-detail (bsp/e
|
||||
(fill/y (fixed/x 20 (align/nw :sample-status)))
|
||||
:sample-viewer)))
|
||||
|
||||
(view :groove/sequence (bsp/e
|
||||
(fill/y (align/n (bsp/s :status/v :editor-status)))
|
||||
(bsp/e :samples/keys :editor)))
|
||||
|
||||
(mode :sampler (name Sampler) (info A sampling soundboard.)
|
||||
(keys :sampler :global) (view :sampler))
|
||||
|
||||
(view :sampler (bsp/s
|
||||
(fixed/y 1 :transport)
|
||||
(bsp/n (fixed/y 1 :status) (fill :samples/grid))))
|
||||
|
||||
(keys :sampler (see :sampler/directions :sampler/record :sampler/play))
|
||||
|
||||
(keys :sampler/record
|
||||
(@r sampler/record/toggle :sample/selected) (@shift/R sampler/record/back))
|
||||
|
||||
(keys :sampler/play
|
||||
(@p sampler/play/sample :sample/selected) (@P sampler/stop/sample :sample/selected))
|
||||
|
||||
(keys :sampler/import-export
|
||||
(@shift/f6 dialog :dialog/export/sample) (@shift/f9 dialog :dialog/import/sample))
|
||||
|
||||
(keys :sampler/directions
|
||||
(@up sampler/select :sample/above)
|
||||
(@down sampler/select :sample/below)
|
||||
(@left sampler/select :sample/to/left)
|
||||
(@right sampler/select :sample/to/right))
|
||||
|
||||
(mode :sequencer (name Sequencer) (info A MIDI sequencer.)
|
||||
(mode browse (keys :browse)) (mode rename (keys :pool/rename)) (mode length (keys :pool/length))
|
||||
(keys :editor :clock :global) (view :sequencer))
|
||||
|
||||
(view :sequencer (bsp/s
|
||||
(fixed/y 1 :transport)
|
||||
(bsp/n (fixed/y 1 :status) (fill (bsp/a (fill/xy (align/e :pool)) :editor)))))
|
||||
|
||||
(keys :editor (see :editor/view :editor/note))
|
||||
|
||||
(keys :editor/view (see :axis/x :axis/x2 :axis/z :axis/z2)
|
||||
(@z toggle :lock))
|
||||
|
||||
(keys :editor/note (see :axis/i :axis/i2 :axis/y :page)
|
||||
(@a editor/append :true)
|
||||
(@enter editor/append :false)
|
||||
(@del editor/delete/note)
|
||||
(@shift/del editor/delete/note))
|
||||
|
||||
(keys :pool (see :axis-y :axis-w :axis/z2 :color :delete)
|
||||
(@n rename/begin) (@t length/begin) (@m import/begin) (@x export/begin)
|
||||
(@shift/A clip/add :after :new/clip) (@shift/D clip/add :after :cloned/clip))
|
||||
|
||||
(keys :sequencer (see :color :launch)
|
||||
(@shift/I input/add) (@shift/O output/add))
|
||||
468
app/tek.rs
468
app/tek.rs
|
|
@ -1,468 +0,0 @@
|
|||
#![feature(
|
||||
adt_const_params,
|
||||
associated_type_defaults,
|
||||
closure_lifetime_binder,
|
||||
if_let_guard,
|
||||
impl_trait_in_assoc_type,
|
||||
trait_alias,
|
||||
type_alias_impl_trait,
|
||||
type_changing_struct_update,
|
||||
)]
|
||||
|
||||
#![allow(
|
||||
clippy::unit_arg
|
||||
)]
|
||||
|
||||
#[cfg(test)] mod tek_test;
|
||||
|
||||
mod tek_bind; pub use self::tek_bind::*;
|
||||
mod tek_cfg; pub use self::tek_cfg::*;
|
||||
mod tek_deps; pub use self::tek_deps::*;
|
||||
mod tek_mode; pub use self::tek_mode::*;
|
||||
mod tek_view; pub use self::tek_view::*;
|
||||
|
||||
/// Total state
|
||||
#[derive(Default, Debug)]
|
||||
pub struct App {
|
||||
/// Base color.
|
||||
pub color: ItemTheme,
|
||||
/// Must not be dropped for the duration of the process
|
||||
pub jack: Jack<'static>,
|
||||
/// Display size
|
||||
pub size: Measure<TuiOut>,
|
||||
/// Performance counter
|
||||
pub perf: PerfModel,
|
||||
/// Available view modes and input bindings
|
||||
pub config: Config,
|
||||
/// Currently selected mode
|
||||
pub mode: Arc<Mode<Arc<str>>>,
|
||||
/// Undo history
|
||||
pub history: Vec<(AppCommand, Option<AppCommand>)>,
|
||||
/// 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<u7> = 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<str> {
|
||||
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<u7> {
|
||||
word = |app| {
|
||||
":editor/pitch" => Some(
|
||||
(app.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into()
|
||||
)
|
||||
};
|
||||
});
|
||||
|
||||
dsl_ns!(App: Option<usize> {
|
||||
word = |app| {
|
||||
":selected/scene" => app.selection().scene(),
|
||||
":selected/track" => app.selection().track(),
|
||||
};
|
||||
});
|
||||
|
||||
dsl_ns!(App: Option<Arc<RwLock<MidiClip>>> {
|
||||
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<MidiEditor>: |self: App|self.project.editor);
|
||||
has!(Selection: |self: App|self.project.selection);
|
||||
has!(Vec<MidiInput>: |self: App|self.project.midi_ins);
|
||||
has!(Vec<MidiOutput>: |self: App|self.project.midi_outs);
|
||||
has!(Vec<Scene>: |self: App|self.project.scenes);
|
||||
has!(Vec<Track>: |self: App|self.project.tracks);
|
||||
has!(Measure<TuiOut>: |self: App|self.size);
|
||||
has_clips!( |self: App|self.pool.clips);
|
||||
maybe_has!(Track: |self: App| { MaybeHas::<Track>::get(&self.project) };
|
||||
{ MaybeHas::<Track>::get_mut(&mut self.project) });
|
||||
maybe_has!(Scene: |self: App| { MaybeHas::<Scene>::get(&self.project) };
|
||||
{ MaybeHas::<Scene>::get_mut(&mut self.project) });
|
||||
|
||||
impl HasClipsSize for App {
|
||||
fn clips_size (&self) -> &Measure<TuiOut> { &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<bool>) {
|
||||
//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<str>),
|
||||
Browse(BrowseTarget, Arc<Browse>),
|
||||
Options,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct MenuItems(pub Arc<[MenuItem]>);
|
||||
|
||||
impl AsRef<Arc<[MenuItem]>> for MenuItems {
|
||||
fn as_ref (&self) -> &Arc<[MenuItem]> {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MenuItem(
|
||||
/// Label
|
||||
pub Arc<str>,
|
||||
/// Callback
|
||||
pub Arc<Box<dyn Fn(&mut App)->Usually<()> + 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<usize> {
|
||||
if let Self::Menu(selected, _) = self { Some(*selected) } else { None }
|
||||
}
|
||||
pub fn device_kind (&self) -> Option<usize> {
|
||||
if let Self::Device(index) = self { Some(*index) } else { None }
|
||||
}
|
||||
pub fn device_kind_next (&self) -> Option<usize> {
|
||||
self.device_kind().map(|index|(index + 1) % device_kinds().len())
|
||||
}
|
||||
pub fn device_kind_prev (&self) -> Option<usize> {
|
||||
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<Browse>> {
|
||||
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();
|
||||
//});
|
||||
325
app/tek_bind.rs
325
app/tek_bind.rs
|
|
@ -1,325 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
pub type Binds = Arc<RwLock<BTreeMap<Arc<str>, EventMap<TuiEvent, Arc<str>>>>>;
|
||||
|
||||
/// A collection of input bindings.
|
||||
#[derive(Debug)]
|
||||
pub struct EventMap<E, C>(
|
||||
/// Map of each event (e.g. key combination) to
|
||||
/// all command expressions bound to it by
|
||||
/// all loaded input layers.
|
||||
pub BTreeMap<E, Vec<Binding<C>>>
|
||||
);
|
||||
|
||||
/// An input binding.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Binding<C> {
|
||||
pub commands: Arc<[C]>,
|
||||
pub condition: Option<Condition>,
|
||||
pub description: Option<Arc<str>>,
|
||||
pub source: Option<Arc<PathBuf>>,
|
||||
}
|
||||
|
||||
/// Input bindings are only returned if this evaluates to true
|
||||
#[derive(Clone)]
|
||||
pub struct Condition(Arc<Box<dyn Fn()->bool + Send + Sync>>);
|
||||
|
||||
/// Default is always empty map regardless if `E` and `C` implement [Default].
|
||||
impl<E, C> Default for EventMap<E, C> {
|
||||
fn default () -> Self { Self(Default::default()) }
|
||||
}
|
||||
|
||||
impl<E: Clone + Ord, C> EventMap<E, C> {
|
||||
/// 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<C>) -> Self {
|
||||
self.add(event, binding);
|
||||
self
|
||||
}
|
||||
/// Add a binding to an event map.
|
||||
pub fn add (&mut self, event: E, binding: Binding<C>) -> &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<C>]> {
|
||||
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<C>> {
|
||||
self.query(event)
|
||||
.map(|bb|bb.iter().filter(|b|b.condition.as_ref().map(|c|(c.0)()).unwrap_or(true)).next())
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventMap<TuiEvent, Arc<str>> {
|
||||
pub fn load_into (binds: &Binds, name: &impl AsRef<str>, 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<C> Binding<C> {
|
||||
pub fn from_dsl (dsl: impl Dsl) -> Usually<Self> {
|
||||
let command: Option<C> = None;
|
||||
let condition: Option<Condition> = None;
|
||||
let description: Option<Arc<str>> = None;
|
||||
let source: Option<Arc<PathBuf>> = 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<AppCommand> {
|
||||
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<Arc<RwLock<MidiClip>>>) => 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<Option<Dialog>> for DialogCommand {
|
||||
//fn execute (self, state: &mut Option<Dialog>) -> Perhaps<Self> {
|
||||
//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<Dialog>)]//Nope.
|
||||
//impl DialogCommand {
|
||||
//fn open (dialog: &mut Option<Dialog>, new: Dialog) -> Perhaps<Self> {
|
||||
//*dialog = Some(new);
|
||||
//Ok(None)
|
||||
//}
|
||||
//fn close (dialog: &mut Option<Dialog>) -> Perhaps<Self> {
|
||||
//*dialog = None;
|
||||
//Ok(None)
|
||||
//}
|
||||
//}
|
||||
//
|
||||
//dsl_bind!(AppCommand: App {
|
||||
//enqueue = |app, clip: Option<Arc<RwLock<MidiClip>>>| { 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<Self> {
|
||||
////Ok(app.set_color(Some(theme)).map(|theme|Self::Color{theme}))
|
||||
////}
|
||||
////fn launch (app: &mut App) -> Perhaps<Self> {
|
||||
////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));
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
/// Configuration.
|
||||
///
|
||||
/// Contains mode, view, and bind definitions.
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Config {
|
||||
pub dirs: BaseDirectories,
|
||||
pub modes: Modes,
|
||||
pub views: Views,
|
||||
pub binds: Binds,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
const CONFIG: &'static str = "tek.edn";
|
||||
const DEFAULTS: &'static str = include_str!("./tek.edn");
|
||||
|
||||
pub fn new (dirs: Option<BaseDirectories>) -> 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::<Arc<str>>::load_into(&self.modes, &name, &body)?,
|
||||
Some("keys") if let Some(name) = name =>
|
||||
EventMap::<TuiEvent, Arc<str>>::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())
|
||||
})
|
||||
}
|
||||
}
|
||||
132
app/tek_cli.rs
132
app/tek_cli.rs
|
|
@ -1,132 +0,0 @@
|
|||
pub(crate) use tek::*;
|
||||
pub(crate) use clap::{self, Parser, Subcommand};
|
||||
|
||||
/// Application entrypoint.
|
||||
pub fn main () -> Usually<()> {
|
||||
Cli::parse().run()
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(name = "tek", version, about = Some(HEADER), long_about = Some(HEADER))]
|
||||
pub struct Cli {
|
||||
/// Pre-defined configuration modes.
|
||||
///
|
||||
/// TODO: Replace these with scripted configurations.
|
||||
#[command(subcommand)] mode: Option<LaunchMode>,
|
||||
/// Name of JACK client
|
||||
#[arg(short='n', long)] name: Option<String>,
|
||||
/// 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<f64>,
|
||||
/// 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<String>,
|
||||
/// MIDI outs to connect to (multiple instances accepted)
|
||||
#[arg(short='i', long)] midi_from_re: Vec<String>,
|
||||
/// MIDI ins to connect to (multiple instances accepted)
|
||||
#[arg(short='O', long)] midi_to: Vec<String>,
|
||||
/// MIDI ins to connect to (multiple instances accepted)
|
||||
#[arg(short='o', long)] midi_to_re: Vec<String>,
|
||||
/// Audio outs to connect to left input
|
||||
#[arg(short='l', long)] left_from: Vec<String>,
|
||||
/// Audio outs to connect to right input
|
||||
#[arg(short='r', long)] right_from: Vec<String>,
|
||||
/// Audio ins to connect from left output
|
||||
#[arg(short='L', long)] left_to: Vec<String>,
|
||||
/// Audio ins to connect from right output
|
||||
#[arg(short='R', long)] right_to: Vec<String>,
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
Connect::collect(&self.midi_from, &[] as &[&str], &self.midi_from_re)
|
||||
}
|
||||
fn midi_tos (&self) -> Vec<Connect> {
|
||||
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();
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
pub(crate) use ::{
|
||||
tek_device::{*, tek_engine::*},
|
||||
tengri::{
|
||||
Usually, Perhaps, Has, MaybeHas, has, maybe_has, impl_debug, from,
|
||||
wrap_inc, wrap_dec,
|
||||
dsl::*,
|
||||
input::*,
|
||||
output::*,
|
||||
tui::{
|
||||
*,
|
||||
ratatui::{
|
||||
self,
|
||||
prelude::{Rect, Style, Stylize, Buffer, Modifier, buffer::Cell, Color::{self, *}},
|
||||
widgets::{Widget, canvas::{Canvas, Line}},
|
||||
},
|
||||
crossterm::{
|
||||
self,
|
||||
event::{Event, KeyCode::{self, *}},
|
||||
},
|
||||
}
|
||||
},
|
||||
std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, RwLock},
|
||||
sync::atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed},
|
||||
error::Error,
|
||||
collections::BTreeMap,
|
||||
fmt::Write,
|
||||
cmp::Ord,
|
||||
ffi::OsString,
|
||||
fmt::{Debug, Formatter},
|
||||
fs::File,
|
||||
ops::{Add, Sub, Mul, Div, Rem},
|
||||
thread::JoinHandle
|
||||
},
|
||||
xdg::BaseDirectories,
|
||||
atomic_float::*
|
||||
};
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
use super::*;
|
||||
|
||||
pub type Modes = Arc<RwLock<BTreeMap<Arc<str>, Arc<Mode<Arc<str>>>>>>;
|
||||
|
||||
/// A set of currently active view and keys definitions,
|
||||
/// with optional name and description.
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Mode<D: Dsl + Ord> {
|
||||
pub path: PathBuf,
|
||||
pub name: Vec<D>,
|
||||
pub info: Vec<D>,
|
||||
pub view: Vec<D>,
|
||||
pub keys: Vec<D>,
|
||||
pub modes: Modes,
|
||||
}
|
||||
|
||||
impl<D: Dsl + Ord> Draw<TuiOut> for Mode<D> {
|
||||
fn draw (&self, to: &mut TuiOut) {
|
||||
self.content().draw(to)
|
||||
}
|
||||
}
|
||||
|
||||
impl Mode<Arc<str>> {
|
||||
|
||||
pub fn load_into (modes: &Modes, name: &impl AsRef<str>, 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());
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
103
app/tek_test.rs
103
app/tek_test.rs
|
|
@ -1,103 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
#[cfg(test)] #[test] fn test_app () -> Usually<()> {
|
||||
let mut app = App::default();
|
||||
let _ = app.scene_add(None, None)?;
|
||||
let _ = app.update_clock();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)] #[test] fn test_track () -> Usually<()> {
|
||||
let track = Track::default();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)] #[test] fn test_scene () -> Usually<()> {
|
||||
let scene = Scene::default();
|
||||
let _ = scene.pulses();
|
||||
let _ = scene.is_playing(&[]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)] #[test] fn test_view_layout () {
|
||||
let _ = button_play_pause(true);
|
||||
let _ = button_2("", "", true);
|
||||
let _ = button_2("", "", false);
|
||||
let _ = button_3("", "", "", true);
|
||||
let _ = button_3("", "", "", false);
|
||||
//let _ = heading("", "", 0, "", true);
|
||||
//let _ = heading("", "", 0, "", false);
|
||||
let _ = wrap(Reset, Reset, "");
|
||||
}
|
||||
|
||||
#[cfg(test)] mod test_view_meter {
|
||||
use super::*;
|
||||
use proptest::prelude::*;
|
||||
#[test] fn test_view_meter () {
|
||||
let _ = view_meter("", 0.0);
|
||||
let _ = view_meters(&[0.0, 0.0]);
|
||||
}
|
||||
proptest! {
|
||||
#[test] fn proptest_view_meter (
|
||||
label in "\\PC*", value in f32::MIN..f32::MAX
|
||||
) {
|
||||
let _ = view_meter(&label, value);
|
||||
}
|
||||
#[test] fn proptest_view_meters (
|
||||
value1 in f32::MIN..f32::MAX,
|
||||
value2 in f32::MIN..f32::MAX
|
||||
) {
|
||||
let _ = view_meters(&[value1, value2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)] #[test] fn test_view_iter () {
|
||||
let mut app = App::default();
|
||||
app.project.editor = Some(Default::default());
|
||||
//let _: Vec<_> = app.project.inputs_with_sizes().collect();
|
||||
//let _: Vec<_> = app.project.outputs_with_sizes().collect();
|
||||
let _: Vec<_> = app.project.tracks_with_sizes().collect();
|
||||
//let _: Vec<_> = app.project.scenes_with_sizes(true, 10, 10).collect();
|
||||
//let _: Vec<_> = app.scenes_with_colors(true, 10).collect();
|
||||
//let _: Vec<_> = app.scenes_with_track_colors(true, 10, 10).collect();
|
||||
}
|
||||
|
||||
#[cfg(test)] #[test] fn test_view_sizes () {
|
||||
let app = App::default();
|
||||
let _ = app.project.w();
|
||||
//let _ = app.project.w_sidebar();
|
||||
//let _ = app.project.w_tracks_area();
|
||||
let _ = app.project.h();
|
||||
//let _ = app.project.h_tracks_area();
|
||||
//let _ = app.project.h_inputs();
|
||||
//let _ = app.project.h_outputs();
|
||||
let _ = app.project.h_scenes();
|
||||
}
|
||||
|
||||
#[cfg(test)] #[test] fn test_midi_edit () {
|
||||
let _editor = MidiEditor::default();
|
||||
let mut editor = MidiEditor {
|
||||
mode: PianoHorizontal::new(Some(&Arc::new(RwLock::new(MidiClip::stop_all())))),
|
||||
size: Default::default(),
|
||||
//keys: Default::default(),
|
||||
};
|
||||
let _ = editor.put_note(true);
|
||||
let _ = editor.put_note(false);
|
||||
let _ = editor.clip_status();
|
||||
let _ = editor.edit_status();
|
||||
struct TestEditorHost(Option<MidiEditor>);
|
||||
has!(Option<MidiEditor>: |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();
|
||||
}
|
||||
353
app/tek_view.rs
353
app/tek_view.rs
|
|
@ -1,353 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
pub type Views = Arc<RwLock<BTreeMap<Arc<str>, Arc<str>>>>;
|
||||
|
||||
impl Draw<TuiOut> 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<TuiOut, ()> 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("<no name>");
|
||||
let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or("<no info>");
|
||||
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<TuiOut> {
|
||||
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<TuiOut> {
|
||||
//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<TuiOut> {
|
||||
//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<TuiOut>)|{
|
||||
//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<TuiOut> + 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<TuiOut> + 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<TuiOut> + 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<TuiOut> + 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<TuiOut> + 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<TuiOut> + use<'_> {
|
||||
//self.project.view_midi_ins_status(self.color)
|
||||
//}
|
||||
//pub fn view_midi_outs_status (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
//self.project.view_midi_outs_status(self.color)
|
||||
//}
|
||||
//pub fn view_audio_ins_status (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
//self.project.view_audio_ins_status(self.color)
|
||||
//}
|
||||
//pub fn view_audio_outs_status (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
//self.project.view_audio_outs_status(self.color)
|
||||
//}
|
||||
//pub fn view_scenes (&self) -> impl Content<TuiOut> + 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<TuiOut> + use<'_> {
|
||||
//self.project.view_scenes_names()
|
||||
//}
|
||||
//pub fn view_scenes_clips (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
//self.project.view_scenes_clips()
|
||||
//}
|
||||
//pub fn view_tracks_inputs <'a> (&'a self) -> impl Content<TuiOut> + 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<TuiOut> + use<'a> {
|
||||
//self.project.view_outputs(self.color)
|
||||
//}
|
||||
//pub fn view_tracks_devices <'a> (&'a self) -> impl Content<TuiOut> + use<'a> {
|
||||
//Fixed::Y(4, self.project.view_track_devices(self.color))
|
||||
//}
|
||||
//pub fn view_tracks_names <'a> (&'a self) -> impl Content<TuiOut> + use<'a> {
|
||||
//Fixed::Y(2, self.project.view_track_names(self.color))
|
||||
//}
|
||||
//pub fn view_pool (&self) -> impl Content<TuiOut> + 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<TuiOut> + use<'_> {
|
||||
//self.project.sampler().map(|s|s.view_list(true, self.editor().unwrap()))
|
||||
//}
|
||||
//pub fn view_samples_grid (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
//self.project.sampler().map(|s|s.view_grid())
|
||||
//}
|
||||
//pub fn view_sample_viewer (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
//self.project.sampler().map(|s|s.view_sample(self.editor().unwrap().get_note_pos()))
|
||||
//}
|
||||
//pub fn view_sample_info (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
//self.project.sampler().map(|s|s.view_sample_info(self.editor().unwrap().get_note_pos()))
|
||||
//}
|
||||
//pub fn view_sample_status (&self) -> impl Content<TuiOut> + 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)))
|
||||
64
bacon.toml
64
bacon.toml
|
|
@ -1,64 +0,0 @@
|
|||
# https://dystroy.org/bacon/config/
|
||||
default_job = "test"
|
||||
env.CARGO_TERM_COLOR = "always"
|
||||
[keybindings]
|
||||
c = "job:check"
|
||||
t = "job:test"
|
||||
n = "job:nextest"
|
||||
l = "job:clippy"
|
||||
[jobs]
|
||||
[jobs.check]
|
||||
command = ["cargo", "check"]
|
||||
need_stdout = false
|
||||
watch = ["deps", "engine", "device", "app"]
|
||||
[jobs.check-all]
|
||||
command = ["cargo", "check", "--all-targets"]
|
||||
need_stdout = false
|
||||
watch = ["deps", "engine", "device", "app"]
|
||||
[jobs.clippy]
|
||||
command = ["cargo", "clippy"]
|
||||
need_stdout = false
|
||||
watch = ["deps", "engine", "device", "app"]
|
||||
[jobs.clippy-all]
|
||||
command = ["cargo", "clippy", "--all-targets"]
|
||||
need_stdout = false
|
||||
watch = ["deps", "engine", "device", "app"]
|
||||
[jobs.test]
|
||||
command = ["cargo", "test", "--workspace", "--exclude", "jack"]
|
||||
need_stdout = true
|
||||
watch = ["deps", "engine", "device", "app"]
|
||||
[jobs.nextest]
|
||||
watch = ["deps", "engine", "device", "app"]
|
||||
command = [
|
||||
"cargo", "nextest", "run",
|
||||
"--hide-progress-bar", "--failure-output", "final"
|
||||
]
|
||||
need_stdout = true
|
||||
analyzer = "nextest"
|
||||
[jobs.doc]
|
||||
command = ["cargo", "doc", "--no-deps"]
|
||||
need_stdout = false
|
||||
[jobs.doc-open]
|
||||
command = ["cargo", "doc", "--no-deps", "--open"]
|
||||
need_stdout = false
|
||||
on_success = "back" # so that we don't open the browser at each change
|
||||
[jobs.run]
|
||||
command = [
|
||||
"cargo", "run",
|
||||
# put launch parameters for your program behind a `--` separator
|
||||
]
|
||||
need_stdout = true
|
||||
allow_warnings = true
|
||||
background = true
|
||||
[jobs.run-long]
|
||||
watch = ["deps", "engine", "device", "app"]
|
||||
command = [ "cargo", "run", ]
|
||||
need_stdout = true
|
||||
allow_warnings = true
|
||||
background = false
|
||||
on_change_strategy = "kill_then_restart"
|
||||
[jobs.ex]
|
||||
watch = ["deps", "engine", "device", "app"]
|
||||
command = ["cargo", "run", "--example"]
|
||||
need_stdout = true
|
||||
allow_warnings = true
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
FROM docker.io/library/debian:bookworm
|
||||
RUN apt update \
|
||||
&& apt install -y build-essential bash tree git wget \
|
||||
pkg-config libjack-dev liblilv-dev libserd-dev libsord-dev
|
||||
RUN adduser --quiet --uid 1000 --disabled-password build
|
||||
RUN wget https://static.rust-lang.org/rustup/dist/x86_64-unknown-linux-gnu/rustup-init \
|
||||
&& chmod +x ./rustup-init \
|
||||
&& mv rustup-init /usr/bin/rustup-init
|
||||
USER build
|
||||
WORKDIR /home/build
|
||||
RUN rustup-init -yv --profile minimal --default-toolchain nightly \
|
||||
&& rm -rvf "$HOME/.rustup/roolchains/*/share"
|
||||
RUN ls -alh "$HOME" && bash -c '. "$HOME/.cargo/env" \
|
||||
&& cargo version -vv'
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
FROM docker.io/library/alpine:edge
|
||||
|
||||
RUN apk add --no-cache build-base bash tree rustup git just cloc clang20-dev pipewire-jack-dev
|
||||
|
||||
RUN adduser -Du1000 build
|
||||
|
||||
USER 1000
|
||||
|
||||
RUN rustup-init -y --profile minimal --default-toolchain nightly \
|
||||
&& rm -rvf "$HOME/.rustup/roolchains/*/share"
|
||||
|
||||
RUN source "$HOME/.cargo/env" \
|
||||
&& cargo version -vv
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
This directory contains Dockerfiles and shell scripts
|
||||
for building Tek in a container. For now, only the
|
||||
GLIBC build works, as the Musl static build is unable
|
||||
to `dlopen` the system's `libjack.so`.
|
||||
|
||||
Invoke from repo root, like this: `build/release-glibc.sh`.
|
||||
This will first build a Docker image, `tek:glibc`, which
|
||||
will contain all build-time dependencies; then, it
|
||||
will invoke a `cargo build --release` in a container
|
||||
spawned from that image, ultimately placing the
|
||||
release build in this directory, as `build/tek`.
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
set -exo pipefail
|
||||
docker inspect tek:glibc || time docker build --cache-from=internal \
|
||||
-f build/Dockerfile.glibc -t tek:glibc .
|
||||
time docker run \
|
||||
--rm -itu0 \
|
||||
-v .:/build -w /build \
|
||||
-vtek-build-cargo:/home/build/.cargo \
|
||||
-vtek-build-target:/build/target \
|
||||
-eRUST_JACK_DLOPEN=true \
|
||||
tek:glibc $@
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
set -exo pipefail
|
||||
docker inspect tek:glibc || time docker build --cache-from=internal \
|
||||
-f build/Dockerfile.glibc -t tek:glibc .
|
||||
time docker run \
|
||||
--rm -itu0 \
|
||||
-v .:/build -w /build \
|
||||
-vtek-build-cargo:/home/build/.cargo \
|
||||
-vtek-build-target:/build/target \
|
||||
-eRUST_JACK_DLOPEN=true \
|
||||
tek:glibc sh -c "chown -R 1000:1000 /build/target \
|
||||
&& su build -c '. ~/.cargo/env \
|
||||
&& time cargo build -j4 --release \
|
||||
&& cp target/release/tek build/'"
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
set -exo pipefail
|
||||
docker inspect tek:musl || time docker build --cache-from=internal \
|
||||
-f build/Dockerfile.musl -t tek:musl .
|
||||
time docker run \
|
||||
--rm -itu0 \
|
||||
-v .:/build -w /build \
|
||||
-vtek-build-cargo:/home/build/.cargo \
|
||||
-vtek-build-target:/build/target \
|
||||
-eRUST_JACK_DLOPEN=true \
|
||||
tek:musl $@
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
set -exo pipefail
|
||||
docker inspect tek:musl || time docker build --cache-from=internal \
|
||||
-f build/Dockerfile.musl -t tek:musl .
|
||||
time docker run \
|
||||
--rm -itu0 \
|
||||
-v .:/build -w /build \
|
||||
-vtek-build-cargo:/home/build/.cargo \
|
||||
-vtek-build-target:/build/target \
|
||||
-eRUST_JACK_DLOPEN=true \
|
||||
tek:musl sh -c "chown -R 1000:1000 /build/target \
|
||||
&& su build -c 'source ~/.cargo/env \
|
||||
&& just build-release \
|
||||
&& cp target/release/tek build/'"
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
[package]
|
||||
name = "tek_suil"
|
||||
name = "suil-rs"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
gtk = "0.18.1"
|
||||
livi = "0.7.4"
|
||||
#winit = { version = "0.30.4", features = [ "x11" ] }
|
||||
|
||||
[build-dependencies]
|
||||
bindgen = "0.69.4"
|
||||
|
|
@ -78,19 +78,19 @@ pub const KEYMAP_GLOBAL: &'static [KeyBinding<App>] = keymap!(App {
|
|||
Ok(true)
|
||||
}],
|
||||
[Char('+'), NONE, "quant_inc", "quantize coarser", |app: &mut App| {
|
||||
app.transport.quant = Note::next(app.transport.quant);
|
||||
app.transport.quant = next_note_length(app.transport.quant);
|
||||
Ok(true)
|
||||
}],
|
||||
[Char('_'), NONE, "quant_dec", "quantize finer", |app: &mut App| {
|
||||
app.transport.quant = Note::prev(app.transport.quant);
|
||||
app.transport.quant = prev_note_length(app.transport.quant);
|
||||
Ok(true)
|
||||
}],
|
||||
[Char('='), NONE, "zoom_in", "show fewer ticks per block", |app: &mut App| {
|
||||
app.arranger.sequencer_mut().map(|s|s.time_axis.scale_mut(&Note::prev));
|
||||
app.arranger.sequencer_mut().map(|s|s.time_axis.scale_mut(&prev_note_length));
|
||||
Ok(true)
|
||||
}],
|
||||
[Char('-'), NONE, "zoom_out", "show more ticks per block", |app: &mut App| {
|
||||
app.arranger.sequencer_mut().map(|s|s.time_axis.scale_mut(&Note::next));
|
||||
app.arranger.sequencer_mut().map(|s|s.time_axis.scale_mut(&next_note_length));
|
||||
Ok(true)
|
||||
}],
|
||||
[Char('x'), NONE, "extend", "double the current clip", |app: &mut App| {
|
||||
60
crates/tek/Cargo.toml
Normal file
60
crates/tek/Cargo.toml
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
[package]
|
||||
name = "tek"
|
||||
edition = "2021"
|
||||
version = "0.2.0"
|
||||
|
||||
[dependencies]
|
||||
#no_deadlocks = "1.3.2"
|
||||
#vst3 = "0.1.0"
|
||||
atomic_float = "1.0.0"
|
||||
backtrace = "0.3.72"
|
||||
better-panic = "0.3.0"
|
||||
clap = { version = "4.5.4", features = [ "derive" ] }
|
||||
clojure-reader = "0.1.0"
|
||||
crossterm = "0.27"
|
||||
jack = "0.13"
|
||||
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"
|
||||
ratatui = { version = "0.26.3", features = [ "unstable-widget-ref", "underline-color" ] }
|
||||
#suil-rs = { path = "../suil" }
|
||||
symphonia = { version = "0.5.4", features = [ "all" ] }
|
||||
toml = "0.8.12"
|
||||
uuid = { version = "1.10.0", features = [ "v4" ] }
|
||||
#vst = "0.4.0"
|
||||
wavers = "1.4.3"
|
||||
#winit = { version = "0.30.4", features = [ "x11" ] }
|
||||
|
||||
[dev-dependencies]
|
||||
#tek_app = { version = "0.1.0", path = "../tek_app" }
|
||||
|
||||
[[bin]]
|
||||
name = "tek_arranger"
|
||||
path = "src/cli/cli_arranger.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "tek_sequencer"
|
||||
path = "src/cli/cli_sequencer.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "tek_transport"
|
||||
path = "src/cli/cli_transport.rs"
|
||||
|
||||
#[[bin]]
|
||||
#name = "tek_mixer"
|
||||
#path = "src/cli_mixer.rs"
|
||||
|
||||
#[[bin]]
|
||||
#name = "tek_track"
|
||||
#path = "src/cli_track.rs"
|
||||
|
||||
#[[bin]]
|
||||
#name = "tek_sampler"
|
||||
#path = "src/cli_sampler.rs"
|
||||
|
||||
#[[bin]]
|
||||
#name = "tek_plugin"
|
||||
#path = "src/cli_plugin.rs"
|
||||
49
crates/tek/README.md
Normal file
49
crates/tek/README.md
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
|
||||
|
||||
# `tek_sequencer`
|
||||
|
||||
This crate implements a MIDI sequencer and arranger with clip launching.
|
||||
|
||||
---
|
||||
|
||||
# `tek_arranger`
|
||||
|
||||
---
|
||||
|
||||
# `tek_timer`
|
||||
|
||||
This crate implements time sync and JACK transport control.
|
||||
|
||||
* Warning: If transport is set rolling by qjackctl, this program can't pause it
|
||||
* Todo: bpm: shift +/- 0.001
|
||||
* Todo: quant/sync: shift = next/prev value of same type (normal, triplet, dotted)
|
||||
* Or: use shift to switch between inc/dec top/bottom value?
|
||||
* Todo: focus play button
|
||||
* Todo: focus time position
|
||||
* Todo: edit numeric values
|
||||
* Todo: jump to time/bbt markers
|
||||
* Todo: count xruns
|
||||
|
||||
---
|
||||
|
||||
# `tek_mixer`
|
||||
|
||||
// TODO:
|
||||
// - Meters: propagate clipping:
|
||||
// - If one stage clips, all stages after it are marked red
|
||||
// - If one track clips, all tracks that feed from it are marked red?
|
||||
|
||||
# `tek_track`
|
||||
|
||||
---
|
||||
|
||||
# `tek_sampler`
|
||||
|
||||
This crate implements a sampler device which plays audio files
|
||||
in response to MIDI notes.
|
||||
|
||||
---
|
||||
|
||||
# `tek_plugin`
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
|
@ -1,4 +1,5 @@
|
|||
use tek::*;
|
||||
use tek_core::*;
|
||||
use tek_core::jack::*;
|
||||
|
||||
fn main () -> Usually<()> {
|
||||
Tui::run(Arc::new(RwLock::new(Demo::new())))?;
|
||||
|
|
@ -14,7 +15,20 @@ impl Demo<Tui> {
|
|||
fn new () -> Self {
|
||||
Self {
|
||||
index: 0,
|
||||
items: vec![]
|
||||
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
|
||||
//}),
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -27,26 +41,26 @@ impl Content for Demo<Tui> {
|
|||
|
||||
add(&Background(Color::Rgb(0,128,128)))?;
|
||||
|
||||
add(&Margin::XY(1, 1, Stack::down(|add|{
|
||||
add(&Outset::XY(1, 1, Stack::down(|add|{
|
||||
|
||||
add(&Layers::new(|add|{
|
||||
add(&Background(Color::Rgb(128,96,0)))?;
|
||||
add(&Border(Square(border_style)))?;
|
||||
add(&Margin::XY(2, 1, "..."))?;
|
||||
add(&Outset::XY(2, 1, "..."))?;
|
||||
Ok(())
|
||||
}).debug())?;
|
||||
|
||||
add(&Layers::new(|add|{
|
||||
add(&Background(Color::Rgb(128,64,0)))?;
|
||||
add(&Border(Lozenge(border_style)))?;
|
||||
add(&Margin::XY(4, 2, "---"))?;
|
||||
add(&Outset::XY(4, 2, "---"))?;
|
||||
Ok(())
|
||||
}).debug())?;
|
||||
|
||||
add(&Layers::new(|add|{
|
||||
add(&Background(Color::Rgb(96,64,0)))?;
|
||||
add(&Border(SquareBold(border_style)))?;
|
||||
add(&Margin::XY(6, 3, "~~~"))?;
|
||||
add(&Outset::XY(6, 3, "~~~"))?;
|
||||
Ok(())
|
||||
}).debug())?;
|
||||
|
||||
|
|
@ -56,15 +70,15 @@ impl Content for Demo<Tui> {
|
|||
Ok(())
|
||||
|
||||
}))
|
||||
//Align::Center(Margin::X(1, Layers::new(|add|{
|
||||
//Align::Center(Outset::X(1, Layers::new(|add|{
|
||||
//add(&Background(Color::Rgb(128,0,0)))?;
|
||||
//add(&Stack::down(|add|{
|
||||
//add(&Margin::Y(1, Layers::new(|add|{
|
||||
//add(&Outset::Y(1, Layers::new(|add|{
|
||||
//add(&Background(Color::Rgb(0,128,0)))?;
|
||||
//add(&Align::Center("12345"))?;
|
||||
//add(&Align::Center("FOO"))
|
||||
//})))?;
|
||||
//add(&Margin::XY(1, 1, Layers::new(|add|{
|
||||
//add(&Outset::XY(1, 1, Layers::new(|add|{
|
||||
//add(&Align::Center("1234567"))?;
|
||||
//add(&Align::Center("BAR"))?;
|
||||
//add(&Background(Color::Rgb(0,0,128)))
|
||||
|
|
@ -74,13 +88,13 @@ impl Content for Demo<Tui> {
|
|||
|
||||
//Align::Y(Layers::new(|add|{
|
||||
//add(&Background(Color::Rgb(128,0,0)))?;
|
||||
//add(&Margin::X(1, Align::Center(Stack::down(|add|{
|
||||
//add(&Align::X(Margin::Y(1, Layers::new(|add|{
|
||||
//add(&Outset::X(1, Align::Center(Stack::down(|add|{
|
||||
//add(&Align::X(Outset::Y(1, Layers::new(|add|{
|
||||
//add(&Background(Color::Rgb(0,128,0)))?;
|
||||
//add(&Align::Center("12345"))?;
|
||||
//add(&Align::Center("FOO"))
|
||||
//})))?;
|
||||
//add(&Margin::XY(1, 1, Layers::new(|add|{
|
||||
//add(&Outset::XY(1, 1, Layers::new(|add|{
|
||||
//add(&Align::Center("1234567"))?;
|
||||
//add(&Align::Center("BAR"))?;
|
||||
//add(&Background(Color::Rgb(0,0,128)))
|
||||
|
|
@ -91,14 +105,13 @@ impl Content for Demo<Tui> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Handle<TuiIn> for Demo<Tui> {
|
||||
fn handle (&mut self, from: &TuiIn) -> Perhaps<bool> {
|
||||
use KeyCode::{PageUp, PageDown};
|
||||
impl Handle<Tui> for Demo<Tui> {
|
||||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||
match from.event() {
|
||||
kexp!(PageUp) => {
|
||||
key!(KeyCode::PageUp) => {
|
||||
self.index = (self.index + 1) % self.items.len();
|
||||
},
|
||||
kexp!(PageDown) => {
|
||||
key!(KeyCode::PageDown) => {
|
||||
self.index = if self.index > 1 {
|
||||
self.index - 1
|
||||
} else {
|
||||
|
|
@ -110,3 +123,22 @@ impl Handle<TuiIn> for Demo<Tui> {
|
|||
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 "~~~"))))))))
|
||||
18
crates/tek/examples/midi_import.rs
Normal file
18
crates/tek/examples/midi_import.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
use tek_api::*;
|
||||
|
||||
struct ExamplePhrases(Vec<Arc<RwLock<Phrase>>>);
|
||||
|
||||
impl HasPhrases for ExamplePhrases {
|
||||
fn phrases (&self) -> &Vec<Arc<RwLock<Phrase>>> {
|
||||
&self.0
|
||||
}
|
||||
fn phrases_mut (&mut self) -> &mut Vec<Arc<RwLock<Phrase>>> {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
fn main () -> Usually<()> {
|
||||
let mut phrases = ExamplePhrases(vec![]);
|
||||
PhrasePoolCommand::Import(0, String::from("./example.mid")).execute(&mut phrases)?;
|
||||
Ok(())
|
||||
}
|
||||
10
crates/tek/src/api.rs
Normal file
10
crates/tek/src/api.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
use crate::*;
|
||||
|
||||
mod phrase; pub(crate) use phrase::*;
|
||||
mod jack; pub(crate) use self::jack::*;
|
||||
mod clip; pub(crate) use clip::*;
|
||||
mod color; pub(crate) use color::*;
|
||||
mod clock; pub(crate) use clock::*;
|
||||
mod player; pub(crate) use player::*;
|
||||
mod scene; pub(crate) use scene::*;
|
||||
mod track; pub(crate) use track::*;
|
||||
83
crates/tek/src/api/_todo_api_channel.rs
Normal file
83
crates/tek/src/api/_todo_api_channel.rs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
use crate::*;
|
||||
|
||||
pub enum MixerTrackCommand {}
|
||||
|
||||
/// A mixer track.
|
||||
#[derive(Debug)]
|
||||
pub struct MixerTrack {
|
||||
pub name: String,
|
||||
/// Inputs of 1st device
|
||||
pub audio_ins: Vec<Port<AudioIn>>,
|
||||
/// Outputs of last device
|
||||
pub audio_outs: Vec<Port<AudioOut>>,
|
||||
/// Device chain
|
||||
pub devices: Vec<Box<dyn MixerTrackDevice>>,
|
||||
}
|
||||
|
||||
//impl MixerTrackDevice for LV2Plugin {}
|
||||
|
||||
impl MixerTrack {
|
||||
const SYM_NAME: &'static str = ":name";
|
||||
const SYM_GAIN: &'static str = ":gain";
|
||||
const SYM_SAMPLER: &'static str = "sampler";
|
||||
const SYM_LV2: &'static str = "lv2";
|
||||
pub fn from_edn <'a, 'e> (jack: &Arc<RwLock<JackClient>>, args: &[Edn<'e>]) -> Usually<Self> {
|
||||
let mut _gain = 0.0f64;
|
||||
let mut track = MixerTrack {
|
||||
name: String::new(),
|
||||
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(Self::SYM_NAME)) {
|
||||
track.name = n.to_string();
|
||||
}
|
||||
if let Some(Edn::Double(g)) = map.get(&Edn::Key(Self::SYM_GAIN)) {
|
||||
_gain = f64::from(*g);
|
||||
}
|
||||
},
|
||||
Edn::List(args) => match args.get(0) {
|
||||
// Add a sampler device to the track
|
||||
Some(Edn::Symbol(Self::SYM_SAMPLER)) => {
|
||||
track.devices.push(
|
||||
Box::new(Sampler::from_edn(jack, &args[1..])?) as Box<dyn MixerTrackDevice>
|
||||
);
|
||||
panic!(
|
||||
"unsupported in track {}: {:?}; tek_mixer not compiled with feature \"sampler\"",
|
||||
&track.name,
|
||||
args.get(0).unwrap()
|
||||
)
|
||||
},
|
||||
// Add a LV2 plugin to the track.
|
||||
Some(Edn::Symbol(Self::SYM_LV2)) => {
|
||||
track.devices.push(
|
||||
Box::new(LV2Plugin::from_edn(jack, &args[1..])?) as Box<dyn MixerTrackDevice>
|
||||
);
|
||||
panic!(
|
||||
"unsupported in track {}: {:?}; tek_mixer not compiled with feature \"plugin\"",
|
||||
&track.name,
|
||||
args.get(0).unwrap()
|
||||
)
|
||||
},
|
||||
None =>
|
||||
panic!("empty list track {}", &track.name),
|
||||
_ =>
|
||||
panic!("unexpected in track {}: {:?}", &track.name, args.get(0).unwrap())
|
||||
},
|
||||
_ => {}
|
||||
});
|
||||
Ok(track)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait MixerTrackDevice: Debug + Send + Sync {
|
||||
fn boxed (self) -> Box<dyn MixerTrackDevice> where Self: Sized + 'static {
|
||||
Box::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl MixerTrackDevice for Sampler {}
|
||||
|
||||
impl MixerTrackDevice for Plugin {}
|
||||
27
crates/tek/src/api/_todo_api_mixer.rs
Normal file
27
crates/tek/src/api/_todo_api_mixer.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Mixer {
|
||||
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
||||
pub jack: Arc<RwLock<JackClient>>,
|
||||
pub name: String,
|
||||
pub tracks: Vec<MixerTrack>,
|
||||
pub selected_track: usize,
|
||||
pub selected_column: usize,
|
||||
}
|
||||
|
||||
pub struct MixerAudio {
|
||||
model: Arc<RwLock<Mixer>>
|
||||
}
|
||||
|
||||
impl From<&Arc<RwLock<Mixer>>> for MixerAudio {
|
||||
fn from (model: &Arc<RwLock<Mixer>>) -> Self {
|
||||
Self { model: model.clone() }
|
||||
}
|
||||
}
|
||||
|
||||
impl Audio for MixerAudio {
|
||||
fn process (&mut self, _: &Client, _: &ProcessScope) -> Control {
|
||||
Control::Continue
|
||||
}
|
||||
}
|
||||
114
crates/tek/src/api/_todo_api_plugin.rs
Normal file
114
crates/tek/src/api/_todo_api_plugin.rs
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
use crate::*;
|
||||
|
||||
/// A plugin device.
|
||||
#[derive(Debug)]
|
||||
pub struct Plugin {
|
||||
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
||||
pub jack: Arc<RwLock<JackClient>>,
|
||||
pub name: String,
|
||||
pub path: Option<String>,
|
||||
pub plugin: Option<PluginKind>,
|
||||
pub selected: usize,
|
||||
pub mapping: bool,
|
||||
pub midi_ins: Vec<Port<MidiIn>>,
|
||||
pub midi_outs: Vec<Port<MidiOut>>,
|
||||
pub audio_ins: Vec<Port<AudioIn>>,
|
||||
pub audio_outs: Vec<Port<AudioOut>>,
|
||||
}
|
||||
impl Plugin {
|
||||
pub fn new_lv2 (
|
||||
jack: &Arc<RwLock<JackClient>>,
|
||||
name: &str,
|
||||
path: &str,
|
||||
) -> Usually<Self> {
|
||||
Ok(Self {
|
||||
jack: jack.clone(),
|
||||
name: name.into(),
|
||||
path: Some(String::from(path)),
|
||||
plugin: Some(PluginKind::LV2(LV2Plugin::new(path)?)),
|
||||
selected: 0,
|
||||
mapping: false,
|
||||
midi_ins: vec![],
|
||||
midi_outs: vec![],
|
||||
audio_ins: vec![],
|
||||
audio_outs: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
//fn jack_from_lv2 (name: &str, plugin: &::livi::Plugin) -> Usually<Jack> {
|
||||
//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)
|
||||
//}
|
||||
}
|
||||
|
||||
pub struct PluginAudio(Arc<RwLock<Plugin>>);
|
||||
|
||||
impl From<&Arc<RwLock<Plugin>>> for PluginAudio {
|
||||
fn from (model: &Arc<RwLock<Plugin>>) -> Self {
|
||||
Self(model.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Audio for PluginAudio {
|
||||
fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
|
||||
let state = &mut*self.0.write().unwrap();
|
||||
match state.plugin.as_mut() {
|
||||
Some(PluginKind::LV2(LV2Plugin {
|
||||
features,
|
||||
ref mut instance,
|
||||
ref mut input_buffer,
|
||||
..
|
||||
})) => {
|
||||
let urid = features.midi_urid();
|
||||
input_buffer.clear();
|
||||
for port in state.midi_ins.iter() {
|
||||
let mut atom = ::livi::event::LV2AtomSequence::new(
|
||||
&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(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
input_buffer.push(atom);
|
||||
}
|
||||
let mut outputs = vec![];
|
||||
for _ in state.midi_outs.iter() {
|
||||
outputs.push(::livi::event::LV2AtomSequence::new(
|
||||
&features,
|
||||
scope.n_frames() as usize
|
||||
));
|
||||
}
|
||||
let ports = ::livi::EmptyPortConnections::new()
|
||||
.with_atom_sequence_inputs(input_buffer.iter())
|
||||
.with_atom_sequence_outputs(outputs.iter_mut())
|
||||
.with_audio_inputs(state.audio_ins.iter().map(|o|o.as_slice(scope)))
|
||||
.with_audio_outputs(state.audio_outs.iter_mut().map(|o|o.as_mut_slice(scope)));
|
||||
unsafe {
|
||||
instance.run(scope.n_frames() as usize, ports).unwrap()
|
||||
};
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
Control::Continue
|
||||
}
|
||||
}
|
||||
21
crates/tek/src/api/_todo_api_plugin_kind.rs
Normal file
21
crates/tek/src/api/_todo_api_plugin_kind.rs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
use crate::*;
|
||||
|
||||
/// Supported plugin formats.
|
||||
#[derive(Default)]
|
||||
pub enum PluginKind {
|
||||
#[default] None,
|
||||
LV2(LV2Plugin),
|
||||
VST2 { instance: ::vst::host::PluginInstance },
|
||||
VST3,
|
||||
}
|
||||
|
||||
impl Debug for PluginKind {
|
||||
fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), Error> {
|
||||
write!(f, "{}", match self {
|
||||
Self::None => "(none)",
|
||||
Self::LV2(_) => "LV2",
|
||||
Self::VST2{..} => "VST2",
|
||||
Self::VST3 => "VST3",
|
||||
})
|
||||
}
|
||||
}
|
||||
61
crates/tek/src/api/_todo_api_plugin_lv2.rs
Normal file
61
crates/tek/src/api/_todo_api_plugin_lv2.rs
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
use crate::*;
|
||||
|
||||
/// A LV2 plugin.
|
||||
#[derive(Debug)]
|
||||
pub struct LV2Plugin {
|
||||
pub world: livi::World,
|
||||
pub instance: livi::Instance,
|
||||
pub plugin: livi::Plugin,
|
||||
pub features: Arc<livi::Features>,
|
||||
pub port_list: Vec<livi::Port>,
|
||||
pub input_buffer: Vec<livi::event::LV2AtomSequence>,
|
||||
pub ui_thread: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl LV2Plugin {
|
||||
const INPUT_BUFFER: usize = 1024;
|
||||
pub fn new (uri: &str) -> Usually<Self> {
|
||||
let world = livi::World::with_load_bundle(&uri);
|
||||
let features = world
|
||||
.build_features(livi::FeaturesBuilder {
|
||||
min_block_length: 1,
|
||||
max_block_length: 65536,
|
||||
});
|
||||
let plugin = world
|
||||
.iter_plugins()
|
||||
.nth(0)
|
||||
.expect(&format!("plugin not found: {uri}"));
|
||||
Ok(Self {
|
||||
instance: unsafe {
|
||||
plugin
|
||||
.instantiate(features.clone(), 48000.0)
|
||||
.expect(&format!("instantiate failed: {uri}"))
|
||||
},
|
||||
port_list: plugin.ports().collect::<Vec<_>>(),
|
||||
input_buffer: Vec::with_capacity(Self::INPUT_BUFFER),
|
||||
ui_thread: None,
|
||||
world,
|
||||
features,
|
||||
plugin,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl LV2Plugin {
|
||||
pub fn from_edn <'e> (jack: &Arc<RwLock<JackClient>>, args: &[Edn<'e>]) -> Usually<Plugin> {
|
||||
let mut name = String::new();
|
||||
let mut path = String::new();
|
||||
edn!(edn in args {
|
||||
Edn::Map(map) => {
|
||||
if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) {
|
||||
name = String::from(*n);
|
||||
}
|
||||
if let Some(Edn::Str(p)) = map.get(&Edn::Key(":path")) {
|
||||
path = String::from(*p);
|
||||
}
|
||||
},
|
||||
_ => panic!("unexpected in lv2 '{name}'"),
|
||||
});
|
||||
Plugin::new_lv2(jack, &name, &path)
|
||||
}
|
||||
}
|
||||
135
crates/tek/src/api/_todo_api_sampler.rs
Normal file
135
crates/tek/src/api/_todo_api_sampler.rs
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
use crate::*;
|
||||
|
||||
/// The sampler plugin plays sounds.
|
||||
#[derive(Debug)]
|
||||
pub struct Sampler {
|
||||
pub jack: Arc<RwLock<JackClient>>,
|
||||
pub name: String,
|
||||
pub mapped: BTreeMap<u7, Arc<RwLock<Sample>>>,
|
||||
pub unmapped: Vec<Arc<RwLock<Sample>>>,
|
||||
pub voices: Arc<RwLock<Vec<Voice>>>,
|
||||
pub midi_in: Port<MidiIn>,
|
||||
pub audio_outs: Vec<Port<AudioOut>>,
|
||||
pub buffer: Vec<Vec<f32>>,
|
||||
pub output_gain: f32
|
||||
}
|
||||
|
||||
impl Sampler {
|
||||
pub fn from_edn <'e> (jack: &Arc<RwLock<JackClient>>, args: &[Edn<'e>]) -> Usually<Self> {
|
||||
let mut name = String::new();
|
||||
let mut dir = String::new();
|
||||
let mut samples = BTreeMap::new();
|
||||
edn!(edn in args {
|
||||
Edn::Map(map) => {
|
||||
if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) {
|
||||
name = String::from(*n);
|
||||
}
|
||||
if let Some(Edn::Str(n)) = map.get(&Edn::Key(":dir")) {
|
||||
dir = String::from(*n);
|
||||
}
|
||||
},
|
||||
Edn::List(args) => match args.get(0) {
|
||||
Some(Edn::Symbol("sample")) => {
|
||||
let (midi, sample) = Sample::from_edn(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}: {edn:?}")
|
||||
});
|
||||
let midi_in = jack.read().unwrap().client().register_port("in", MidiIn::default())?;
|
||||
Ok(Sampler {
|
||||
jack: jack.clone(),
|
||||
name: name.into(),
|
||||
mapped: samples,
|
||||
unmapped: Default::default(),
|
||||
voices: Default::default(),
|
||||
buffer: Default::default(),
|
||||
midi_in: midi_in,
|
||||
audio_outs: vec![],
|
||||
output_gain: 0.
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SamplerAudio {
|
||||
model: Arc<RwLock<Sampler>>
|
||||
}
|
||||
|
||||
impl From<&Arc<RwLock<Sampler>>> for SamplerAudio {
|
||||
fn from (model: &Arc<RwLock<Sampler>>) -> Self {
|
||||
Self { model: model.clone() }
|
||||
}
|
||||
}
|
||||
|
||||
impl Audio for SamplerAudio {
|
||||
#[inline] fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
|
||||
self.process_midi_in(scope);
|
||||
self.clear_output_buffer();
|
||||
self.process_audio_out(scope);
|
||||
self.write_output_buffer(scope);
|
||||
Control::Continue
|
||||
}
|
||||
}
|
||||
|
||||
impl SamplerAudio {
|
||||
|
||||
/// 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.model.read().unwrap();
|
||||
for RawMidi { time, bytes } in midi_in.iter(scope) {
|
||||
if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() {
|
||||
if let MidiMessage::NoteOn { ref key, ref vel } = message {
|
||||
if let Some(sample) = mapped.get(key) {
|
||||
voices.write().unwrap().push(Sample::play(sample, time as usize, vel));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Zero the output buffer.
|
||||
pub fn clear_output_buffer (&mut self) {
|
||||
for buffer in self.model.write().unwrap().buffer.iter_mut() {
|
||||
buffer.fill(0.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Mix all currently playing samples into the output.
|
||||
pub fn process_audio_out (&mut self, scope: &ProcessScope) {
|
||||
let Sampler { ref mut buffer, voices, output_gain, .. } = &mut*self.model.write().unwrap();
|
||||
let channel_count = buffer.len();
|
||||
voices.write().unwrap().retain_mut(|voice|{
|
||||
for index in 0..scope.n_frames() as usize {
|
||||
if let Some(frame) = voice.next() {
|
||||
for (channel, sample) in frame.iter().enumerate() {
|
||||
// Averaging mixer:
|
||||
//self.buffer[channel % channel_count][index] = (
|
||||
//(self.buffer[channel % channel_count][index] + sample * self.output_gain) / 2.0
|
||||
//);
|
||||
buffer[channel % channel_count][index] += sample * *output_gain;
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
});
|
||||
}
|
||||
|
||||
/// Write output buffer to output ports.
|
||||
pub fn write_output_buffer (&mut self, scope: &ProcessScope) {
|
||||
let Sampler { ref mut audio_outs, buffer, .. } = &mut*self.model.write().unwrap();
|
||||
for (i, port) in audio_outs.iter_mut().enumerate() {
|
||||
let buffer = &buffer[i];
|
||||
for (i, value) in port.as_mut_slice(scope).iter_mut().enumerate() {
|
||||
*value = *buffer.get(i).unwrap_or(&0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
72
crates/tek/src/api/_todo_api_sampler_sample.rs
Normal file
72
crates/tek/src/api/_todo_api_sampler_sample.rs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
use crate::*;
|
||||
|
||||
/// A sound sample.
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Sample {
|
||||
pub name: String,
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
pub channels: Vec<Vec<f32>>,
|
||||
pub rate: Option<usize>,
|
||||
}
|
||||
|
||||
impl Sample {
|
||||
pub fn new (name: &str, start: usize, end: usize, channels: Vec<Vec<f32>>) -> Self {
|
||||
Self { name: name.to_string(), start, end, channels, rate: None }
|
||||
}
|
||||
pub fn play (sample: &Arc<RwLock<Self>>, after: usize, velocity: &u7) -> Voice {
|
||||
Voice {
|
||||
sample: sample.clone(),
|
||||
after,
|
||||
position: sample.read().unwrap().start,
|
||||
velocity: velocity.as_int() as f32 / 127.0,
|
||||
}
|
||||
}
|
||||
pub fn from_edn <'e> (jack: &Arc<RwLock<JackClient>>, dir: &str, args: &[Edn<'e>]) -> Usually<(Option<u7>, Arc<RwLock<Self>>)> {
|
||||
let mut name = String::new();
|
||||
let mut file = String::new();
|
||||
let mut midi = None;
|
||||
let mut start = 0usize;
|
||||
edn!(edn in args {
|
||||
Edn::Map(map) => {
|
||||
if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) {
|
||||
name = String::from(*n);
|
||||
}
|
||||
if let Some(Edn::Str(f)) = map.get(&Edn::Key(":file")) {
|
||||
file = String::from(*f);
|
||||
}
|
||||
if let Some(Edn::Int(i)) = map.get(&Edn::Key(":start")) {
|
||||
start = *i as usize;
|
||||
}
|
||||
if let Some(Edn::Int(m)) = map.get(&Edn::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(Self {
|
||||
name: name.into(),
|
||||
start,
|
||||
end,
|
||||
channels: data,
|
||||
rate: None
|
||||
}))))
|
||||
}
|
||||
|
||||
/// Read WAV from file
|
||||
pub fn read_data (src: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
|
||||
let mut channels: Vec<wavers::Samples<f32>> = vec![];
|
||||
for channel in wavers::Wav::from_path(src)?.channels() {
|
||||
channels.push(channel);
|
||||
}
|
||||
let mut end = 0;
|
||||
let mut data: Vec<Vec<f32>> = vec![];
|
||||
for samples in channels.iter() {
|
||||
let channel = Vec::from(samples.as_ref());
|
||||
end = end.max(channel.len());
|
||||
data.push(channel);
|
||||
}
|
||||
Ok((end, data))
|
||||
}
|
||||
}
|
||||
30
crates/tek/src/api/_todo_api_sampler_voice.rs
Normal file
30
crates/tek/src/api/_todo_api_sampler_voice.rs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
use crate::*;
|
||||
|
||||
/// A currently playing instance of a sample.
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct Voice {
|
||||
pub sample: Arc<RwLock<Sample>>,
|
||||
pub after: usize,
|
||||
pub position: usize,
|
||||
pub velocity: f32,
|
||||
}
|
||||
|
||||
impl Iterator for Voice {
|
||||
type Item = [f32;2];
|
||||
fn next (&mut self) -> Option<Self::Item> {
|
||||
if self.after > 0 {
|
||||
self.after = 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 = self.position + 1;
|
||||
return sample.channels[0].get(position).map(|_amplitude|[
|
||||
sample.channels[0][position] * self.velocity,
|
||||
sample.channels[0][position] * self.velocity,
|
||||
])
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
20
crates/tek/src/api/clip.rs
Normal file
20
crates/tek/src/api/clip.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ArrangerClipCommand {
|
||||
Play,
|
||||
Get(usize, usize),
|
||||
Set(usize, usize, Option<Arc<RwLock<Phrase>>>),
|
||||
Edit(Option<Arc<RwLock<Phrase>>>),
|
||||
SetLoop(bool),
|
||||
RandomColor,
|
||||
}
|
||||
|
||||
//impl<T: ArrangerApi> Command<T> for ArrangerClipCommand {
|
||||
//fn execute (self, state: &mut T) -> Perhaps<Self> {
|
||||
//match self {
|
||||
//_ => todo!()
|
||||
//}
|
||||
//Ok(None)
|
||||
//}
|
||||
//}
|
||||
198
crates/tek/src/api/clock.rs
Normal file
198
crates/tek/src/api/clock.rs
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
use crate::*;
|
||||
|
||||
pub trait HasClock: Send + Sync {
|
||||
fn clock (&self) -> &ClockModel;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum ClockCommand {
|
||||
Play(Option<u32>),
|
||||
Pause(Option<u32>),
|
||||
SeekUsec(f64),
|
||||
SeekSample(f64),
|
||||
SeekPulse(f64),
|
||||
SetBpm(f64),
|
||||
SetQuant(f64),
|
||||
SetSync(f64),
|
||||
}
|
||||
|
||||
impl<T: HasClock> Command<T> for ClockCommand {
|
||||
fn execute (self, state: &mut T) -> Perhaps<Self> {
|
||||
use ClockCommand::*;
|
||||
match self {
|
||||
Play(start) => state.clock().play_from(start)?,
|
||||
Pause(pause) => state.clock().pause_at(pause)?,
|
||||
SeekUsec(usec) => state.clock().playhead.update_from_usec(usec),
|
||||
SeekSample(sample) => state.clock().playhead.update_from_sample(sample),
|
||||
SeekPulse(pulse) => state.clock().playhead.update_from_pulse(pulse),
|
||||
SetBpm(bpm) => return Ok(Some(SetBpm(state.clock().timebase().bpm.set(bpm)))),
|
||||
SetQuant(quant) => return Ok(Some(SetQuant(state.clock().quant.set(quant)))),
|
||||
SetSync(sync) => return Ok(Some(SetSync(state.clock().sync.set(sync)))),
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Timeline {
|
||||
pub timebase: Arc<Timebase>,
|
||||
pub started: Arc<RwLock<Option<Moment>>>,
|
||||
pub loopback: Arc<RwLock<Option<Moment>>>,
|
||||
}
|
||||
|
||||
impl Default for Timeline {
|
||||
fn default () -> Self {
|
||||
Self {
|
||||
timebase: Arc::new(Timebase::default()),
|
||||
started: RwLock::new(None).into(),
|
||||
loopback: RwLock::new(None).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ClockModel {
|
||||
/// JACK transport handle.
|
||||
pub transport: Arc<Transport>,
|
||||
/// Global temporal resolution (shared by [Moment] fields)
|
||||
pub timebase: Arc<Timebase>,
|
||||
/// Current global sample and usec (monotonic from JACK clock)
|
||||
pub global: Arc<Moment>,
|
||||
/// Global sample and usec at which playback started
|
||||
pub started: Arc<RwLock<Option<Moment>>>,
|
||||
/// Current playhead position
|
||||
pub playhead: Arc<Moment>,
|
||||
/// Note quantization factor
|
||||
pub quant: Arc<Quantize>,
|
||||
/// Launch quantization factor
|
||||
pub sync: Arc<LaunchSync>,
|
||||
/// Size of buffer in samples
|
||||
pub chunk: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
impl From<&Arc<RwLock<JackClient>>> for ClockModel {
|
||||
fn from (jack: &Arc<RwLock<JackClient>>) -> Self {
|
||||
let jack = jack.read().unwrap();
|
||||
let chunk = jack.client().buffer_size();
|
||||
let transport = jack.client().transport();
|
||||
let timebase = Arc::new(Timebase::default());
|
||||
Self {
|
||||
quant: Arc::new(24.into()),
|
||||
sync: Arc::new(384.into()),
|
||||
transport: Arc::new(transport),
|
||||
chunk: Arc::new((chunk as usize).into()),
|
||||
global: Arc::new(Moment::zero(&timebase)),
|
||||
playhead: Arc::new(Moment::zero(&timebase)),
|
||||
started: RwLock::new(None).into(),
|
||||
timebase,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ClockModel {
|
||||
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||
f.debug_struct("ClockModel")
|
||||
.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 ClockModel {
|
||||
pub fn timebase (&self) -> &Arc<Timebase> {
|
||||
&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<u32>) -> Usually<()> {
|
||||
if let Some(start) = start {
|
||||
self.transport.locate(start)?;
|
||||
}
|
||||
self.transport.start()?;
|
||||
Ok(())
|
||||
}
|
||||
/// Pause, optionally seeking to a given location afterwards
|
||||
pub fn pause_at (&self, pause: Option<u32>) -> Usually<()> {
|
||||
self.transport.stop()?;
|
||||
if let Some(pause) = pause {
|
||||
self.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, Ordering::Relaxed);
|
||||
}
|
||||
pub fn update_from_scope (&self, scope: &ProcessScope) -> Usually<()> {
|
||||
self.set_chunk(scope.n_frames() as usize);
|
||||
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();
|
||||
match self.transport.query_state()? {
|
||||
TransportState::Rolling => {
|
||||
if started.is_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 => {
|
||||
if started.is_some() {
|
||||
*started = None;
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
};
|
||||
self.playhead.update_from_sample(match *started {
|
||||
Some(ref instant) => current_frames as f64 - instant.sample.get(),
|
||||
None => 0.
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Hosts the JACK callback for updating the temporal pointer and playback status.
|
||||
pub struct ClockAudio<'a, T: HasClock>(pub &'a mut T);
|
||||
|
||||
impl<'a, T: HasClock> Audio for ClockAudio<'a, T> {
|
||||
#[inline] fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
|
||||
self.0.clock().update_from_scope(scope).unwrap();
|
||||
Control::Continue
|
||||
}
|
||||
}
|
||||
6
crates/tek/src/api/color.rs
Normal file
6
crates/tek/src/api/color.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
use crate::*;
|
||||
|
||||
pub trait HasColor {
|
||||
fn color (&self) -> ItemColor;
|
||||
fn color_mut (&self) -> &mut ItemColor;
|
||||
}
|
||||
463
crates/tek/src/api/jack.rs
Normal file
463
crates/tek/src/api/jack.rs
Normal file
|
|
@ -0,0 +1,463 @@
|
|||
use crate::*;
|
||||
|
||||
pub trait JackApi {
|
||||
fn jack (&self) -> &Arc<RwLock<JackClient>>;
|
||||
}
|
||||
|
||||
pub trait HasMidiIns {
|
||||
fn midi_ins (&self) -> &Vec<Port<MidiIn>>;
|
||||
fn midi_ins_mut (&mut self) -> &mut Vec<Port<MidiIn>>;
|
||||
fn has_midi_ins (&self) -> bool {
|
||||
self.midi_ins().len() > 0
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HasMidiOuts {
|
||||
fn midi_outs (&self) -> &Vec<Port<MidiOut>>;
|
||||
fn midi_outs_mut (&mut self) -> &mut Vec<Port<MidiOut>>;
|
||||
fn midi_note (&mut self) -> &mut Vec<u8>;
|
||||
fn has_midi_outs (&self) -> bool {
|
||||
self.midi_outs().len() > 0
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
pub trait JackActivate: Sized {
|
||||
fn activate_with <T: Audio + 'static> (
|
||||
self,
|
||||
init: impl FnOnce(&Arc<RwLock<JackClient>>)->Usually<T>
|
||||
)
|
||||
-> Usually<Arc<RwLock<T>>>;
|
||||
}
|
||||
|
||||
impl JackActivate for JackClient {
|
||||
fn activate_with <T: Audio + 'static> (
|
||||
self,
|
||||
init: impl FnOnce(&Arc<RwLock<JackClient>>)->Usually<T>
|
||||
)
|
||||
-> Usually<Arc<RwLock<T>>>
|
||||
{
|
||||
let client = Arc::new(RwLock::new(self));
|
||||
let target = Arc::new(RwLock::new(init(&client)?));
|
||||
let event = Box::new(move|_|{/*TODO*/}) as Box<dyn Fn(JackEvent) + Send + Sync>;
|
||||
let events = Notifications(event);
|
||||
let frame = Box::new({
|
||||
let target = target.clone();
|
||||
move|c: &_, s: &_|if let Ok(mut target) = target.write() {
|
||||
target.process(c, s)
|
||||
} else {
|
||||
Control::Quit
|
||||
}
|
||||
});
|
||||
let frames = ClosureProcessHandler::new(frame as BoxedAudioHandler);
|
||||
let mut buffer = Self::Activating;
|
||||
std::mem::swap(&mut*client.write().unwrap(), &mut buffer);
|
||||
*client.write().unwrap() = Self::Active(Client::from(buffer).activate_async(events, frames)?);
|
||||
Ok(target)
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for things that have a JACK process callback.
|
||||
pub trait Audio: Send + Sync {
|
||||
fn process(&mut self, _: &Client, _: &ProcessScope) -> Control {
|
||||
Control::Continue
|
||||
}
|
||||
fn callback(
|
||||
state: &Arc<RwLock<Self>>, client: &Client, scope: &ProcessScope
|
||||
) -> Control where Self: Sized {
|
||||
if let Ok(mut state) = state.write() {
|
||||
state.process(client, scope)
|
||||
} else {
|
||||
Control::Quit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A UI component that may be associated with a JACK client by the `Jack` factory.
|
||||
pub trait AudioComponent<E: Engine>: Component<E> + Audio {
|
||||
/// Perform type erasure for collecting heterogeneous devices.
|
||||
fn boxed(self) -> Box<dyn AudioComponent<E>>
|
||||
where
|
||||
Self: Sized + 'static,
|
||||
{
|
||||
Box::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// All things that implement the required traits can be treated as `AudioComponent`.
|
||||
impl<E: Engine, W: Component<E> + Audio> AudioComponent<E> for W {}
|
||||
|
||||
/// Trait for things that may expose JACK ports.
|
||||
pub trait Ports {
|
||||
fn audio_ins(&self) -> Usually<Vec<&Port<Unowned>>> {
|
||||
Ok(vec![])
|
||||
}
|
||||
fn audio_outs(&self) -> Usually<Vec<&Port<Unowned>>> {
|
||||
Ok(vec![])
|
||||
}
|
||||
fn midi_ins(&self) -> Usually<Vec<&Port<Unowned>>> {
|
||||
Ok(vec![])
|
||||
}
|
||||
fn midi_outs(&self) -> Usually<Vec<&Port<Unowned>>> {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
fn register_ports<T: PortSpec + Copy>(
|
||||
client: &Client,
|
||||
names: Vec<String>,
|
||||
spec: T,
|
||||
) -> Usually<BTreeMap<String, Port<T>>> {
|
||||
names
|
||||
.into_iter()
|
||||
.try_fold(BTreeMap::new(), |mut ports, name| {
|
||||
let port = client.register_port(&name, spec)?;
|
||||
ports.insert(name, port);
|
||||
Ok(ports)
|
||||
})
|
||||
}
|
||||
|
||||
fn query_ports(client: &Client, names: Vec<String>) -> BTreeMap<String, Port<Unowned>> {
|
||||
names.into_iter().fold(BTreeMap::new(), |mut ports, name| {
|
||||
let port = client.port_by_name(&name).unwrap();
|
||||
ports.insert(name, port);
|
||||
ports
|
||||
})
|
||||
}
|
||||
|
||||
///// A [AudioComponent] bound to a JACK client and a set of ports.
|
||||
//pub struct JackDevice<E: Engine> {
|
||||
///// The active JACK client of this device.
|
||||
//pub client: DynamicAsyncClient,
|
||||
///// The device state, encapsulated for sharing between threads.
|
||||
//pub state: Arc<RwLock<Box<dyn AudioComponent<E>>>>,
|
||||
///// Unowned copies of the device's JACK ports, for connecting to the device.
|
||||
///// The "real" readable/writable `Port`s are owned by the `state`.
|
||||
//pub ports: UnownedJackPorts,
|
||||
//}
|
||||
|
||||
//impl<E: Engine> std::fmt::Debug for JackDevice<E> {
|
||||
//fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
//f.debug_struct("JackDevice")
|
||||
//.field("ports", &self.ports)
|
||||
//.finish()
|
||||
//}
|
||||
//}
|
||||
|
||||
//impl<E: Engine> Render for JackDevice<E> {
|
||||
//type Engine = E;
|
||||
//fn min_size(&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
//self.state.read().unwrap().layout(to)
|
||||
//}
|
||||
//fn render(&self, to: &mut E::Output) -> Usually<()> {
|
||||
//self.state.read().unwrap().render(to)
|
||||
//}
|
||||
//}
|
||||
|
||||
//impl<E: Engine> Handle<E> for JackDevice<E> {
|
||||
//fn handle(&mut self, from: &E::Input) -> Perhaps<E::Handled> {
|
||||
//self.state.write().unwrap().handle(from)
|
||||
//}
|
||||
//}
|
||||
|
||||
//impl<E: Engine> Ports for JackDevice<E> {
|
||||
//fn audio_ins(&self) -> Usually<Vec<&Port<Unowned>>> {
|
||||
//Ok(self.ports.audio_ins.values().collect())
|
||||
//}
|
||||
//fn audio_outs(&self) -> Usually<Vec<&Port<Unowned>>> {
|
||||
//Ok(self.ports.audio_outs.values().collect())
|
||||
//}
|
||||
//fn midi_ins(&self) -> Usually<Vec<&Port<Unowned>>> {
|
||||
//Ok(self.ports.midi_ins.values().collect())
|
||||
//}
|
||||
//fn midi_outs(&self) -> Usually<Vec<&Port<Unowned>>> {
|
||||
//Ok(self.ports.midi_outs.values().collect())
|
||||
//}
|
||||
//}
|
||||
|
||||
//impl<E: Engine> JackDevice<E> {
|
||||
///// Returns a locked mutex of the state's contents.
|
||||
//pub fn state(&self) -> LockResult<RwLockReadGuard<Box<dyn AudioComponent<E>>>> {
|
||||
//self.state.read()
|
||||
//}
|
||||
///// Returns a locked mutex of the state's contents.
|
||||
//pub fn state_mut(&self) -> LockResult<RwLockWriteGuard<Box<dyn AudioComponent<E>>>> {
|
||||
//self.state.write()
|
||||
//}
|
||||
//pub fn connect_midi_in(&self, index: usize, port: &Port<Unowned>) -> Usually<()> {
|
||||
//Ok(self
|
||||
//.client
|
||||
//.as_client()
|
||||
//.connect_ports(port, self.midi_ins()?[index])?)
|
||||
//}
|
||||
//pub fn connect_midi_out(&self, index: usize, port: &Port<Unowned>) -> Usually<()> {
|
||||
//Ok(self
|
||||
//.client
|
||||
//.as_client()
|
||||
//.connect_ports(self.midi_outs()?[index], port)?)
|
||||
//}
|
||||
//pub fn connect_audio_in(&self, index: usize, port: &Port<Unowned>) -> Usually<()> {
|
||||
//Ok(self
|
||||
//.client
|
||||
//.as_client()
|
||||
//.connect_ports(port, self.audio_ins()?[index])?)
|
||||
//}
|
||||
//pub fn connect_audio_out(&self, index: usize, port: &Port<Unowned>) -> Usually<()> {
|
||||
//Ok(self
|
||||
//.client
|
||||
//.as_client()
|
||||
//.connect_ports(self.audio_outs()?[index], port)?)
|
||||
//}
|
||||
//}
|
||||
|
||||
///// Collection of JACK ports as [AudioIn]/[AudioOut]/[MidiIn]/[MidiOut].
|
||||
//#[derive(Default, Debug)]
|
||||
//pub struct JackPorts {
|
||||
//pub audio_ins: BTreeMap<String, Port<AudioIn>>,
|
||||
//pub midi_ins: BTreeMap<String, Port<MidiIn>>,
|
||||
//pub audio_outs: BTreeMap<String, Port<AudioOut>>,
|
||||
//pub midi_outs: BTreeMap<String, Port<MidiOut>>,
|
||||
//}
|
||||
|
||||
///// Collection of JACK ports as [Unowned].
|
||||
//#[derive(Default, Debug)]
|
||||
//pub struct UnownedJackPorts {
|
||||
//pub audio_ins: BTreeMap<String, Port<Unowned>>,
|
||||
//pub midi_ins: BTreeMap<String, Port<Unowned>>,
|
||||
//pub audio_outs: BTreeMap<String, Port<Unowned>>,
|
||||
//pub midi_outs: BTreeMap<String, Port<Unowned>>,
|
||||
//}
|
||||
|
||||
//impl JackPorts {
|
||||
//pub fn clone_unowned(&self) -> UnownedJackPorts {
|
||||
//let mut unowned = UnownedJackPorts::default();
|
||||
//for (name, port) in self.midi_ins.iter() {
|
||||
//unowned.midi_ins.insert(name.clone(), port.clone_unowned());
|
||||
//}
|
||||
//for (name, port) in self.midi_outs.iter() {
|
||||
//unowned.midi_outs.insert(name.clone(), port.clone_unowned());
|
||||
//}
|
||||
//for (name, port) in self.audio_ins.iter() {
|
||||
//unowned.audio_ins.insert(name.clone(), port.clone_unowned());
|
||||
//}
|
||||
//for (name, port) in self.audio_outs.iter() {
|
||||
//unowned
|
||||
//.audio_outs
|
||||
//.insert(name.clone(), port.clone_unowned());
|
||||
//}
|
||||
//unowned
|
||||
//}
|
||||
//}
|
||||
|
||||
///// Implement the `Ports` trait.
|
||||
//#[macro_export]
|
||||
//macro_rules! ports {
|
||||
//($T:ty $({ $(audio: {
|
||||
//$(ins: |$ai_arg:ident|$ai_impl:expr,)?
|
||||
//$(outs: |$ao_arg:ident|$ao_impl:expr,)?
|
||||
//})? $(midi: {
|
||||
//$(ins: |$mi_arg:ident|$mi_impl:expr,)?
|
||||
//$(outs: |$mo_arg:ident|$mo_impl:expr,)?
|
||||
//})?})?) => {
|
||||
//impl Ports for $T {$(
|
||||
//$(
|
||||
//$(fn audio_ins <'a> (&'a self) -> Usually<Vec<&'a Port<Unowned>>> {
|
||||
//let cb = |$ai_arg:&'a Self|$ai_impl;
|
||||
//cb(self)
|
||||
//})?
|
||||
//)?
|
||||
//$(
|
||||
//$(fn audio_outs <'a> (&'a self) -> Usually<Vec<&'a Port<Unowned>>> {
|
||||
//let cb = (|$ao_arg:&'a Self|$ao_impl);
|
||||
//cb(self)
|
||||
//})?
|
||||
//)?
|
||||
//)? $(
|
||||
//$(
|
||||
//$(fn midi_ins <'a> (&'a self) -> Usually<Vec<&'a Port<Unowned>>> {
|
||||
//let cb = (|$mi_arg:&'a Self|$mi_impl);
|
||||
//cb(self)
|
||||
//})?
|
||||
//)?
|
||||
//$(
|
||||
//$(fn midi_outs <'a> (&'a self) -> Usually<Vec<&'a Port<Unowned>>> {
|
||||
//let cb = (|$mo_arg:&'a Self|$mo_impl);
|
||||
//cb(self)
|
||||
//})?
|
||||
//)?
|
||||
//)?}
|
||||
//};
|
||||
//}
|
||||
|
||||
///// `JackDevice` factory. Creates JACK `Client`s, performs port registration
|
||||
///// and activation, and encapsulates a `AudioComponent` into a `JackDevice`.
|
||||
//pub struct Jack {
|
||||
//pub client: Client,
|
||||
//pub midi_ins: Vec<String>,
|
||||
//pub audio_ins: Vec<String>,
|
||||
//pub midi_outs: Vec<String>,
|
||||
//pub audio_outs: Vec<String>,
|
||||
//}
|
||||
|
||||
//impl Jack {
|
||||
//pub fn new(name: &str) -> Usually<Self> {
|
||||
//Ok(Self {
|
||||
//midi_ins: vec![],
|
||||
//audio_ins: vec![],
|
||||
//midi_outs: vec![],
|
||||
//audio_outs: vec![],
|
||||
//client: Client::new(name, ClientOptions::NO_START_SERVER)?.0,
|
||||
//})
|
||||
//}
|
||||
//pub fn run<'a: 'static, D, E>(
|
||||
//self,
|
||||
//state: impl FnOnce(JackPorts) -> Box<D>,
|
||||
//) -> Usually<JackDevice<E>>
|
||||
//where
|
||||
//D: AudioComponent<E> + Sized + 'static,
|
||||
//E: Engine + 'static,
|
||||
//{
|
||||
//let owned_ports = JackPorts {
|
||||
//audio_ins: register_ports(&self.client, self.audio_ins, AudioIn::default())?,
|
||||
//audio_outs: register_ports(&self.client, self.audio_outs, AudioOut::default())?,
|
||||
//midi_ins: register_ports(&self.client, self.midi_ins, MidiIn::default())?,
|
||||
//midi_outs: register_ports(&self.client, self.midi_outs, MidiOut::default())?,
|
||||
//};
|
||||
//let midi_outs = owned_ports
|
||||
//.midi_outs
|
||||
//.values()
|
||||
//.map(|p| Ok(p.name()?))
|
||||
//.collect::<Usually<Vec<_>>>()?;
|
||||
//let midi_ins = owned_ports
|
||||
//.midi_ins
|
||||
//.values()
|
||||
//.map(|p| Ok(p.name()?))
|
||||
//.collect::<Usually<Vec<_>>>()?;
|
||||
//let audio_outs = owned_ports
|
||||
//.audio_outs
|
||||
//.values()
|
||||
//.map(|p| Ok(p.name()?))
|
||||
//.collect::<Usually<Vec<_>>>()?;
|
||||
//let audio_ins = owned_ports
|
||||
//.audio_ins
|
||||
//.values()
|
||||
//.map(|p| Ok(p.name()?))
|
||||
//.collect::<Usually<Vec<_>>>()?;
|
||||
//let state = Arc::new(RwLock::new(state(owned_ports) as Box<dyn AudioComponent<E>>));
|
||||
//let client = self.client.activate_async(
|
||||
//Notifications(Box::new({
|
||||
//let _state = state.clone();
|
||||
//move |_event| {
|
||||
//// FIXME: this deadlocks
|
||||
////state.lock().unwrap().handle(&event).unwrap();
|
||||
//}
|
||||
//}) as Box<dyn Fn(JackEvent) + Send + Sync>),
|
||||
//contrib::ClosureProcessHandler::new(Box::new({
|
||||
//let state = state.clone();
|
||||
//move |c: &Client, s: &ProcessScope| state.write().unwrap().process(c, s)
|
||||
//}) as BoxedAudioHandler),
|
||||
//)?;
|
||||
//Ok(JackDevice {
|
||||
//ports: UnownedJackPorts {
|
||||
//audio_ins: query_ports(&client.as_client(), audio_ins),
|
||||
//audio_outs: query_ports(&client.as_client(), audio_outs),
|
||||
//midi_ins: query_ports(&client.as_client(), midi_ins),
|
||||
//midi_outs: query_ports(&client.as_client(), midi_outs),
|
||||
//},
|
||||
//client,
|
||||
//state,
|
||||
//})
|
||||
//}
|
||||
//pub fn audio_in(mut self, name: &str) -> Self {
|
||||
//self.audio_ins.push(name.to_string());
|
||||
//self
|
||||
//}
|
||||
//pub fn audio_out(mut self, name: &str) -> Self {
|
||||
//self.audio_outs.push(name.to_string());
|
||||
//self
|
||||
//}
|
||||
//pub fn midi_in(mut self, name: &str) -> Self {
|
||||
//self.midi_ins.push(name.to_string());
|
||||
//self
|
||||
//}
|
||||
//pub fn midi_out(mut self, name: &str) -> Self {
|
||||
//self.midi_outs.push(name.to_string());
|
||||
//self
|
||||
//}
|
||||
//}
|
||||
|
||||
//impl Command<ArrangerModel> for ArrangerSceneCommand {
|
||||
//}
|
||||
//Edit(phrase) => { state.state.phrase = phrase.clone() },
|
||||
//ToggleViewMode => { state.state.mode.to_next(); },
|
||||
//Delete => { state.state.delete(); },
|
||||
//Activate => { state.state.activate(); },
|
||||
//ZoomIn => { state.state.zoom_in(); },
|
||||
//ZoomOut => { state.state.zoom_out(); },
|
||||
//MoveBack => { state.state.move_back(); },
|
||||
//MoveForward => { state.state.move_forward(); },
|
||||
//RandomColor => { state.state.randomize_color(); },
|
||||
//Put => { state.state.phrase_put(); },
|
||||
//Get => { state.state.phrase_get(); },
|
||||
//AddScene => { state.state.scene_add(None, None)?; },
|
||||
//AddTrack => { state.state.track_add(None, None)?; },
|
||||
//ToggleLoop => { state.state.toggle_loop() },
|
||||
//pub fn zoom_in (&mut self) {
|
||||
//if let ArrangerEditorMode::Vertical(factor) = self.mode {
|
||||
//self.mode = ArrangerEditorMode::Vertical(factor + 1)
|
||||
//}
|
||||
//}
|
||||
//pub fn zoom_out (&mut self) {
|
||||
//if let ArrangerEditorMode::Vertical(factor) = self.mode {
|
||||
//self.mode = ArrangerEditorMode::Vertical(factor.saturating_sub(1))
|
||||
//}
|
||||
//}
|
||||
//pub fn move_back (&mut self) {
|
||||
//match self.selected {
|
||||
//ArrangerEditorFocus::Scene(s) => {
|
||||
//if s > 0 {
|
||||
//self.scenes.swap(s, s - 1);
|
||||
//self.selected = ArrangerEditorFocus::Scene(s - 1);
|
||||
//}
|
||||
//},
|
||||
//ArrangerEditorFocus::Track(t) => {
|
||||
//if t > 0 {
|
||||
//self.tracks.swap(t, t - 1);
|
||||
//self.selected = ArrangerEditorFocus::Track(t - 1);
|
||||
//// FIXME: also swap clip order in scenes
|
||||
//}
|
||||
//},
|
||||
//_ => todo!("arrangement: move forward")
|
||||
//}
|
||||
//}
|
||||
//pub fn move_forward (&mut self) {
|
||||
//match self.selected {
|
||||
//ArrangerEditorFocus::Scene(s) => {
|
||||
//if s < self.scenes.len().saturating_sub(1) {
|
||||
//self.scenes.swap(s, s + 1);
|
||||
//self.selected = ArrangerEditorFocus::Scene(s + 1);
|
||||
//}
|
||||
//},
|
||||
//ArrangerEditorFocus::Track(t) => {
|
||||
//if t < self.tracks.len().saturating_sub(1) {
|
||||
//self.tracks.swap(t, t + 1);
|
||||
//self.selected = ArrangerEditorFocus::Track(t + 1);
|
||||
//// FIXME: also swap clip order in scenes
|
||||
//}
|
||||
//},
|
||||
//_ => todo!("arrangement: move forward")
|
||||
//}
|
||||
//}
|
||||
|
||||
//impl From<Moment> for Clock {
|
||||
//fn from (current: Moment) -> Self {
|
||||
//Self {
|
||||
//playing: Some(TransportState::Stopped).into(),
|
||||
//started: None.into(),
|
||||
//quant: 24.into(),
|
||||
//sync: (current.timebase.ppq.get() * 4.).into(),
|
||||
//current,
|
||||
//}
|
||||
//}
|
||||
//}
|
||||
172
crates/tek/src/api/phrase.rs
Normal file
172
crates/tek/src/api/phrase.rs
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
use crate::*;
|
||||
|
||||
pub trait HasPhrases {
|
||||
fn phrases (&self) -> &Vec<Arc<RwLock<Phrase>>>;
|
||||
fn phrases_mut (&mut self) -> &mut Vec<Arc<RwLock<Phrase>>>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum PhrasePoolCommand {
|
||||
Add(usize, Phrase),
|
||||
Delete(usize),
|
||||
Swap(usize, usize),
|
||||
Import(usize, PathBuf),
|
||||
Export(usize, PathBuf),
|
||||
SetName(usize, String),
|
||||
SetLength(usize, usize),
|
||||
SetColor(usize, ItemColor),
|
||||
}
|
||||
|
||||
impl<T: HasPhrases> Command<T> for PhrasePoolCommand {
|
||||
fn execute (self, model: &mut T) -> Perhaps<Self> {
|
||||
use PhrasePoolCommand::*;
|
||||
Ok(match self {
|
||||
Add(mut index, phrase) => {
|
||||
let phrase = Arc::new(RwLock::new(phrase));
|
||||
let phrases = model.phrases_mut();
|
||||
if index >= phrases.len() {
|
||||
index = phrases.len();
|
||||
phrases.push(phrase)
|
||||
} else {
|
||||
phrases.insert(index, phrase);
|
||||
}
|
||||
Some(Self::Delete(index))
|
||||
},
|
||||
Delete(index) => {
|
||||
let phrase = model.phrases_mut().remove(index).read().unwrap().clone();
|
||||
Some(Self::Add(index, phrase))
|
||||
},
|
||||
Swap(index, other) => {
|
||||
model.phrases_mut().swap(index, other);
|
||||
Some(Self::Swap(index, other))
|
||||
},
|
||||
Import(index, path) => {
|
||||
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 phrase = Phrase::new("imported", true, t as usize + 1, None, None);
|
||||
for event in events.iter() {
|
||||
phrase.notes[event.0 as usize].push(event.2);
|
||||
}
|
||||
Self::Add(index, phrase).execute(model)?
|
||||
},
|
||||
Export(_index, _path) => {
|
||||
todo!("export phrase to midi file");
|
||||
},
|
||||
SetName(index, name) => {
|
||||
let mut phrase = model.phrases()[index].write().unwrap();
|
||||
let old_name = phrase.name.clone();
|
||||
phrase.name = name;
|
||||
Some(Self::SetName(index, old_name))
|
||||
},
|
||||
SetLength(index, length) => {
|
||||
let mut phrase = model.phrases()[index].write().unwrap();
|
||||
let old_len = phrase.length;
|
||||
phrase.length = length;
|
||||
Some(Self::SetLength(index, old_len))
|
||||
},
|
||||
SetColor(index, color) => {
|
||||
let mut color = ItemColorTriplet::from(color);
|
||||
std::mem::swap(&mut color, &mut model.phrases()[index].write().unwrap().color);
|
||||
Some(Self::SetColor(index, color.base))
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A MIDI sequence.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Phrase {
|
||||
pub uuid: uuid::Uuid,
|
||||
/// Name of phrase
|
||||
pub name: String,
|
||||
/// Temporal resolution in pulses per quarter note
|
||||
pub ppq: usize,
|
||||
/// Length of phrase in pulses
|
||||
pub length: usize,
|
||||
/// Notes in phrase
|
||||
pub notes: PhraseData,
|
||||
/// Whether to loop the phrase or play it once
|
||||
pub loop_on: 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 phrase
|
||||
pub color: ItemColorTriplet,
|
||||
}
|
||||
|
||||
/// MIDI message structural
|
||||
pub type PhraseData = Vec<Vec<MidiMessage>>;
|
||||
|
||||
impl Phrase {
|
||||
pub fn new (
|
||||
name: impl AsRef<str>,
|
||||
loop_on: bool,
|
||||
length: usize,
|
||||
notes: Option<PhraseData>,
|
||||
color: Option<ItemColorTriplet>,
|
||||
) -> Self {
|
||||
Self {
|
||||
uuid: uuid::Uuid::new_v4(),
|
||||
name: name.as_ref().to_string(),
|
||||
ppq: PPQ,
|
||||
length,
|
||||
notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]),
|
||||
loop_on,
|
||||
loop_start: 0,
|
||||
loop_length: length,
|
||||
percussive: true,
|
||||
color: color.unwrap_or_else(ItemColorTriplet::random)
|
||||
}
|
||||
}
|
||||
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.loop_on = !self.loop_on; }
|
||||
pub fn record_event (&mut self, pulse: usize, message: MidiMessage) {
|
||||
if pulse >= self.length { panic!("extend phrase 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 {
|
||||
//panic!("{:?} {start} {end}", &self);
|
||||
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 } }
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Phrase {
|
||||
fn default () -> Self {
|
||||
Self::new("(empty)", false, 0, None, Some(ItemColor::from(Color::Rgb(0, 0, 0)).into()))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Phrase {
|
||||
fn eq (&self, other: &Self) -> bool {
|
||||
self.uuid == other.uuid
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Phrase {}
|
||||
362
crates/tek/src/api/player.rs
Normal file
362
crates/tek/src/api/player.rs
Normal file
|
|
@ -0,0 +1,362 @@
|
|||
use crate::*;
|
||||
|
||||
pub trait HasPlayer {
|
||||
fn player (&self) -> &impl MidiPlayerApi;
|
||||
fn player_mut (&mut self) -> &mut impl MidiPlayerApi;
|
||||
}
|
||||
|
||||
pub trait MidiPlayerApi: MidiRecordApi + MidiPlaybackApi + Send + Sync {}
|
||||
|
||||
pub trait HasPlayPhrase: HasClock {
|
||||
fn reset (&self) -> bool;
|
||||
fn reset_mut (&mut self) -> &mut bool;
|
||||
fn play_phrase (&self) -> &Option<(Moment, Option<Arc<RwLock<Phrase>>>)>;
|
||||
fn play_phrase_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<Phrase>>>)>;
|
||||
fn next_phrase (&self) -> &Option<(Moment, Option<Arc<RwLock<Phrase>>>)>;
|
||||
fn next_phrase_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<Phrase>>>)>;
|
||||
fn pulses_since_start (&self) -> Option<f64> {
|
||||
if let Some((started, Some(_))) = self.play_phrase().as_ref() {
|
||||
Some(self.clock().playhead.pulse.get() - started.pulse.get())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
fn enqueue_next (&mut self, phrase: Option<&Arc<RwLock<Phrase>>>) {
|
||||
let start = self.clock().next_launch_pulse() as f64;
|
||||
let instant = Moment::from_pulse(&self.clock().timebase(), start);
|
||||
let phrase = phrase.map(|p|p.clone());
|
||||
*self.next_phrase_mut() = Some((instant, phrase));
|
||||
*self.reset_mut() = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub trait MidiRecordApi: HasClock + HasPlayPhrase + HasMidiIns {
|
||||
fn notes_in (&self) -> &Arc<RwLock<[bool;128]>>;
|
||||
|
||||
fn recording (&self) -> bool;
|
||||
fn recording_mut (&mut self) -> &mut bool;
|
||||
fn toggle_record (&mut self) {
|
||||
*self.recording_mut() = !self.recording();
|
||||
}
|
||||
fn record (&mut self, scope: &ProcessScope, midi_buf: &mut Vec<Vec<Vec<u8>>>) {
|
||||
let sample0 = scope.last_frame_time() as usize;
|
||||
// For highlighting keys and note repeat
|
||||
let notes_in = self.notes_in().clone();
|
||||
if self.clock().is_rolling() {
|
||||
if let Some((started, ref phrase)) = self.play_phrase().clone() {
|
||||
let start = started.sample.get() as usize;
|
||||
let quant = self.clock().quant.get();
|
||||
let timebase = self.clock().timebase().clone();
|
||||
let monitoring = self.monitoring();
|
||||
let recording = self.recording();
|
||||
for input in self.midi_ins_mut().iter() {
|
||||
for (sample, event, bytes) in parse_midi_input(input.iter(scope)) {
|
||||
if let LiveEvent::Midi { message, .. } = event {
|
||||
if monitoring {
|
||||
midi_buf[sample].push(bytes.to_vec())
|
||||
}
|
||||
if recording {
|
||||
if let Some(phrase) = phrase {
|
||||
let mut phrase = phrase.write().unwrap();
|
||||
let length = phrase.length;
|
||||
phrase.record_event({
|
||||
let sample = (sample0 + sample - start) as f64;
|
||||
let pulse = timebase.samples_to_pulse(sample);
|
||||
let quantized = (pulse / quant).round() * quant;
|
||||
let looped = quantized as usize % length;
|
||||
looped
|
||||
}, message);
|
||||
}
|
||||
}
|
||||
update_keys(&mut*notes_in.write().unwrap(), &message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((start_at, phrase)) = &self.next_phrase() {
|
||||
// TODO switch to next phrase and record into it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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, midi_buf: &mut Vec<Vec<Vec<u8>>>) {
|
||||
// For highlighting keys and note repeat
|
||||
let notes_in = self.notes_in().clone();
|
||||
for input in self.midi_ins_mut().iter() {
|
||||
for (sample, event, bytes) in parse_midi_input(input.iter(scope)) {
|
||||
if let LiveEvent::Midi { message, .. } = event {
|
||||
midi_buf[sample].push(bytes.to_vec());
|
||||
update_keys(&mut*notes_in.write().unwrap(), &message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn overdub (&self) -> bool;
|
||||
fn overdub_mut (&mut self) -> &mut bool;
|
||||
fn toggle_overdub (&mut self) {
|
||||
*self.overdub_mut() = !self.overdub();
|
||||
}
|
||||
}
|
||||
|
||||
pub trait MidiPlaybackApi: HasPlayPhrase + HasClock + HasMidiOuts {
|
||||
|
||||
fn notes_out (&self) -> &Arc<RwLock<[bool;128]>>;
|
||||
|
||||
/// Clear the section of the output buffer that we will be using,
|
||||
/// emitting "all notes off" at start of buffer if requested.
|
||||
fn clear (
|
||||
&mut self, scope: &ProcessScope, out_buf: &mut Vec<Vec<Vec<u8>>>, reset: bool
|
||||
) {
|
||||
for frame in &mut out_buf[0..scope.n_frames() as usize] {
|
||||
frame.clear();
|
||||
}
|
||||
if reset {
|
||||
all_notes_off(out_buf);
|
||||
}
|
||||
}
|
||||
|
||||
/// Output notes from phrase to MIDI output ports.
|
||||
fn play (
|
||||
&mut self, scope: &ProcessScope, note_buf: &mut Vec<u8>, out_buf: &mut Vec<Vec<Vec<u8>>>
|
||||
) -> bool {
|
||||
let mut next = false;
|
||||
// Write MIDI events from currently playing phrase (if any) to MIDI output buffer
|
||||
if self.clock().is_rolling() {
|
||||
let sample0 = scope.last_frame_time() as usize;
|
||||
let samples = scope.n_frames() as usize;
|
||||
// If no phrase is playing, prepare for switchover immediately
|
||||
next = self.play_phrase().is_none();
|
||||
let phrase = self.play_phrase();
|
||||
let started0 = &self.clock().started;
|
||||
let timebase = self.clock().timebase();
|
||||
let notes_out = self.notes_out();
|
||||
let next_phrase = self.next_phrase();
|
||||
if let Some((started, phrase)) = phrase {
|
||||
// First sample to populate. Greater than 0 means that the first
|
||||
// pulse of the phrase falls somewhere in the middle of the chunk.
|
||||
let sample = started.sample.get() as usize;
|
||||
let sample = sample + started0.read().unwrap().as_ref().unwrap().sample.get() as usize;
|
||||
let sample = sample0.saturating_sub(sample);
|
||||
// Iterator that emits sample (index into output buffer at which to write MIDI event)
|
||||
// paired with pulse (index into phrase from which to take the MIDI event) for each
|
||||
// sample of the output buffer that corresponds to a MIDI pulse.
|
||||
let pulses = timebase.pulses_between_samples(sample, sample + samples);
|
||||
// Notes active during current chunk.
|
||||
let notes = &mut notes_out.write().unwrap();
|
||||
for (sample, pulse) in pulses {
|
||||
// If a next phrase is enqueued, and we're past the end of the current one,
|
||||
// break the loop here (FIXME count pulse correctly)
|
||||
next = next_phrase.is_some() && if let Some(ref phrase) = phrase {
|
||||
pulse >= phrase.read().unwrap().length
|
||||
} else {
|
||||
true
|
||||
};
|
||||
if next {
|
||||
break
|
||||
}
|
||||
// If there's a currently playing phrase, output notes from it to buffer:
|
||||
if let Some(ref phrase) = phrase {
|
||||
// Source phrase from which the MIDI events will be taken.
|
||||
let phrase = phrase.read().unwrap();
|
||||
// Phrase with zero length is not processed
|
||||
if phrase.length > 0 {
|
||||
// Current pulse index in source phrase
|
||||
let pulse = pulse % phrase.length;
|
||||
// Output each MIDI event from phrase at appropriate frames of output buffer:
|
||||
for message in phrase.notes[pulse].iter() {
|
||||
// Clear output buffer for this MIDI event.
|
||||
note_buf.clear();
|
||||
// TODO: support MIDI channels other than CH1.
|
||||
let channel = 0.into();
|
||||
// Serialize MIDI event into message buffer.
|
||||
LiveEvent::Midi { channel, message: *message }
|
||||
.write(note_buf)
|
||||
.unwrap();
|
||||
// Append serialized message to output buffer.
|
||||
out_buf[sample].push(note_buf.clone());
|
||||
// Update the list of currently held notes.
|
||||
update_keys(&mut*notes, &message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
next
|
||||
}
|
||||
|
||||
/// Handle switchover from current to next playing phrase.
|
||||
fn switchover (
|
||||
&mut self, scope: &ProcessScope, note_buf: &mut Vec<u8>, out_buf: &mut Vec<Vec<Vec<u8>>>
|
||||
) {
|
||||
if self.clock().is_rolling() {
|
||||
let sample0 = scope.last_frame_time() as usize;
|
||||
//let samples = scope.n_frames() as usize;
|
||||
if let Some((start_at, phrase)) = &self.next_phrase() {
|
||||
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 phrase:
|
||||
if start <= sample0.saturating_sub(sample) {
|
||||
// Samples elapsed since phrase was supposed to start
|
||||
let skipped = sample0 - start;
|
||||
// Switch over to enqueued phrase
|
||||
let started = Moment::from_sample(&self.clock().timebase(), start as f64);
|
||||
*self.play_phrase_mut() = Some((started, phrase.clone()));
|
||||
// Unset enqueuement (TODO: where to implement looping?)
|
||||
*self.next_phrase_mut() = None
|
||||
}
|
||||
// TODO fill in remaining ticks of chunk from next phrase.
|
||||
// ?? just call self.play(scope) again, since enqueuement is off ???
|
||||
self.play(scope, note_buf, out_buf);
|
||||
// ?? or must it be with modified scope ??
|
||||
// likely not because start time etc
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a chunk of MIDI notes to the output buffer.
|
||||
fn write (
|
||||
&mut self, scope: &ProcessScope, out_buf: &Vec<Vec<Vec<u8>>>
|
||||
) {
|
||||
let samples = scope.n_frames() as usize;
|
||||
for port in self.midi_outs_mut().iter_mut() {
|
||||
let writer = &mut port.writer(scope);
|
||||
for time in 0..samples {
|
||||
for event in out_buf[time].iter() {
|
||||
writer.write(&RawMidi { time: time as u32, bytes: &event })
|
||||
.expect(&format!("{event:?}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Add "all notes off" to the start of a buffer.
|
||||
pub fn all_notes_off (output: &mut [Vec<Vec<u8>>]) {
|
||||
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);
|
||||
}
|
||||
|
||||
/// Return boxed iterator of MIDI events
|
||||
pub fn parse_midi_input (input: MidiIter) -> Box<dyn Iterator<Item=(usize, LiveEvent, &[u8])> + '_> {
|
||||
Box::new(input.map(|RawMidi { time, bytes }|(
|
||||
time as usize,
|
||||
LiveEvent::parse(bytes).unwrap(),
|
||||
bytes
|
||||
)))
|
||||
}
|
||||
|
||||
/// 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; },
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Hosts the JACK callback for a single MIDI player
|
||||
pub struct PlayerAudio<'a, T: MidiPlayerApi>(
|
||||
/// Player
|
||||
pub &'a mut T,
|
||||
/// Note buffer
|
||||
pub &'a mut Vec<u8>,
|
||||
/// Note chunk buffer
|
||||
pub &'a mut Vec<Vec<Vec<u8>>>,
|
||||
);
|
||||
|
||||
/// JACK process callback for a sequencer's phrase player/recorder.
|
||||
impl<'a, T: MidiPlayerApi> Audio for PlayerAudio<'a, T> {
|
||||
fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
|
||||
let model = &mut self.0;
|
||||
let note_buf = &mut self.1;
|
||||
let midi_buf = &mut self.2;
|
||||
// Clear output buffer(s)
|
||||
model.clear(scope, midi_buf, false);
|
||||
// Write chunk of phrase to output, handle switchover
|
||||
if model.play(scope, note_buf, midi_buf) {
|
||||
model.switchover(scope, note_buf, midi_buf);
|
||||
}
|
||||
if model.has_midi_ins() {
|
||||
if model.recording() || model.monitoring() {
|
||||
// Record and/or monitor input
|
||||
model.record(scope, midi_buf)
|
||||
} else if model.has_midi_outs() && model.monitoring() {
|
||||
// Monitor input to output
|
||||
model.monitor(scope, midi_buf)
|
||||
}
|
||||
}
|
||||
// Write to output port(s)
|
||||
model.write(scope, midi_buf);
|
||||
Control::Continue
|
||||
}
|
||||
}
|
||||
|
||||
//#[derive(Debug)]
|
||||
//pub struct MIDIPlayer {
|
||||
///// Global timebase
|
||||
//pub clock: Arc<Clock>,
|
||||
///// Start time and phrase being played
|
||||
//pub play_phrase: Option<(Moment, Option<Arc<RwLock<Phrase>>>)>,
|
||||
///// Start time and next phrase
|
||||
//pub next_phrase: Option<(Moment, Option<Arc<RwLock<Phrase>>>)>,
|
||||
///// 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<Port<MidiIn>>,
|
||||
///// Play from current sequence to MIDI ports
|
||||
//pub midi_outputs: Vec<Port<MidiOut>>,
|
||||
///// MIDI output buffer
|
||||
//pub midi_note: Vec<u8>,
|
||||
///// MIDI output buffer
|
||||
//pub midi_chunk: Vec<Vec<Vec<u8>>>,
|
||||
///// Notes currently held at input
|
||||
//pub notes_in: Arc<RwLock<[bool; 128]>>,
|
||||
///// Notes currently held at output
|
||||
//pub notes_out: Arc<RwLock<[bool; 128]>>,
|
||||
//}
|
||||
|
||||
///// Methods used primarily by the process callback
|
||||
//impl MIDIPlayer {
|
||||
//pub fn new (
|
||||
//jack: &Arc<RwLock<JackClient>>,
|
||||
//clock: &Arc<Clock>,
|
||||
//name: &str
|
||||
//) -> Usually<Self> {
|
||||
//let jack = jack.read().unwrap();
|
||||
//Ok(Self {
|
||||
//clock: clock.clone(),
|
||||
//phrase: None,
|
||||
//next_phrase: 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())?
|
||||
//],
|
||||
//})
|
||||
//}
|
||||
//}
|
||||
127
crates/tek/src/api/scene.rs
Normal file
127
crates/tek/src/api/scene.rs
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
use crate::*;
|
||||
|
||||
pub trait HasScenes<S: ArrangerSceneApi> {
|
||||
fn scenes (&self) -> &Vec<S>;
|
||||
fn scenes_mut (&mut self) -> &mut Vec<S>;
|
||||
fn scene_add (&mut self, name: Option<&str>, color: Option<ItemColor>) -> Usually<&mut S>;
|
||||
fn scene_del (&mut self, index: usize) {
|
||||
self.scenes_mut().remove(index);
|
||||
}
|
||||
fn scene_default_name (&self) -> String {
|
||||
format!("Scene {}", self.scenes().len() + 1)
|
||||
}
|
||||
fn selected_scene (&self) -> Option<&S> {
|
||||
None
|
||||
}
|
||||
fn selected_scene_mut (&mut self) -> Option<&mut S> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ArrangerSceneCommand {
|
||||
Add,
|
||||
Delete(usize),
|
||||
RandomColor,
|
||||
Play(usize),
|
||||
Swap(usize, usize),
|
||||
SetSize(usize),
|
||||
SetZoom(usize),
|
||||
}
|
||||
|
||||
//impl<T: ArrangerApi> Command<T> for ArrangerSceneCommand {
|
||||
//fn execute (self, state: &mut T) -> Perhaps<Self> {
|
||||
//match self {
|
||||
//Self::Delete(index) => { state.scene_del(index); },
|
||||
//_ => todo!()
|
||||
//}
|
||||
//Ok(None)
|
||||
//}
|
||||
//}
|
||||
|
||||
pub trait ArrangerSceneApi: Sized {
|
||||
fn name (&self) -> &Arc<RwLock<String>>;
|
||||
fn clips (&self) -> &Vec<Option<Arc<RwLock<Phrase>>>>;
|
||||
fn color (&self) -> ItemColor;
|
||||
|
||||
fn ppqs (scenes: &[Self], factor: usize) -> Vec<(usize, usize)> {
|
||||
let mut total = 0;
|
||||
if factor == 0 {
|
||||
scenes.iter().map(|scene|{
|
||||
let pulses = scene.pulses().max(PPQ);
|
||||
total = total + pulses;
|
||||
(pulses, total - pulses)
|
||||
}).collect()
|
||||
} else {
|
||||
(0..=scenes.len()).map(|i|{
|
||||
(factor*PPQ, factor*PPQ*i)
|
||||
}).collect()
|
||||
}
|
||||
}
|
||||
|
||||
fn longest_name (scenes: &[Self]) -> usize {
|
||||
scenes.iter().map(|s|s.name().read().unwrap().len()).fold(0, usize::max)
|
||||
}
|
||||
|
||||
/// Returns the pulse length of the longest phrase in the scene
|
||||
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 phrases in the scene are
|
||||
/// currently playing on the given collection of tracks.
|
||||
fn is_playing <T: ArrangerTrackApi> (&self, tracks: &[T]) -> bool {
|
||||
self.clips().iter().any(|clip|clip.is_some()) && self.clips().iter().enumerate()
|
||||
.all(|(track_index, clip)|match clip {
|
||||
Some(clip) => tracks
|
||||
.get(track_index)
|
||||
.map(|track|{
|
||||
if let Some((_, Some(phrase))) = track.player().play_phrase() {
|
||||
*phrase.read().unwrap() == *clip.read().unwrap()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.unwrap_or(false),
|
||||
None => true
|
||||
})
|
||||
}
|
||||
|
||||
fn clip (&self, index: usize) -> Option<&Arc<RwLock<Phrase>>> {
|
||||
match self.clips().get(index) { Some(Some(clip)) => Some(clip), _ => None }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//impl ArrangerScene {
|
||||
|
||||
////TODO
|
||||
////pub fn from_edn <'a, 'e> (args: &[Edn<'e>]) -> Usually<Self> {
|
||||
////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,
|
||||
////})
|
||||
////}
|
||||
//}
|
||||
87
crates/tek/src/api/track.rs
Normal file
87
crates/tek/src/api/track.rs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
use crate::*;
|
||||
|
||||
pub trait HasTracks<T: ArrangerTrackApi>: Send + Sync {
|
||||
fn tracks (&self) -> &Vec<T>;
|
||||
fn tracks_mut (&mut self) -> &mut Vec<T>;
|
||||
}
|
||||
|
||||
impl<T: ArrangerTrackApi> HasTracks<T> for Vec<T> {
|
||||
fn tracks (&self) -> &Vec<T> {
|
||||
self
|
||||
}
|
||||
fn tracks_mut (&mut self) -> &mut Vec<T> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ArrangerTracksApi<T: ArrangerTrackApi>: HasTracks<T> {
|
||||
fn track_add (&mut self, name: Option<&str>, color: Option<ItemColor>)-> Usually<&mut T>;
|
||||
fn track_del (&mut self, index: usize);
|
||||
fn track_default_name (&self) -> String {
|
||||
format!("Track {}", self.tracks().len() + 1)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ArrangerTrackCommand {
|
||||
Add,
|
||||
Delete(usize),
|
||||
RandomColor,
|
||||
Stop,
|
||||
Swap(usize, usize),
|
||||
SetSize(usize),
|
||||
SetZoom(usize),
|
||||
}
|
||||
|
||||
pub trait ArrangerTrackApi: HasPlayer + Send + Sync + Sized {
|
||||
/// Name of track
|
||||
fn name (&self) -> &Arc<RwLock<String>>;
|
||||
/// Preferred width of track column
|
||||
fn width (&self) -> usize;
|
||||
/// Preferred width of track column
|
||||
fn width_mut (&mut self) -> &mut usize;
|
||||
/// Identifying color of track
|
||||
fn color (&self) -> ItemColor;
|
||||
|
||||
fn longest_name (tracks: &[Self]) -> usize {
|
||||
tracks.iter().map(|s|s.name().read().unwrap().len()).fold(0, usize::max)
|
||||
}
|
||||
|
||||
const MIN_WIDTH: usize = 3;
|
||||
|
||||
fn width_inc (&mut self) {
|
||||
*self.width_mut() += 1;
|
||||
}
|
||||
|
||||
fn width_dec (&mut self) {
|
||||
if self.width() > Self::MIN_WIDTH {
|
||||
*self.width_mut() -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Hosts the JACK callback for a collection of tracks
|
||||
pub struct TracksAudio<'a, T: ArrangerTrackApi, H: HasTracks<T>>(
|
||||
// Track collection
|
||||
pub &'a mut H,
|
||||
/// Note buffer
|
||||
pub &'a mut Vec<u8>,
|
||||
/// Note chunk buffer
|
||||
pub &'a mut Vec<Vec<Vec<u8>>>,
|
||||
/// Marker
|
||||
pub PhantomData<T>,
|
||||
);
|
||||
|
||||
impl<'a, T: ArrangerTrackApi, H: HasTracks<T>> Audio for TracksAudio<'a, T, H> {
|
||||
#[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
||||
let model = &mut self.0;
|
||||
let note_buffer = &mut self.1;
|
||||
let output_buffer = &mut self.2;
|
||||
for track in model.tracks_mut().iter_mut() {
|
||||
if PlayerAudio(track.player_mut(), note_buffer, output_buffer).process(client, scope) == Control::Quit {
|
||||
return Control::Quit
|
||||
}
|
||||
}
|
||||
Control::Continue
|
||||
}
|
||||
}
|
||||
49
crates/tek/src/cli/cli_arranger.rs
Normal file
49
crates/tek/src/cli/cli_arranger.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
include!("../lib.rs");
|
||||
|
||||
pub fn main () -> Usually<()> {
|
||||
ArrangerCli::parse().run()
|
||||
}
|
||||
|
||||
/// Parses CLI arguments to the `tek_arranger` invocation.
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(version, about, long_about = None)]
|
||||
pub struct ArrangerCli {
|
||||
/// Name of JACK client
|
||||
#[arg(short, long)] name: Option<String>,
|
||||
/// 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 = 8)] tracks: usize,
|
||||
/// Number of scenes
|
||||
#[arg(short, long, default_value_t = 8)] scenes: usize,
|
||||
}
|
||||
|
||||
impl ArrangerCli {
|
||||
/// Run the arranger TUI from CLI arguments.
|
||||
fn run (&self) -> Usually<()> {
|
||||
Tui::run(JackClient::new("tek_arranger")?.activate_with(|jack|{
|
||||
let mut app = ArrangerTui::try_from(jack)?;
|
||||
if let Some(name) = self.name.as_ref() {
|
||||
*app.name.write().unwrap() = name.clone();
|
||||
}
|
||||
let track_color_1 = ItemColor::random();
|
||||
let track_color_2 = ItemColor::random();
|
||||
for i in 0..self.tracks {
|
||||
let _track = app.track_add(
|
||||
None,
|
||||
Some(track_color_1.mix(track_color_2, i as f32 / self.tracks as f32))
|
||||
)?;
|
||||
}
|
||||
let scene_color_1 = ItemColor::random();
|
||||
let scene_color_2 = ItemColor::random();
|
||||
for i in 0..self.scenes {
|
||||
let _scene = app.scene_add(
|
||||
None,
|
||||
Some(scene_color_1.mix(scene_color_2, i as f32 / self.scenes as f32))
|
||||
)?;
|
||||
}
|
||||
Ok(app)
|
||||
})?)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
45
crates/tek/src/cli/cli_sequencer.rs
Normal file
45
crates/tek/src/cli/cli_sequencer.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
include!("../lib.rs");
|
||||
|
||||
pub fn main () -> Usually<()> {
|
||||
SequencerCli::parse().run()
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(version, about, long_about = None)]
|
||||
pub struct SequencerCli {
|
||||
/// Name of JACK client
|
||||
#[arg(short, long)] name: Option<String>,
|
||||
/// Pulses per quarter note (sequencer resolution; default: 96)
|
||||
#[arg(short, long)] ppq: Option<usize>,
|
||||
/// Default phrase duration (in pulses; default: 4 * PPQ = 1 bar)
|
||||
#[arg(short, long)] length: Option<usize>,
|
||||
/// Whether to include a transport toolbar (default: true)
|
||||
#[arg(short, long, default_value_t = true)] transport: bool
|
||||
}
|
||||
|
||||
impl SequencerCli {
|
||||
fn run (&self) -> Usually<()> {
|
||||
Tui::run(JackClient::new("tek_sequencer")?.activate_with(|jack|{
|
||||
let mut app = SequencerTui::try_from(jack)?;
|
||||
//app.editor.view_mode.set_time_zoom(1);
|
||||
// TODO: create from arguments
|
||||
let midi_in = app.jack.read().unwrap().register_port("in", MidiIn::default())?;
|
||||
app.player.midi_ins.push(midi_in);
|
||||
let midi_out = app.jack.read().unwrap().register_port("out", MidiOut::default())?;
|
||||
app.player.midi_outs.push(midi_out);
|
||||
if let Some(_) = self.name.as_ref() {
|
||||
// TODO: sequencer.name = Arc::new(RwLock::new(name.clone()));
|
||||
}
|
||||
if let Some(_) = self.ppq {
|
||||
// TODO: sequencer.ppq = ppq;
|
||||
}
|
||||
if let Some(_) = self.length {
|
||||
// TODO: if let Some(phrase) = sequencer.phrase.as_mut() {
|
||||
//phrase.write().unwrap().length = length;
|
||||
//}
|
||||
}
|
||||
Ok(app)
|
||||
})?)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
9
crates/tek/src/cli/cli_transport.rs
Normal file
9
crates/tek/src/cli/cli_transport.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
include!("../lib.rs");
|
||||
|
||||
/// Application entrypoint.
|
||||
pub fn main () -> Usually<()> {
|
||||
Tui::run(JackClient::new("tek_transport")?.activate_with(|jack|{
|
||||
TransportTui::try_from(jack)
|
||||
})?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
include!("./lib.rs");
|
||||
use crate::*;
|
||||
|
||||
pub fn main () -> Usually<()> {
|
||||
MixerCli::parse().run()
|
||||
|
|
@ -13,7 +13,7 @@ pub fn main () -> Usually<()> {
|
|||
|
||||
impl MixerCli {
|
||||
fn run (&self) -> Usually<()> {
|
||||
Tui::run(JackConnection::new("tek_mixer")?.activate_with(|jack|{
|
||||
Tui::run(JackClient::new("tek_mixer")?.activate_with(|jack|{
|
||||
let mut mixer = Mixer::new(jack, self.name.as_ref().map(|x|x.as_str()).unwrap_or("mixer"))?;
|
||||
for channel in 0..self.channels.unwrap_or(8) {
|
||||
mixer.track_add(&format!("Track {}", channel + 1), 1)?;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
include!("./lib.rs");
|
||||
use crate::*;
|
||||
|
||||
pub fn main () -> Usually<()> {
|
||||
PluginCli::parse().run()
|
||||
|
|
@ -13,7 +13,7 @@ pub fn main () -> Usually<()> {
|
|||
|
||||
impl PluginCli {
|
||||
fn run (&self) -> Usually<()> {
|
||||
Tui::run(JackConnection::new("tek_plugin")?.activate_with(|jack|{
|
||||
Tui::run(JackClient::new("tek_plugin")?.activate_with(|jack|{
|
||||
let mut plugin = Plugin::new_lv2(
|
||||
jack,
|
||||
self.name.as_ref().map(|x|x.as_str()).unwrap_or("mixer"),
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
include!("./lib.rs");
|
||||
use crate::*;
|
||||
|
||||
pub fn main () -> Usually<()> {
|
||||
SamplerCli::parse().run()
|
||||
|
|
@ -13,7 +13,7 @@ pub fn main () -> Usually<()> {
|
|||
|
||||
impl SamplerCli {
|
||||
fn run (&self) -> Usually<()> {
|
||||
Tui::run(JackConnection::new("tek_sampler")?.activate_with(|jack|{
|
||||
Tui::run(JackClient::new("tek_sampler")?.activate_with(|jack|{
|
||||
let mut plugin = Sampler::new(
|
||||
jack,
|
||||
self.name.as_ref().map(|x|x.as_str()).unwrap_or("mixer"),
|
||||
13
crates/tek/src/core.rs
Normal file
13
crates/tek/src/core.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
use crate::*;
|
||||
|
||||
mod audio; pub(crate) use audio::*;
|
||||
mod color; pub(crate) use color::*;
|
||||
mod command; pub(crate) use command::*;
|
||||
mod edn; pub(crate) use edn::*;
|
||||
mod engine; pub(crate) use engine::*;
|
||||
mod focus; pub(crate) use focus::*;
|
||||
mod input; pub(crate) use input::*;
|
||||
mod output; pub(crate) use output::*;
|
||||
mod pitch; pub(crate) use pitch::*;
|
||||
mod space; pub(crate) use space::*;
|
||||
mod time; pub(crate) use time::*;
|
||||
181
crates/tek/src/core/audio.rs
Normal file
181
crates/tek/src/core/audio.rs
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
use crate::*;
|
||||
use jack::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Event enum for JACK events.
|
||||
pub enum JackEvent {
|
||||
ThreadInit,
|
||||
Shutdown(ClientStatus, String),
|
||||
Freewheel(bool),
|
||||
SampleRate(Frames),
|
||||
ClientRegistration(String, bool),
|
||||
PortRegistration(PortId, bool),
|
||||
PortRename(PortId, String, String),
|
||||
PortsConnected(PortId, PortId, bool),
|
||||
GraphReorder,
|
||||
XRun,
|
||||
}
|
||||
|
||||
/// Wraps [Client] or [DynamicAsyncClient] in place.
|
||||
#[derive(Debug)]
|
||||
pub enum JackClient {
|
||||
/// Before activation.
|
||||
Inactive(Client),
|
||||
/// During activation.
|
||||
Activating,
|
||||
/// After activation. Must not be dropped for JACK thread to persist.
|
||||
Active(DynamicAsyncClient),
|
||||
}
|
||||
|
||||
/// Trait for things that wrap a JACK client.
|
||||
pub trait AudioEngine {
|
||||
|
||||
fn transport (&self) -> Transport {
|
||||
self.client().transport()
|
||||
}
|
||||
|
||||
fn port_by_name (&self, name: &str) -> Option<Port<Unowned>> {
|
||||
self.client().port_by_name(name)
|
||||
}
|
||||
|
||||
fn register_port <PS: PortSpec> (&self, name: &str, spec: PS) -> Usually<Port<PS>> {
|
||||
Ok(self.client().register_port(name, spec)?)
|
||||
}
|
||||
|
||||
fn client (&self) -> &Client;
|
||||
|
||||
fn activate (
|
||||
self,
|
||||
process: impl FnMut(&Arc<RwLock<Self>>, &Client, &ProcessScope) -> Control + Send + 'static
|
||||
) -> Usually<Arc<RwLock<Self>>> where Self: Send + Sync + 'static;
|
||||
|
||||
fn thread_init (&self, _: &Client) {}
|
||||
|
||||
unsafe fn shutdown (&mut self, status: ClientStatus, reason: &str) {}
|
||||
|
||||
fn freewheel (&mut self, _: &Client, enabled: bool) {}
|
||||
|
||||
fn client_registration (&mut self, _: &Client, name: &str, reg: bool) {}
|
||||
|
||||
fn port_registration (&mut self, _: &Client, id: PortId, reg: bool) {}
|
||||
|
||||
fn ports_connected (&mut self, _: &Client, a: PortId, b: PortId, are: bool) {}
|
||||
|
||||
fn sample_rate (&mut self, _: &Client, frames: Frames) -> Control {
|
||||
Control::Continue
|
||||
}
|
||||
|
||||
fn port_rename (&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control {
|
||||
Control::Continue
|
||||
}
|
||||
|
||||
fn graph_reorder (&mut self, _: &Client) -> Control {
|
||||
Control::Continue
|
||||
}
|
||||
|
||||
fn xrun (&mut self, _: &Client) -> Control {
|
||||
Control::Continue
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioEngine for JackClient {
|
||||
fn client(&self) -> &Client {
|
||||
match self {
|
||||
Self::Inactive(ref client) => client,
|
||||
Self::Activating => panic!("jack client has not finished activation"),
|
||||
Self::Active(ref client) => client.as_client(),
|
||||
}
|
||||
}
|
||||
fn activate(
|
||||
self,
|
||||
mut cb: impl FnMut(&Arc<RwLock<Self>>, &Client, &ProcessScope) -> Control + Send + 'static,
|
||||
) -> Usually<Arc<RwLock<Self>>>
|
||||
where
|
||||
Self: Send + Sync + 'static
|
||||
{
|
||||
let client = Client::from(self);
|
||||
let state = Arc::new(RwLock::new(Self::Activating));
|
||||
let event = Box::new(move|_|{/*TODO*/}) as Box<dyn Fn(JackEvent) + Send + Sync>;
|
||||
let events = Notifications(event);
|
||||
let frame = Box::new({let state = state.clone(); move|c: &_, s: &_|cb(&state, c, s)});
|
||||
let frames = contrib::ClosureProcessHandler::new(frame as BoxedAudioHandler);
|
||||
*state.write().unwrap() = Self::Active(client.activate_async(events, frames)?);
|
||||
Ok(state)
|
||||
}
|
||||
}
|
||||
|
||||
pub type DynamicAsyncClient = AsyncClient<DynamicNotifications, DynamicAudioHandler>;
|
||||
|
||||
pub type DynamicAudioHandler = contrib::ClosureProcessHandler<(), BoxedAudioHandler>;
|
||||
|
||||
pub type BoxedAudioHandler = Box<dyn FnMut(&Client, &ProcessScope) -> Control + Send>;
|
||||
|
||||
impl JackClient {
|
||||
pub fn new (name: &str) -> Usually<Self> {
|
||||
let (client, _) = Client::new(name, ClientOptions::NO_START_SERVER)?;
|
||||
Ok(Self::Inactive(client))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JackClient> for Client {
|
||||
fn from (jack: JackClient) -> Client {
|
||||
match jack {
|
||||
JackClient::Inactive(client) => client,
|
||||
JackClient::Activating => panic!("jack client still activating"),
|
||||
JackClient::Active(_) => panic!("jack client already activated"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Notification handler used by the [Jack] factory
|
||||
/// when constructing [JackDevice]s.
|
||||
pub type DynamicNotifications = Notifications<Box<dyn Fn(JackEvent) + Send + Sync>>;
|
||||
|
||||
/// Generic notification handler that emits [JackEvent]
|
||||
pub struct Notifications<T: Fn(JackEvent) + Send>(pub T);
|
||||
|
||||
impl<T: Fn(JackEvent) + Send> NotificationHandler for Notifications<T> {
|
||||
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
|
||||
}
|
||||
}
|
||||
75
crates/tek/src/core/color.rs
Normal file
75
crates/tek/src/core/color.rs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
use crate::*;
|
||||
use rand::{thread_rng, distributions::uniform::UniformSampler};
|
||||
pub use ratatui::prelude::Color;
|
||||
|
||||
/// A color in OKHSL and RGB representations.
|
||||
#[derive(Debug, Default, Copy, Clone, PartialEq)]
|
||||
pub struct ItemColor {
|
||||
pub okhsl: Okhsl<f32>,
|
||||
pub rgb: Color,
|
||||
}
|
||||
/// A color in OKHSL and RGB with lighter and darker variants.
|
||||
#[derive(Debug, Default, Copy, Clone, PartialEq)]
|
||||
pub struct ItemColorTriplet {
|
||||
pub base: ItemColor,
|
||||
pub light: ItemColor,
|
||||
pub dark: ItemColor,
|
||||
}
|
||||
/// Adds TUI RGB representation to an OKHSL value.
|
||||
impl From<Okhsl<f32>> for ItemColor {
|
||||
fn from (okhsl: Okhsl<f32>) -> Self { Self { okhsl, rgb: okhsl_to_rgb(okhsl) } }
|
||||
}
|
||||
/// Adds OKHSL representation to a TUI RGB value.
|
||||
impl From<Color> for ItemColor {
|
||||
fn from (rgb: Color) -> Self { Self { rgb, okhsl: rgb_to_okhsl(rgb) } }
|
||||
}
|
||||
impl ItemColor {
|
||||
pub fn random () -> Self {
|
||||
let mut rng = thread_rng();
|
||||
let lo = Okhsl::new(-180.0, 0.01, 0.25);
|
||||
let hi = Okhsl::new( 180.0, 0.9, 0.5);
|
||||
UniformOkhsl::new(lo, hi).sample(&mut rng).into()
|
||||
}
|
||||
pub fn random_dark () -> Self {
|
||||
let mut rng = thread_rng();
|
||||
let lo = Okhsl::new(-180.0, 0.025, 0.075);
|
||||
let hi = Okhsl::new( 180.0, 0.5, 0.150);
|
||||
UniformOkhsl::new(lo, hi).sample(&mut rng).into()
|
||||
}
|
||||
pub fn random_near (color: Self, distance: f32) -> Self {
|
||||
color.mix(Self::random(), distance)
|
||||
}
|
||||
pub fn mix (&self, other: Self, distance: f32) -> Self {
|
||||
if distance > 1.0 { panic!("color mixing takes distance between 0.0 and 1.0"); }
|
||||
self.okhsl.mix(other.okhsl, distance).into()
|
||||
}
|
||||
}
|
||||
impl From<ItemColor> for ItemColorTriplet {
|
||||
fn from (base: ItemColor) -> Self {
|
||||
let mut light = base.okhsl.clone();
|
||||
light.lightness = (light.lightness * 1.15).min(Okhsl::<f32>::max_lightness());
|
||||
let mut dark = base.okhsl.clone();
|
||||
dark.lightness = (dark.lightness * 0.85).max(Okhsl::<f32>::min_lightness());
|
||||
dark.saturation = (dark.saturation * 0.85).max(Okhsl::<f32>::min_saturation());
|
||||
Self { base, light: light.into(), dark: dark.into() }
|
||||
}
|
||||
}
|
||||
impl ItemColorTriplet {
|
||||
pub fn random () -> Self {
|
||||
ItemColor::random().into()
|
||||
}
|
||||
pub fn random_near (color: Self, distance: f32) -> Self {
|
||||
color.base.mix(ItemColor::random(), distance).into()
|
||||
}
|
||||
}
|
||||
pub fn okhsl_to_rgb (color: Okhsl<f32>) -> Color {
|
||||
let Srgb { red, green, blue, .. }: Srgb<f32> = Srgb::from_color_unclamped(color);
|
||||
Color::Rgb((red * 255.0) as u8, (green * 255.0) as u8, (blue * 255.0) as u8,)
|
||||
}
|
||||
pub fn rgb_to_okhsl (color: Color) -> Okhsl<f32> {
|
||||
if let Color::Rgb(r, g, b) = color {
|
||||
Okhsl::from_color(Srgb::new(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0))
|
||||
} else {
|
||||
unreachable!("only Color::Rgb is supported")
|
||||
}
|
||||
}
|
||||
96
crates/tek/src/core/command.rs
Normal file
96
crates/tek/src/core/command.rs
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum NextPrev {
|
||||
Next,
|
||||
Prev,
|
||||
}
|
||||
|
||||
pub trait Execute<T> {
|
||||
fn command (&mut self, command: T) -> Perhaps<T>;
|
||||
}
|
||||
|
||||
pub trait Command<S>: Send + Sync + Sized {
|
||||
fn execute (self, state: &mut S) -> Perhaps<Self>;
|
||||
}
|
||||
pub fn delegate <B, C: Command<S>, S> (
|
||||
cmd: C,
|
||||
wrap: impl Fn(C)->B,
|
||||
state: &mut S,
|
||||
) -> Perhaps<B> {
|
||||
Ok(cmd.execute(state)?.map(|x|wrap(x)))
|
||||
}
|
||||
|
||||
pub trait InputToCommand<E: Engine, S>: Command<S> + Sized {
|
||||
fn input_to_command (state: &S, input: &E::Input) -> Option<Self>;
|
||||
fn execute_with_state (state: &mut S, input: &E::Input) -> Perhaps<bool> {
|
||||
Ok(if let Some(command) = Self::input_to_command(state, input) {
|
||||
let _undo = command.execute(state)?;
|
||||
Some(true)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}
|
||||
}
|
||||
pub struct MenuBar<E: Engine, S, C: Command<S>> {
|
||||
pub menus: Vec<Menu<E, S, C>>,
|
||||
pub index: usize,
|
||||
}
|
||||
impl<E: Engine, S, C: Command<S>> MenuBar<E, S, C> {
|
||||
pub fn new () -> Self { Self { menus: vec![], index: 0 } }
|
||||
pub fn add (mut self, menu: Menu<E, S, C>) -> Self {
|
||||
self.menus.push(menu);
|
||||
self
|
||||
}
|
||||
}
|
||||
pub struct Menu<E: Engine, S, C: Command<S>> {
|
||||
pub title: String,
|
||||
pub items: Vec<MenuItem<E, S, C>>,
|
||||
pub index: Option<usize>,
|
||||
}
|
||||
impl<E: Engine, S, C: Command<S>> Menu<E, S, C> {
|
||||
pub fn new (title: impl AsRef<str>) -> Self {
|
||||
Self {
|
||||
title: title.as_ref().to_string(),
|
||||
items: vec![],
|
||||
index: None,
|
||||
}
|
||||
}
|
||||
pub fn add (mut self, item: MenuItem<E, S, C>) -> Self {
|
||||
self.items.push(item);
|
||||
self
|
||||
}
|
||||
pub fn sep (mut self) -> Self {
|
||||
self.items.push(MenuItem::sep());
|
||||
self
|
||||
}
|
||||
pub fn cmd (mut self, hotkey: &'static str, text: &'static str, command: C) -> Self {
|
||||
self.items.push(MenuItem::cmd(hotkey, text, command));
|
||||
self
|
||||
}
|
||||
pub fn off (mut self, hotkey: &'static str, text: &'static str) -> Self {
|
||||
self.items.push(MenuItem::off(hotkey, text));
|
||||
self
|
||||
}
|
||||
}
|
||||
pub enum MenuItem<E: Engine, S, C: Command<S>> {
|
||||
/// Unused.
|
||||
__(PhantomData<E>, PhantomData<S>),
|
||||
/// A separator. Skip it.
|
||||
Separator,
|
||||
/// A menu item with command, description and hotkey.
|
||||
Command(&'static str, &'static str, C),
|
||||
/// A menu item that can't be activated but has description and hotkey
|
||||
Disabled(&'static str, &'static str)
|
||||
}
|
||||
impl<E: Engine, S, C: Command<S>> MenuItem<E, S, C> {
|
||||
pub fn sep () -> Self {
|
||||
Self::Separator
|
||||
}
|
||||
pub fn cmd (hotkey: &'static str, text: &'static str, command: C) -> Self {
|
||||
Self::Command(hotkey, text, command)
|
||||
}
|
||||
pub fn off (hotkey: &'static str, text: &'static str) -> Self {
|
||||
Self::Disabled(hotkey, text)
|
||||
}
|
||||
}
|
||||
14
crates/tek/src/core/edn.rs
Normal file
14
crates/tek/src/core/edn.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
pub use clojure_reader::{edn::{read, Edn}, error::Error as EdnError};
|
||||
|
||||
/// 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),* })
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
54
crates/tek/src/core/engine.rs
Normal file
54
crates/tek/src/core/engine.rs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
use crate::*;
|
||||
|
||||
/// Entry point for main loop
|
||||
pub trait App<T: Engine> {
|
||||
fn run (self, context: T) -> Usually<T>;
|
||||
}
|
||||
|
||||
/// Platform backend.
|
||||
pub trait Engine: Send + Sync + Sized {
|
||||
/// Input event type
|
||||
type Input: Input<Self>;
|
||||
/// Result of handling input
|
||||
type Handled;
|
||||
/// Render target
|
||||
type Output: Output<Self>;
|
||||
/// Unit of length
|
||||
type Unit: Coordinate;
|
||||
/// Rectangle without offset
|
||||
type Size: Size<Self::Unit> + From<[Self::Unit;2]> + Debug + Copy;
|
||||
/// Rectangle with offset
|
||||
type Area: Area<Self::Unit> + 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 UI component that can render itself as a [Render], and [Handle] input.
|
||||
pub trait Component<E: Engine>: Render<E> + Handle<E> {}
|
||||
|
||||
/// Everything that implements [Render] and [Handle] is a [Component].
|
||||
impl<E: Engine, C: Render<E> + Handle<E>> Component<E> for C {}
|
||||
|
||||
/// A component that can exit.
|
||||
pub trait Exit: Send {
|
||||
fn exited (&self) -> bool;
|
||||
fn exit (&mut self);
|
||||
fn boxed (self) -> Box<dyn Exit> where Self: Sized + 'static {
|
||||
Box::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Marker trait for [Component]s that can [Exit].
|
||||
pub trait ExitableComponent<E>: Exit + Component<E> where E: Engine {
|
||||
/// Perform type erasure for collecting heterogeneous components.
|
||||
fn boxed (self) -> Box<dyn ExitableComponent<E>> where Self: Sized + 'static {
|
||||
Box::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// All [Components]s that implement [Exit] implement [ExitableComponent].
|
||||
impl<E: Engine, C: Component<E> + Exit> ExitableComponent<E> for C {}
|
||||
303
crates/tek/src/core/focus.rs
Normal file
303
crates/tek/src/core/focus.rs
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum FocusState<T: Copy + Debug + PartialEq> {
|
||||
Focused(T),
|
||||
Entered(T),
|
||||
}
|
||||
|
||||
impl<T: Copy + Debug + PartialEq> FocusState<T> {
|
||||
pub fn inner (&self) -> T {
|
||||
match self {
|
||||
Self::Focused(inner) => *inner,
|
||||
Self::Entered(inner) => *inner,
|
||||
}
|
||||
}
|
||||
pub fn set_inner (&mut self, inner: T) {
|
||||
*self = match self {
|
||||
Self::Focused(_) => Self::Focused(inner),
|
||||
Self::Entered(_) => Self::Entered(inner),
|
||||
}
|
||||
}
|
||||
pub fn is_focused (&self) -> bool {
|
||||
if let Self::Focused(_) = self { true } else { false }
|
||||
}
|
||||
pub fn is_entered (&self) -> bool {
|
||||
if let Self::Entered(_) = self { true } else { false }
|
||||
}
|
||||
pub fn to_focused (&mut self) {
|
||||
*self = Self::Focused(self.inner())
|
||||
}
|
||||
pub fn to_entered (&mut self) {
|
||||
*self = Self::Entered(self.inner())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||
pub enum FocusCommand {
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
Next,
|
||||
Prev,
|
||||
Enter,
|
||||
Exit,
|
||||
}
|
||||
|
||||
impl<F: HasFocus + HasEnter + FocusGrid + FocusOrder> Command<F> for FocusCommand {
|
||||
fn execute (self, state: &mut F) -> Perhaps<FocusCommand> {
|
||||
use FocusCommand::*;
|
||||
match self {
|
||||
Next => { state.focus_next(); },
|
||||
Prev => { state.focus_prev(); },
|
||||
Up => { state.focus_up(); },
|
||||
Down => { state.focus_down(); },
|
||||
Left => { state.focus_left(); },
|
||||
Right => { state.focus_right(); },
|
||||
Enter => { state.focus_enter(); },
|
||||
Exit => { state.focus_exit(); },
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for things that have focusable subparts.
|
||||
pub trait HasFocus {
|
||||
type Item: Copy + PartialEq + Debug;
|
||||
/// Get the currently focused item.
|
||||
fn focused (&self) -> Self::Item;
|
||||
/// Get the currently focused item.
|
||||
fn set_focused (&mut self, to: Self::Item);
|
||||
/// Loop forward until a specific item is focused.
|
||||
fn focus_to (&mut self, to: Self::Item) {
|
||||
self.set_focused(to);
|
||||
self.focus_updated();
|
||||
}
|
||||
/// Run this on focus update
|
||||
fn focus_updated (&mut self) {}
|
||||
}
|
||||
|
||||
/// Trait for things that have enterable subparts.
|
||||
pub trait HasEnter: HasFocus {
|
||||
/// Get the currently focused item.
|
||||
fn entered (&self) -> bool;
|
||||
/// Get the currently focused item.
|
||||
fn set_entered (&mut self, entered: bool);
|
||||
/// Enter into the currently focused component
|
||||
fn focus_enter (&mut self) {
|
||||
self.set_entered(true);
|
||||
self.focus_updated();
|
||||
}
|
||||
/// Exit the currently entered component
|
||||
fn focus_exit (&mut self) {
|
||||
self.set_entered(false);
|
||||
self.focus_updated();
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for things that implement directional navigation between focusable elements.
|
||||
pub trait FocusGrid: HasFocus {
|
||||
fn focus_layout (&self) -> &[&[Self::Item]];
|
||||
fn focus_cursor (&self) -> (usize, usize);
|
||||
fn focus_cursor_mut (&mut self) -> &mut (usize, usize);
|
||||
fn focus_current (&self) -> Self::Item {
|
||||
let (x, y) = self.focus_cursor();
|
||||
self.focus_layout()[y][x]
|
||||
}
|
||||
fn focus_update (&mut self) {
|
||||
self.focus_to(self.focus_current());
|
||||
self.focus_updated()
|
||||
}
|
||||
fn focus_up (&mut self) {
|
||||
let original_focused = self.focused();
|
||||
let (_, original_y) = self.focus_cursor();
|
||||
loop {
|
||||
let (x, y) = self.focus_cursor();
|
||||
let next_y = if y == 0 {
|
||||
self.focus_layout().len().saturating_sub(1)
|
||||
} else {
|
||||
y - 1
|
||||
};
|
||||
if next_y == original_y {
|
||||
break
|
||||
}
|
||||
let next_x = if self.focus_layout()[y].len() == self.focus_layout()[next_y].len() {
|
||||
x
|
||||
} else {
|
||||
((x as f32 / self.focus_layout()[original_y].len() as f32)
|
||||
* self.focus_layout()[next_y].len() as f32) as usize
|
||||
};
|
||||
*self.focus_cursor_mut() = (next_x, next_y);
|
||||
if self.focus_current() != original_focused {
|
||||
break
|
||||
}
|
||||
}
|
||||
self.focus_update();
|
||||
}
|
||||
fn focus_down (&mut self) {
|
||||
let original_focused = self.focused();
|
||||
let (_, original_y) = self.focus_cursor();
|
||||
loop {
|
||||
let (x, y) = self.focus_cursor();
|
||||
let next_y = if y >= self.focus_layout().len().saturating_sub(1) {
|
||||
0
|
||||
} else {
|
||||
y + 1
|
||||
};
|
||||
if next_y == original_y {
|
||||
break
|
||||
}
|
||||
let next_x = if self.focus_layout()[y].len() == self.focus_layout()[next_y].len() {
|
||||
x
|
||||
} else {
|
||||
((x as f32 / self.focus_layout()[original_y].len() as f32)
|
||||
* self.focus_layout()[next_y].len() as f32) as usize
|
||||
};
|
||||
*self.focus_cursor_mut() = (next_x, next_y);
|
||||
if self.focus_current() != original_focused {
|
||||
break
|
||||
}
|
||||
}
|
||||
self.focus_update();
|
||||
}
|
||||
fn focus_left (&mut self) {
|
||||
let original_focused = self.focused();
|
||||
let (original_x, y) = self.focus_cursor();
|
||||
loop {
|
||||
let x = self.focus_cursor().0;
|
||||
let next_x = if x == 0 {
|
||||
self.focus_layout()[y].len().saturating_sub(1)
|
||||
} else {
|
||||
x - 1
|
||||
};
|
||||
if next_x == original_x {
|
||||
break
|
||||
}
|
||||
*self.focus_cursor_mut() = (next_x, y);
|
||||
if self.focus_current() != original_focused {
|
||||
break
|
||||
}
|
||||
}
|
||||
self.focus_update();
|
||||
}
|
||||
fn focus_right (&mut self) {
|
||||
let original_focused = self.focused();
|
||||
let (original_x, y) = self.focus_cursor();
|
||||
loop {
|
||||
let x = self.focus_cursor().0;
|
||||
let next_x = if x >= self.focus_layout()[y].len().saturating_sub(1) {
|
||||
0
|
||||
} else {
|
||||
x + 1
|
||||
};
|
||||
if next_x == original_x {
|
||||
break
|
||||
}
|
||||
self.focus_cursor_mut().0 = next_x;
|
||||
if self.focus_current() != original_focused {
|
||||
break
|
||||
}
|
||||
}
|
||||
self.focus_update();
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for things that implement next/prev navigation between focusable elements.
|
||||
pub trait FocusOrder {
|
||||
/// Focus the next item.
|
||||
fn focus_next (&mut self);
|
||||
/// Focus the previous item.
|
||||
fn focus_prev (&mut self);
|
||||
}
|
||||
|
||||
/// Next/prev navigation for directional focusables works in the given way.
|
||||
impl<T: FocusGrid + HasEnter> FocusOrder for T {
|
||||
/// Focus the next item.
|
||||
fn focus_next (&mut self) {
|
||||
let current = self.focused();
|
||||
let (x, y) = self.focus_cursor();
|
||||
if x < self.focus_layout()[y].len().saturating_sub(1) {
|
||||
self.focus_right();
|
||||
} else {
|
||||
self.focus_down();
|
||||
self.focus_cursor_mut().0 = 0;
|
||||
}
|
||||
if self.focused() == current { // FIXME: prevent infinite loop
|
||||
self.focus_next()
|
||||
}
|
||||
self.focus_exit();
|
||||
self.focus_update();
|
||||
}
|
||||
/// Focus the previous item.
|
||||
fn focus_prev (&mut self) {
|
||||
let current = self.focused();
|
||||
let (x, _) = self.focus_cursor();
|
||||
if x > 0 {
|
||||
self.focus_left();
|
||||
} else {
|
||||
self.focus_up();
|
||||
let (_, y) = self.focus_cursor();
|
||||
let next_x = self.focus_layout()[y].len().saturating_sub(1);
|
||||
self.focus_cursor_mut().0 = next_x;
|
||||
}
|
||||
if self.focused() == current { // FIXME: prevent infinite loop
|
||||
self.focus_prev()
|
||||
}
|
||||
self.focus_exit();
|
||||
self.focus_update();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[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');
|
||||
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue