mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-02-01 16:46:41 +01:00
wip3 (33e): ermh...
This commit is contained in:
parent
b028dc41a3
commit
1bb0107485
135 changed files with 590 additions and 654 deletions
|
|
@ -1,35 +1,60 @@
|
|||
[package]
|
||||
name = "tek"
|
||||
edition = "2021"
|
||||
version = "0.1.0"
|
||||
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"
|
||||
microxdg = "0.1.2"
|
||||
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" ] }
|
||||
|
||||
tek_core = { path = "../tek_core" }
|
||||
#tek_sequencer = { path = "../tek_sequencer" }
|
||||
#tek_mixer = { path = "../tek_mixer" }
|
||||
#jack = "0.10"
|
||||
#crossterm = "0.27"
|
||||
#ratatui = { version = "0.26.3", features = [ "unstable-widget-ref", "underline-color" ] }
|
||||
#backtrace = "0.3.72"
|
||||
#toml = "0.8.12"
|
||||
#better-panic = "0.3.0"
|
||||
#midly = "0.5"
|
||||
[dev-dependencies]
|
||||
#tek_app = { version = "0.1.0", path = "../tek_app" }
|
||||
|
||||
#vst = "0.4.0"
|
||||
##vst3 = "0.1.0"
|
||||
#livi = "0.7.4"
|
||||
##atomic_enum = "0.3.0"
|
||||
#wavers = "1.4.3"
|
||||
#music-math = "0.1.1"
|
||||
#fraction = "0.15.3"
|
||||
#rlsf = "0.2.1"
|
||||
#r8brain-rs = "0.3.5"
|
||||
[[bin]]
|
||||
name = "tek_arranger"
|
||||
path = "src/cli_arranger.rs"
|
||||
|
||||
#symphonia = { version = "0.5.4", features = [ "all" ] }
|
||||
[[bin]]
|
||||
name = "tek_sequencer"
|
||||
path = "src/cli_sequencer.rs"
|
||||
|
||||
#dasp = { version = "0.11.0", features = [ "all" ] }
|
||||
[[bin]]
|
||||
name = "tek_transport"
|
||||
path = "src/cli_transport.rs"
|
||||
|
||||
#rubato = "0.15.0"
|
||||
#[[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"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,46 @@
|
|||
# `tek`
|
||||
|
||||
This crate unifies the `tek_*` subcrates into a single application.
|
||||
# `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`
|
||||
|
|
|
|||
4
crates/tek/architecture.svg
Normal file
4
crates/tek/architecture.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 25 KiB |
|
|
@ -1,139 +0,0 @@
|
|||
;(bpm 150)
|
||||
|
||||
;(midi-in "nanoKEY Studio.*capture.*")
|
||||
;(midi-in "nanoKEY Studio.*capture.*")
|
||||
;(audio-out "Built-.+:playback_FL", "Built-.+:playback_FR")
|
||||
|
||||
;(scene { :name "Intro" } 0 0 _ _)
|
||||
;(scene { :name "Hook" } 1 1 0 _)
|
||||
;(scene { :name "Verse" } 2 2 1 _)
|
||||
;(scene { :name "Chorus" } 3 3 2 _)
|
||||
;(scene { :name "Bridge" } _ 4 3 _)
|
||||
;(scene { :name "Outro" } 4 1 4 _)
|
||||
|
||||
;(track { :name "Drums" :gain +0.0 }
|
||||
;(phrase { :name "4 kicks" :beats 4 :steps 16 }
|
||||
;(:00 (36 128))
|
||||
;(:04 (36 100))
|
||||
;(:08 (36 100))
|
||||
;(:12 (36 100)))
|
||||
;(phrase { :name "5 kicks" :beats 4 :steps 16 }
|
||||
;(:00 (36 128))
|
||||
;(:04 (36 100))
|
||||
;(:08 (36 128))
|
||||
;(:12 (36 100))
|
||||
;(:14 (36 110)))
|
||||
;(phrase { :name "D Beat" :beats 8 :steps 32 }
|
||||
;(:00 (44 70) (36 128) (49 110))
|
||||
;(:02 (44 30))
|
||||
;(:04 (44 80) (40 100))
|
||||
;(:06 (44 50))
|
||||
;(:08 (44 30) (36 100))
|
||||
;(:10 (44 50) (36 100))
|
||||
;(:12 (44 80) (40 100))
|
||||
;(:14 (44 50))
|
||||
;(:15 (36 50))
|
||||
;(:16 (44 60) (36 80))
|
||||
;(:18 (44 60) (36 80))
|
||||
;(:20 (44 60) (40 80))
|
||||
;(:22 (44 60))
|
||||
;(:24 (44 60))
|
||||
;(:26 (44 30) (36 80))
|
||||
;(:27 (44 60))
|
||||
;(:28 (44 60) (40 80))
|
||||
;(:30 (44 60)))
|
||||
;(phrase { :name "Garage" :beats 4 :steps 16 }
|
||||
;(:00 (44 100) (36 100) (35 100))
|
||||
;(:01 (44 100))
|
||||
;(:02 (44 100) (35 100))
|
||||
;(:03 (44 100))
|
||||
;(:04 (44 100) (40 100))
|
||||
;(:06 (44 100))
|
||||
;(:07 (44 100) (34 100))
|
||||
;(:09 (44 100))
|
||||
;(:10 (44 100))
|
||||
;(:11 (35 100) (36 100))
|
||||
;(:12 (44 100) (40 100))
|
||||
;(:14 (44 100)))
|
||||
|
||||
;(phrase { :name "Trap Pinging" :beats 8 :steps 96 }
|
||||
;(:00 (42 100) (36 100) (34 120) (49 100))
|
||||
;(:01 (42 100))
|
||||
;(:02 (42 100))
|
||||
;(:06 (42 100) (35 80) (36 80) (49 100))
|
||||
;(:07 (42 100))
|
||||
;(:08 (42 100))
|
||||
;(:12 (42 100))
|
||||
;(:15 (39 100) (34 100))
|
||||
;(:18 (42 100))
|
||||
;(:24 (42 100) (38 50) (40 50))
|
||||
;(:27 (42 100) (36 50))
|
||||
;(:30 (42 100))
|
||||
;(:33 (42 100) (36 50) (34 100))
|
||||
;(:36 (42 90))
|
||||
;(:39 (42 80))
|
||||
;(:42 (42 70))
|
||||
;(:45 (42 60))
|
||||
|
||||
;(:48 (42 100) (36 100) (34 100))
|
||||
;(:50 (42 100))
|
||||
;(:52 (42 110))
|
||||
;(:54 (46 50) (42 120))
|
||||
;(:56 (42 90))
|
||||
;(:58 (42 100))
|
||||
;(:60 (42 100) (35 100))
|
||||
;(:64 (39 100))
|
||||
;(:66 (42 100) (34 100))
|
||||
|
||||
;(:70 (42 100))
|
||||
;(:71 (42 100))
|
||||
;(:72 (42 100) (38 50) (40 50))
|
||||
;(:75 (42 100) (36 50) (34 80))
|
||||
;(:78 (42 100))
|
||||
;(:81 (42 100) (36 50))
|
||||
;(:84 (38 40) (40 50) (34 90))
|
||||
;(:87 (42 90) (35 40))
|
||||
;(:90 (42 70)))
|
||||
|
||||
;(sampler { :name "DrumKit1" :dir "/home/user/Lab/Music/pak" }
|
||||
;(sample { :midi 34 :name "808 D" :file "808.wav" })
|
||||
;(sample { :midi 35 :name "Kick 1" :file "kik.wav" })
|
||||
;(sample { :midi 36 :name "Kick 2" :file "kik2.wav" })
|
||||
;(sample { :midi 37 :name "Rim" :file "rim.wav" })
|
||||
;(sample { :midi 38 :name "Snare 1" :file "sna.wav" })
|
||||
;(sample { :midi 39 :name "Shaker" :file "shk.wav" })
|
||||
;(sample { :midi 40 :name "Snare 2" :file "sna2.wav" })
|
||||
;(sample { :midi 42 :name "Closed HH 1" :file "chh.wav" })
|
||||
;(sample { :midi 44 :name "Closed HH 2" :file "chh2.wav" })
|
||||
;(sample { :midi 45 :name "Open HH 0" :file "ohh.wav" })
|
||||
;(sample { :midi 46 :name "Open HH 1" :file "ohh1.wav" })
|
||||
;(sample { :midi 47 :name "Open HH 2" :file "ohh2.wav" })
|
||||
;(sample { :midi 49 :name "Crash" :file "crs.wav" })))
|
||||
|
||||
;(track { :name "Bass" :gain +0.0 }
|
||||
;(phrase { :name "Bass 1" :beats 4 })
|
||||
;(phrase { :name "Bass 2" :beats 4 })
|
||||
;(phrase { :name "Bass 3" :beats 4 })
|
||||
;(phrase { :name "Bass 4" :beats 4 })
|
||||
;(phrase { :name "Bass 5" :beats 4 })
|
||||
;(phrase { :name "Bass 6" :beats 4 })
|
||||
;(phrase { :name "Bass 7" :beats 4 })
|
||||
;(phrase { :name "Bass 8" :beats 4 })
|
||||
;(lv2 {
|
||||
;:name "Odin2"
|
||||
;:path "file:///home/user/.lv2/Odin2.lv2"
|
||||
;}))
|
||||
|
||||
;(track { :name "Lead" :gain +0.0 }
|
||||
;(phrase { :name "Lead 1" :beats 4 })
|
||||
;(phrase { :name "Lead 2" :beats 4 })
|
||||
;(phrase { :name "Lead 3" :beats 4 })
|
||||
;(phrase { :name "Lead 4" :beats 4 })
|
||||
;(phrase { :name "Lead 5" :beats 4 })
|
||||
;(phrase { :name "Lead 6" :beats 4 })
|
||||
;(phrase { :name "Lead 7" :beats 4 })
|
||||
;(phrase { :name "Lead 8" :beats 4 })
|
||||
;(lv2 {
|
||||
;:name "Odin2"
|
||||
;:path "file:///home/user/.lv2/Odin2.lv2"
|
||||
;}))
|
||||
144
crates/tek/examples/demo.rs
Normal file
144
crates/tek/examples/demo.rs
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
use tek_core::*;
|
||||
use tek_core::jack::*;
|
||||
|
||||
fn main () -> Usually<()> {
|
||||
Tui::run(Arc::new(RwLock::new(Demo::new())))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct Demo<E: Engine> {
|
||||
index: usize,
|
||||
items: Vec<Box<dyn Render<Engine = E>>>
|
||||
}
|
||||
|
||||
impl Demo<Tui> {
|
||||
fn new () -> Self {
|
||||
Self {
|
||||
index: 0,
|
||||
items: vec![
|
||||
//Box::new(tek_sequencer::TransportPlayPauseButton {
|
||||
//_engine: Default::default(),
|
||||
//transport: None,
|
||||
//value: Some(TransportState::Stopped),
|
||||
//focused: true
|
||||
//}),
|
||||
//Box::new(tek_sequencer::TransportPlayPauseButton {
|
||||
//_engine: Default::default(),
|
||||
//transport: None,
|
||||
//value: Some(TransportState::Rolling),
|
||||
//focused: false
|
||||
//}),
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Content for Demo<Tui> {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> dyn Render<Engine = Tui> {
|
||||
let border_style = Style::default().fg(Color::Rgb(0,0,0));
|
||||
Align::Center(Layers::new(move|add|{
|
||||
|
||||
add(&Background(Color::Rgb(0,128,128)))?;
|
||||
|
||||
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(&Outset::XY(2, 1, "..."))?;
|
||||
Ok(())
|
||||
}).debug())?;
|
||||
|
||||
add(&Layers::new(|add|{
|
||||
add(&Background(Color::Rgb(128,64,0)))?;
|
||||
add(&Border(Lozenge(border_style)))?;
|
||||
add(&Outset::XY(4, 2, "---"))?;
|
||||
Ok(())
|
||||
}).debug())?;
|
||||
|
||||
add(&Layers::new(|add|{
|
||||
add(&Background(Color::Rgb(96,64,0)))?;
|
||||
add(&Border(SquareBold(border_style)))?;
|
||||
add(&Outset::XY(6, 3, "~~~"))?;
|
||||
Ok(())
|
||||
}).debug())?;
|
||||
|
||||
Ok(())
|
||||
})).debug())?;
|
||||
|
||||
Ok(())
|
||||
|
||||
}))
|
||||
//Align::Center(Outset::X(1, Layers::new(|add|{
|
||||
//add(&Background(Color::Rgb(128,0,0)))?;
|
||||
//add(&Stack::down(|add|{
|
||||
//add(&Outset::Y(1, Layers::new(|add|{
|
||||
//add(&Background(Color::Rgb(0,128,0)))?;
|
||||
//add(&Align::Center("12345"))?;
|
||||
//add(&Align::Center("FOO"))
|
||||
//})))?;
|
||||
//add(&Outset::XY(1, 1, Layers::new(|add|{
|
||||
//add(&Align::Center("1234567"))?;
|
||||
//add(&Align::Center("BAR"))?;
|
||||
//add(&Background(Color::Rgb(0,0,128)))
|
||||
//})))
|
||||
//}))
|
||||
//})))
|
||||
|
||||
//Align::Y(Layers::new(|add|{
|
||||
//add(&Background(Color::Rgb(128,0,0)))?;
|
||||
//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(&Outset::XY(1, 1, Layers::new(|add|{
|
||||
//add(&Align::Center("1234567"))?;
|
||||
//add(&Align::Center("BAR"))?;
|
||||
//add(&Background(Color::Rgb(0,0,128)))
|
||||
//})))?;
|
||||
//Ok(())
|
||||
//})))))
|
||||
//}))
|
||||
}
|
||||
}
|
||||
|
||||
impl Handle<Tui> for Demo<Tui> {
|
||||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||
match from.event() {
|
||||
key!(KeyCode::PageUp) => {
|
||||
self.index = (self.index + 1) % self.items.len();
|
||||
},
|
||||
key!(KeyCode::PageDown) => {
|
||||
self.index = if self.index > 1 {
|
||||
self.index - 1
|
||||
} else {
|
||||
self.items.len() - 1
|
||||
};
|
||||
},
|
||||
_ => return Ok(None)
|
||||
}
|
||||
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(())
|
||||
}
|
||||
12
crates/tek/examples/mixer.edn
Normal file
12
crates/tek/examples/mixer.edn
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
(mixer
|
||||
(track
|
||||
(name "Drums")
|
||||
(sampler
|
||||
(dir "/home/user/Lab/Music/pak")
|
||||
(sample (midi 34) (name "808 D") (file "808.wav"))))
|
||||
(track
|
||||
(name "Lead")
|
||||
(lv2
|
||||
(name "Odin2")
|
||||
(path "file:///home/user/.lv2/Odin2.lv2"))
|
||||
(gain 0.0)))
|
||||
18
crates/tek/examples/sequencer.edn
Normal file
18
crates/tek/examples/sequencer.edn
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
(arranger
|
||||
(track
|
||||
(name "Drums")
|
||||
(phrase
|
||||
(name "4 kicks")
|
||||
(beats 4)
|
||||
(steps 16)
|
||||
(:00 (36 128))
|
||||
(:04 (36 100))
|
||||
(:08 (36 100))
|
||||
(:12 (36 100))))
|
||||
(track
|
||||
(name "Bass")
|
||||
(phrase
|
||||
(beats 4)
|
||||
(steps 16)
|
||||
(:04 (36 100))
|
||||
(:12 (36 100)))))
|
||||
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)
|
||||
//}
|
||||
//}
|
||||
193
crates/tek/src/api/clock.rs
Normal file
193
crates/tek/src/api/clock.rs
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
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()?;
|
||||
let mut started = self.started.write().unwrap();
|
||||
match self.transport.query_state()? {
|
||||
TransportState::Rolling => {
|
||||
if started.is_none() {
|
||||
*started = Some(Moment::from_sample(&self.timebase, current_frames as f64));
|
||||
}
|
||||
},
|
||||
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;
|
||||
}
|
||||
22
crates/tek/src/api/jack.rs
Normal file
22
crates/tek/src/api/jack.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
460
crates/tek/src/api/mod.rs
Normal file
460
crates/tek/src/api/mod.rs
Normal file
|
|
@ -0,0 +1,460 @@
|
|||
use crate::*;
|
||||
|
||||
submod! {
|
||||
clip
|
||||
clock
|
||||
color
|
||||
jack
|
||||
phrase
|
||||
player
|
||||
scene
|
||||
track
|
||||
|
||||
//api_mixer
|
||||
//api_channel
|
||||
//api_plugin
|
||||
//api_plugin_kind
|
||||
//api_plugin_lv2
|
||||
//api_sampler
|
||||
//api_sampler_sample
|
||||
//api_sampler_voice
|
||||
}
|
||||
|
||||
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 = tek_core::jack::contrib::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: tek_core::jack::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,
|
||||
//}
|
||||
//}
|
||||
//}
|
||||
0
crates/tek/src/api/name.rs
Normal file
0
crates/tek/src/api/name.rs
Normal file
170
crates/tek/src/api/phrase.rs
Normal file
170
crates/tek/src/api/phrase.rs
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
use crate::*;
|
||||
use tek_core::midly::Smf;
|
||||
use std::path::PathBuf;
|
||||
|
||||
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 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,
|
||||
////})
|
||||
////}
|
||||
//}
|
||||
0
crates/tek/src/api/timeline.ts
Normal file
0
crates/tek/src/api/timeline.ts
Normal file
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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,151 +0,0 @@
|
|||
use crate::*;
|
||||
use tek_core::Direction;
|
||||
use tek_mixer::Mixer;
|
||||
use tek_sequencer::{Arranger, TransportToolbar};
|
||||
|
||||
/// Root of application state.
|
||||
pub struct App {
|
||||
/// Whether the currently focused section has input priority
|
||||
pub entered: bool,
|
||||
/// Currently focused section
|
||||
pub section: AppFocus,
|
||||
/// Transport model and view.
|
||||
pub transport: TransportToolbar,
|
||||
/// Arranger (contains sequencers)
|
||||
pub arranger: Arranger,
|
||||
/// Mixer (contains tracks)
|
||||
pub mixer: Mixer,
|
||||
/// Main JACK client.
|
||||
pub jack: Option<JackClientt>,
|
||||
/// Map of external MIDI outs in the jack graph
|
||||
/// to internal MIDI ins of this app.
|
||||
pub midi_in: Option<Arc<Port<MidiIn>>>,
|
||||
/// Names of ports to connect to main MIDI IN.
|
||||
pub midi_ins: Vec<String>,
|
||||
/// Display mode of chain section
|
||||
pub chain_mode: bool,
|
||||
/// Main audio outputs.
|
||||
pub audio_outs: Vec<Arc<Port<Unowned>>>,
|
||||
/// Number of frames requested by process callback
|
||||
chunk_size: usize,
|
||||
/// Paths to user directories
|
||||
_xdg: Option<Arc<XdgApp>>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new() -> Usually<Self> {
|
||||
let xdg = Arc::new(XdgApp::new("tek")?);
|
||||
let first_run = AppPaths::new(&xdg)?.should_create();
|
||||
let jack = JackClient::Inactive(Client::new("tek", ClientOptions::NO_START_SERVER)?.0);
|
||||
*MODAL.lock().unwrap() =
|
||||
first_run.then(|| ExitableComponent::boxed(SetupModal(Some(xdg.clone()), false)));
|
||||
Ok(Self {
|
||||
entered: true,
|
||||
section: AppFocus::default(),
|
||||
transport: TransportToolbar::new(Some(jack.transport())),
|
||||
arranger: Arranger::new(""),
|
||||
mixer: Mixer::new("")?,
|
||||
jack: Some(jack),
|
||||
audio_outs: vec![],
|
||||
chain_mode: false,
|
||||
chunk_size: 0,
|
||||
midi_in: None,
|
||||
midi_ins: vec![],
|
||||
_xdg: Some(xdg),
|
||||
})
|
||||
}
|
||||
pub fn client(&self) -> &Client {
|
||||
self.jack.as_ref().unwrap().client()
|
||||
}
|
||||
pub fn audio_out(&self, index: usize) -> Option<Arc<Port<Unowned>>> {
|
||||
self.audio_outs.get(index).map(|x| x.clone())
|
||||
}
|
||||
pub fn with_midi_ins(mut self, names: &[&str]) -> Usually<Self> {
|
||||
self.midi_ins = names.iter().map(|x| x.to_string()).collect();
|
||||
Ok(self)
|
||||
}
|
||||
pub fn with_audio_outs(mut self, names: &[&str]) -> Usually<Self> {
|
||||
let client = self.client();
|
||||
self.audio_outs = names
|
||||
.iter()
|
||||
.map(|name| {
|
||||
client
|
||||
.ports(Some(name), None, PortFlags::empty())
|
||||
.get(0)
|
||||
.map(|name| client.port_by_name(name))
|
||||
})
|
||||
.flatten()
|
||||
.filter_map(|x| x)
|
||||
.map(Arc::new)
|
||||
.collect();
|
||||
Ok(self)
|
||||
}
|
||||
pub fn activate(
|
||||
mut self,
|
||||
init: Option<impl FnOnce(&Arc<RwLock<Self>>) -> Usually<()>>,
|
||||
) -> Usually<Arc<RwLock<Self>>> {
|
||||
let jack = self.jack.take().expect("no jack client");
|
||||
let app = Arc::new(RwLock::new(self));
|
||||
app.write().unwrap().jack = Some(jack.activate(&app.clone(), |state, client, scope| {
|
||||
state.write().unwrap().process(client, scope)
|
||||
})?);
|
||||
if let Some(init) = init {
|
||||
init(&app)?;
|
||||
}
|
||||
Ok(app)
|
||||
}
|
||||
}
|
||||
|
||||
render!(
|
||||
App | self,
|
||||
buf,
|
||||
area | {
|
||||
Stack::down()
|
||||
.add_ref(&self.transport)
|
||||
.add_ref(&self.arranger)
|
||||
.add(If(
|
||||
self.arranger.selected.is_clip(),
|
||||
&Stack::right()
|
||||
.add(tek_mixer::TrackView {
|
||||
direction: Direction::Down,
|
||||
entered: self.entered,
|
||||
focused: self.section == AppFocus::Chain,
|
||||
chain: self.mixer.track(),
|
||||
})
|
||||
.add_ref(&self.arranger.sequencer()),
|
||||
))
|
||||
.render(buf, area)?;
|
||||
if let Some(ref modal) = *MODAL.lock().unwrap() {
|
||||
modal.render(buf, area)?;
|
||||
}
|
||||
Ok(area)
|
||||
}
|
||||
);
|
||||
|
||||
process!(
|
||||
App | self,
|
||||
_client,
|
||||
scope | {
|
||||
let (reset, current_frames, chunk_size, current_usecs, next_usecs, period_usecs) =
|
||||
self.transport.update(&scope);
|
||||
self.chunk_size = chunk_size;
|
||||
for track in self.arranger.tracks.iter_mut() {
|
||||
track.process(
|
||||
self.midi_in.as_ref().map(|p| p.iter(&scope)),
|
||||
&self.transport.timebase,
|
||||
self.transport.playing,
|
||||
self.transport.started,
|
||||
self.transport.quant as usize,
|
||||
reset,
|
||||
&scope,
|
||||
(current_frames as usize, self.chunk_size),
|
||||
(
|
||||
current_usecs as usize,
|
||||
next_usecs.saturating_sub(current_usecs) as usize,
|
||||
),
|
||||
period_usecs as f64,
|
||||
);
|
||||
}
|
||||
Control::Continue
|
||||
}
|
||||
);
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
/// Different sections of the UI that may be focused.
|
||||
#[derive(PartialEq, Clone, Copy)]
|
||||
pub enum AppFocus {
|
||||
/// The transport is selected.
|
||||
Transport,
|
||||
/// The arranger is selected.
|
||||
Arranger,
|
||||
/// The sequencer is selected.
|
||||
Sequencer,
|
||||
/// The device chain is selected.
|
||||
Chain,
|
||||
}
|
||||
|
||||
impl Default for AppFocus {
|
||||
fn default () -> Self { Self::Arranger }
|
||||
}
|
||||
|
||||
impl AppFocus {
|
||||
pub fn prev (&mut self) {
|
||||
*self = match self {
|
||||
Self::Transport => Self::Chain,
|
||||
Self::Arranger => Self::Transport,
|
||||
Self::Sequencer => Self::Arranger,
|
||||
Self::Chain => Self::Sequencer,
|
||||
}
|
||||
}
|
||||
pub fn next (&mut self) {
|
||||
*self = match self {
|
||||
Self::Transport => Self::Arranger,
|
||||
Self::Arranger => Self::Sequencer,
|
||||
Self::Sequencer => Self::Chain,
|
||||
Self::Chain => Self::Transport,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
//! Global settings.
|
||||
|
||||
use crate::*;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::fs::{File, create_dir_all};
|
||||
|
||||
const CONFIG_FILE_NAME: &str = "tek.toml";
|
||||
const PROJECT_FILE_NAME: &str = "project.toml";
|
||||
|
||||
/// Filesystem locations of things.
|
||||
pub struct AppPaths {
|
||||
config_dir: PathBuf,
|
||||
config_file: PathBuf,
|
||||
data_dir: PathBuf,
|
||||
project_file: PathBuf,
|
||||
}
|
||||
|
||||
impl AppPaths {
|
||||
pub fn new (xdg: &XdgApp) -> Usually<Self> {
|
||||
let config_dir = PathBuf::from(xdg.app_config()?);
|
||||
let config_file = PathBuf::from(config_dir.join(CONFIG_FILE_NAME));
|
||||
let data_dir = PathBuf::from(xdg.app_data()?);
|
||||
let project_file = PathBuf::from(data_dir.join(PROJECT_FILE_NAME));
|
||||
Ok(Self { config_dir, config_file, data_dir, project_file })
|
||||
}
|
||||
pub fn should_create (&self) -> bool {
|
||||
for path in [
|
||||
&self.config_dir,
|
||||
&self.config_file,
|
||||
&self.data_dir,
|
||||
&self.project_file,
|
||||
].iter() {
|
||||
if !Path::new(path).exists() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
pub fn create (&self) -> Usually<()> {
|
||||
for dir in [&self.config_dir, &self.data_dir].iter() {
|
||||
if !Path::new(dir).exists() {
|
||||
create_dir_all(&dir)?;
|
||||
}
|
||||
}
|
||||
for file in [&self.config_file, &self.project_file].iter() {
|
||||
if !Path::new(file).exists() {
|
||||
File::create_new(&file)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
//! Command line option parser.
|
||||
|
||||
use tek_core::clap::{self, Parser, Subcommand};
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(version, about, long_about = None)]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
pub command: Option<Command>
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Subcommand)]
|
||||
pub enum Command {
|
||||
/// Launch or control a master transport
|
||||
Transport,
|
||||
/// Launch or control a sequencer
|
||||
Sequencer {
|
||||
#[arg(long="input")]
|
||||
inputs: Vec<Option<String>>,
|
||||
#[arg(long="output")]
|
||||
outputs: Vec<Option<String>>,
|
||||
},
|
||||
/// Launch or control a sampler
|
||||
Sampler,
|
||||
/// Launch or control a mixer
|
||||
Mixer,
|
||||
/// Launch or control a looper
|
||||
Looper,
|
||||
}
|
||||
50
crates/tek/src/cli_arranger.rs
Normal file
50
crates/tek/src/cli_arranger.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
use tek_api::{JackActivate, ArrangerTracksApi, HasScenes};
|
||||
use tek_core::{*, clap::{self, Parser}};
|
||||
|
||||
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 = tek_tui::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_sequencer.rs
Normal file
45
crates/tek/src/cli_sequencer.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
use tek_api::JackActivate;
|
||||
use crate::{*, jack::{MidiIn, MidiOut}, clap::{self, Parser}};
|
||||
|
||||
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 = tek_tui::SequencerTui::try_from(jack)?;
|
||||
// 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(())
|
||||
}
|
||||
}
|
||||
10
crates/tek/src/cli_transport.rs
Normal file
10
crates/tek/src/cli_transport.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
use tek_api::JackActivate;
|
||||
use tek_core::{*, clap::{self, Parser}};
|
||||
|
||||
/// Application entrypoint.
|
||||
pub fn main () -> Usually<()> {
|
||||
Tui::run(JackClient::new("tek_transport")?.activate_with(|jack|{
|
||||
tek_tui::TransportTui::try_from(jack)
|
||||
})?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,184 +0,0 @@
|
|||
//! Handling of input events.
|
||||
|
||||
use crate::*;
|
||||
|
||||
handle!{
|
||||
App |self, e| {
|
||||
if handle_modal(e)? {
|
||||
return Ok(true)
|
||||
}
|
||||
Ok(if self.entered {
|
||||
handle_focused(self, e)?
|
||||
|| handle_keymap(self, e, KEYMAP_GLOBAL)?
|
||||
|| handle_keymap(self, e, crate::control::KEYMAP_FOCUS)?
|
||||
} else {
|
||||
handle_keymap(self, e, KEYMAP_GLOBAL)?
|
||||
|| handle_keymap(self, e, crate::control::KEYMAP_FOCUS)?
|
||||
|| handle_focused(self, e)?
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_modal (e: &AppEvent) -> Usually<bool> {
|
||||
let mut handled = false;
|
||||
let mut close = false;
|
||||
if let Some(ref mut modal) = *MODAL.lock().unwrap() {
|
||||
if modal.handle(e)? {
|
||||
handled = true;
|
||||
if modal.exited() {
|
||||
close = true;
|
||||
}
|
||||
};
|
||||
}
|
||||
if close {
|
||||
*MODAL.lock().unwrap() = None;
|
||||
}
|
||||
Ok(handled)
|
||||
}
|
||||
|
||||
fn handle_focused (state: &mut App, e: &AppEvent) -> Usually<bool> {
|
||||
unimplemented!()
|
||||
//match state.section {
|
||||
//AppFocus::Transport => state.transport.handle(e),
|
||||
//AppFocus::Arranger => state.arranger.sequencer_mut().map(|s|s.handle(e)),
|
||||
//AppFocus::Sequencer => state.arranger.sequencer_mut().map(|s|s.handle(e)),
|
||||
//AppFocus::Chain => Ok(false)[>if state.entered {
|
||||
//handle_device(state, e)? ||
|
||||
//handle_keymap(state, e, crate::control::KEYMAP_CHAIN)?
|
||||
//} else {
|
||||
//handle_keymap(state, e, crate::control::KEYMAP_CHAIN)? || handle_device(state, e)?
|
||||
//})*/
|
||||
//}
|
||||
}
|
||||
|
||||
fn handle_device (state: &mut App, e: &AppEvent) -> Usually<bool> {
|
||||
state.mixer.track()
|
||||
.and_then(|track|track.device_mut())
|
||||
.map(|mut device|device.handle(e))
|
||||
.transpose()
|
||||
.map(|x|x.unwrap_or(false))
|
||||
}
|
||||
|
||||
/// Global key bindings.
|
||||
pub const KEYMAP_GLOBAL: &'static [KeyBinding<App>] = keymap!(App {
|
||||
[Char(' '), NONE, "play_toggle", "play or pause", |app: &mut App| {
|
||||
app.transport.toggle_play()?;
|
||||
Ok(true)
|
||||
}],
|
||||
[Char('r'), NONE, "record_toggle", "toggle recording", |app: &mut App| {
|
||||
app.arranger.track_mut().map(|t|t.toggle_record());
|
||||
Ok(true)
|
||||
}],
|
||||
[Char('o'), NONE, "overdub_toggle", "toggle overdub", |app: &mut App| {
|
||||
app.arranger.track_mut().map(|t|t.toggle_overdub());
|
||||
Ok(true)
|
||||
}],
|
||||
[Char('m'), NONE, "monitor_toggle", "toggle monitor", |app: &mut App| {
|
||||
app.arranger.track_mut().map(|t|t.toggle_monitor());
|
||||
Ok(true)
|
||||
}],
|
||||
[Char('+'), NONE, "quant_inc", "quantize coarser", |app: &mut App| {
|
||||
app.transport.quant = next_note_length(app.transport.quant);
|
||||
Ok(true)
|
||||
}],
|
||||
[Char('_'), NONE, "quant_dec", "quantize finer", |app: &mut App| {
|
||||
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(&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(&next_note_length));
|
||||
Ok(true)
|
||||
}],
|
||||
[Char('x'), NONE, "extend", "double the current clip", |app: &mut App| {
|
||||
app.arranger.phrase().map(|x|x.write().unwrap()).map(|mut phrase|{
|
||||
let mut notes = phrase.notes.clone();
|
||||
notes.extend_from_slice(&mut phrase.notes);
|
||||
phrase.notes = notes;
|
||||
phrase.length = phrase.length * 2;
|
||||
});
|
||||
//app.arranger.show_phrase()?;
|
||||
Ok(true)
|
||||
}],
|
||||
[Char('l'), NONE, "loop_toggle", "toggle looping", |_app: &mut App| {
|
||||
// TODO: This toggles the loop flag for the clip under the cursor.
|
||||
Ok(true)
|
||||
}],
|
||||
[Char('['), NONE, "loop_start_dec", "move loop start back", |_app: &mut App| {
|
||||
// TODO: This moves the loop start to the previous quant.
|
||||
Ok(true)
|
||||
}],
|
||||
[Char(']'), NONE, "loop_start_inc", "move loop start forward", |_app: &mut App| {
|
||||
// TODO: This moves the loop start to the next quant.
|
||||
Ok(true)
|
||||
}],
|
||||
[Char('{'), NONE, "loop_end_dec", "move loop end back", |_app: &mut App| {
|
||||
// TODO: This moves the loop end to the previous quant.
|
||||
Ok(true)
|
||||
}],
|
||||
[Char('}'), NONE, "loop_end_inc", "move loop end forward", |_app: &mut App| {
|
||||
// TODO: This moves the loop end to the next quant.
|
||||
Ok(true)
|
||||
}],
|
||||
[Char('a'), CONTROL, "scene_add", "add a new scene", |app: &mut App| {
|
||||
app.arranger.scene_add(None)?;
|
||||
Ok(true)
|
||||
}],
|
||||
[Char('t'), CONTROL, "track_add", "add a new track", |app: &mut App| {
|
||||
app.arranger.track_add(None)?;
|
||||
Ok(true)
|
||||
}],
|
||||
});
|
||||
|
||||
/// Generic key bindings for views that support focus.
|
||||
pub const KEYMAP_FOCUS: &'static [KeyBinding<App>] = keymap!(App {
|
||||
[Char(';'), NONE, "command", "open command palette", |_: &mut App| {
|
||||
*MODAL.lock().unwrap() = Some(Box::new(HelpModal::new()));
|
||||
Ok(true)
|
||||
}],
|
||||
[Tab, NONE, "focus_next", "focus next area", focus_next],
|
||||
[Tab, SHIFT, "focus_prev", "focus previous area", focus_prev],
|
||||
[Esc, NONE, "focus_exit", "unfocus", |app: &mut App|{
|
||||
app.entered = false;
|
||||
app.transport.entered = app.entered;
|
||||
//app.arranger.entered = app.entered;
|
||||
app.arranger.sequencer_mut().map(|s|s.entered = app.entered);
|
||||
Ok(true)
|
||||
}],
|
||||
[Enter, NONE, "focus_enter", "activate item at cursor", |app: &mut App|{
|
||||
app.entered = true;
|
||||
app.transport.entered = app.entered;
|
||||
//app.arranger.entered = app.entered;
|
||||
app.arranger.sequencer_mut().map(|s|s.entered = app.entered);
|
||||
Ok(true)
|
||||
}],
|
||||
});
|
||||
|
||||
pub fn focus_next (app: &mut App) -> Usually<bool> {
|
||||
app.section.next();
|
||||
app.transport.focused = app.section == AppFocus::Transport;
|
||||
app.transport.entered = app.entered;
|
||||
//app.arranger.focused = app.section == AppFocus::Arranger;
|
||||
//app.arranger.entered = app.entered;
|
||||
app.arranger.sequencer_mut().map(|s|{
|
||||
s.focused = app.section == AppFocus::Sequencer;
|
||||
s.entered = app.entered;
|
||||
});
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn focus_prev (app: &mut App) -> Usually<bool> {
|
||||
app.section.prev();
|
||||
app.transport.focused = app.section == AppFocus::Transport;
|
||||
app.transport.entered = app.entered;
|
||||
//app.arranger.focused = app.section == AppFocus::Arranger;
|
||||
//app.arranger.entered = app.entered;
|
||||
app.arranger.sequencer_mut().map(|s|{
|
||||
s.focused = app.section == AppFocus::Sequencer;
|
||||
s.entered = app.entered;
|
||||
});
|
||||
Ok(true)
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
78
crates/tek/src/core/collect.rs
Normal file
78
crates/tek/src/core/collect.rs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
use crate::*;
|
||||
|
||||
pub enum Collect<'a, E: Engine, const N: usize> {
|
||||
Callback(CallbackCollection<'a, E>),
|
||||
//Iterator(IteratorCollection<'a, E>),
|
||||
Array(ArrayCollection<'a, E, N>),
|
||||
Slice(SliceCollection<'a, E>),
|
||||
}
|
||||
|
||||
impl<'a, E: Engine, const N: usize> Collect<'a, E, N> {
|
||||
pub fn iter (&'a self) -> CollectIterator<'a, E, N> {
|
||||
CollectIterator(0, &self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, E: Engine, const N: usize> From<CallbackCollection<'a, E>> for Collect<'a, E, N> {
|
||||
fn from (callback: CallbackCollection<'a, E>) -> Self {
|
||||
Self::Callback(callback)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, E: Engine, const N: usize> From<SliceCollection<'a, E>> for Collect<'a, E, N> {
|
||||
fn from (slice: SliceCollection<'a, E>) -> Self {
|
||||
Self::Slice(slice)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, E: Engine, const N: usize> From<ArrayCollection<'a, E, N>> for Collect<'a, E, N>{
|
||||
fn from (array: ArrayCollection<'a, E, N>) -> Self {
|
||||
Self::Array(array)
|
||||
}
|
||||
}
|
||||
|
||||
type CallbackCollection<'a, E> =
|
||||
&'a dyn Fn(&'a mut dyn FnMut(&dyn Render<E>)->Usually<()>);
|
||||
|
||||
//type IteratorCollection<'a, E> =
|
||||
//&'a mut dyn Iterator<Item = dyn Render<E>>;
|
||||
|
||||
type SliceCollection<'a, E> =
|
||||
&'a [&'a dyn Render<E>];
|
||||
|
||||
type ArrayCollection<'a, E, const N: usize> =
|
||||
[&'a dyn Render<E>; N];
|
||||
|
||||
pub struct CollectIterator<'a, E: Engine, const N: usize>(usize, &'a Collect<'a, E, N>);
|
||||
|
||||
impl<'a, E: Engine, const N: usize> Iterator for CollectIterator<'a, E, N> {
|
||||
type Item = &'a dyn Render<E>;
|
||||
fn next (&mut self) -> Option<Self::Item> {
|
||||
match self.1 {
|
||||
Collect::Callback(callback) => {
|
||||
todo!()
|
||||
},
|
||||
//Collection::Iterator(iterator) => {
|
||||
//iterator.next()
|
||||
//},
|
||||
Collect::Array(array) => {
|
||||
if let Some(item) = array.get(self.0) {
|
||||
self.0 += 1;
|
||||
//Some(item)
|
||||
None
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Collect::Slice(slice) => {
|
||||
if let Some(item) = slice.get(self.0) {
|
||||
self.0 += 1;
|
||||
//Some(item)
|
||||
None
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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');
|
||||
|
||||
}
|
||||
}
|
||||
58
crates/tek/src/core/input.rs
Normal file
58
crates/tek/src/core/input.rs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
use crate::*;
|
||||
|
||||
/// Current input state
|
||||
pub trait Input<E: Engine> {
|
||||
/// Type of input event
|
||||
type Event;
|
||||
/// Currently handled event
|
||||
fn event (&self) -> &Self::Event;
|
||||
/// Whether component should exit
|
||||
fn is_done (&self) -> bool;
|
||||
/// Mark component as done
|
||||
fn done (&self);
|
||||
}
|
||||
|
||||
/// Handle input
|
||||
pub trait Handle<E: Engine>: Send + Sync {
|
||||
fn handle (&mut self, context: &E::Input) -> Perhaps<E::Handled>;
|
||||
}
|
||||
|
||||
impl<E: Engine, H: Handle<E>> Handle<E> for &mut H {
|
||||
fn handle (&mut self, context: &E::Input) -> Perhaps<E::Handled> {
|
||||
(*self).handle(context)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine, H: Handle<E>> Handle<E> for Option<H> {
|
||||
fn handle (&mut self, context: &E::Input) -> Perhaps<E::Handled> {
|
||||
if let Some(ref mut handle) = self {
|
||||
handle.handle(context)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<H, E: Engine> Handle<E> for Mutex<H> where H: Handle<E> {
|
||||
fn handle (&mut self, context: &E::Input) -> Perhaps<E::Handled> {
|
||||
self.lock().unwrap().handle(context)
|
||||
}
|
||||
}
|
||||
|
||||
impl<H, E: Engine> Handle<E> for Arc<Mutex<H>> where H: Handle<E> {
|
||||
fn handle (&mut self, context: &E::Input) -> Perhaps<E::Handled> {
|
||||
self.lock().unwrap().handle(context)
|
||||
}
|
||||
}
|
||||
|
||||
impl<H, E: Engine> Handle<E> for RwLock<H> where H: Handle<E> {
|
||||
fn handle (&mut self, context: &E::Input) -> Perhaps<E::Handled> {
|
||||
self.write().unwrap().handle(context)
|
||||
}
|
||||
}
|
||||
|
||||
impl<H, E: Engine> Handle<E> for Arc<RwLock<H>> where H: Handle<E> {
|
||||
fn handle (&mut self, context: &E::Input) -> Perhaps<E::Handled> {
|
||||
self.write().unwrap().handle(context)
|
||||
}
|
||||
}
|
||||
21
crates/tek/src/core/mod.rs
Normal file
21
crates/tek/src/core/mod.rs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
use crate::*;
|
||||
|
||||
submod! {
|
||||
//tui
|
||||
audio
|
||||
color
|
||||
collect
|
||||
command
|
||||
edn
|
||||
engine
|
||||
focus
|
||||
input
|
||||
output
|
||||
pitch
|
||||
space
|
||||
time
|
||||
}
|
||||
|
||||
testmod! {
|
||||
test
|
||||
}
|
||||
161
crates/tek/src/core/output.rs
Normal file
161
crates/tek/src/core/output.rs
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
use crate::*;
|
||||
|
||||
/// Rendering target
|
||||
pub trait Output<E: Engine> {
|
||||
/// Current output area
|
||||
fn area (&self) -> E::Area;
|
||||
/// Mutable pointer to area
|
||||
fn area_mut (&mut self) -> &mut E::Area;
|
||||
/// Render widget in area
|
||||
fn render_in (&mut self, area: E::Area, widget: &dyn Render<E>) -> Usually<()>;
|
||||
}
|
||||
|
||||
/// Cast to dynamic pointer
|
||||
pub fn widget <E: Engine, T: Render<E>> (w: &T) -> &dyn Render<E> {
|
||||
w as &dyn Render<E>
|
||||
}
|
||||
|
||||
/// A [Render] that contains other [Render]s
|
||||
pub trait Content<E: Engine>: Send + Sync {
|
||||
fn content (&self) -> impl Render<E>;
|
||||
}
|
||||
|
||||
//impl<E: Engine, C: Content<E>> Render<E> for &C {
|
||||
//fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
//self.content().min_size(to)
|
||||
//}
|
||||
//fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
//match self.min_size(to.area().wh().into())? {
|
||||
//Some(wh) => to.render_in(to.area().clip(wh).into(), &self.content()),
|
||||
//None => Ok(())
|
||||
//}
|
||||
//}
|
||||
//}
|
||||
|
||||
/*
|
||||
|
||||
/// Every struct that has [Content] is a renderable [Render].
|
||||
impl<E: Engine, C: Content<E>> Render<E> for C {
|
||||
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
self.content().min_size(to)
|
||||
}
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
match self.min_size(to.area().wh().into())? {
|
||||
Some(wh) => to.render_in(to.area().clip(wh).into(), &self.content()),
|
||||
None => Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/// A renderable component
|
||||
pub trait Render<E: Engine>: Send + Sync {
|
||||
/// Minimum size to use
|
||||
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
Ok(Some(to))
|
||||
}
|
||||
/// Draw to output render target
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()>;
|
||||
}
|
||||
|
||||
impl<E: Engine, R: Render<E>> Render<E> for &R {
|
||||
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
(*self).min_size(to)
|
||||
}
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
(*self).render(to)
|
||||
}
|
||||
}
|
||||
|
||||
//impl<E: Engine> Render<E> for &dyn Render<E> {
|
||||
//fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
//(*self).min_size(to)
|
||||
//}
|
||||
//fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
//(*self).render(to)
|
||||
//}
|
||||
//}
|
||||
|
||||
impl<E: Engine> Render<E> for &mut dyn Render<E> {
|
||||
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
(*self).min_size(to)
|
||||
}
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
(*self).render(to)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, E: Engine> Render<E> for Box<dyn Render<E> + 'a> {
|
||||
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
(**self).min_size(to)
|
||||
}
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
(**self).render(to)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine, W: Render<E>> Render<E> for Arc<W> {
|
||||
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
self.as_ref().min_size(to)
|
||||
}
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
self.as_ref().render(to)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine, W: Render<E>> Render<E> for Mutex<W> {
|
||||
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
self.lock().unwrap().min_size(to)
|
||||
}
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
self.lock().unwrap().render(to)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine, W: Render<E>> Render<E> for RwLock<W> {
|
||||
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
self.read().unwrap().min_size(to)
|
||||
}
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
self.read().unwrap().render(to)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine, W: Render<E>> Render<E> for Option<W> {
|
||||
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
Ok(self.as_ref().map(|widget|widget.min_size(to)).transpose()?.flatten())
|
||||
}
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
self.as_ref().map(|widget|widget.render(to)).unwrap_or(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
/// A custom [Render] defined by passing layout and render closures in place.
|
||||
pub struct Widget<
|
||||
E: Engine,
|
||||
L: Send + Sync + Fn(E::Size)->Perhaps<E::Size>,
|
||||
R: Send + Sync + Fn(&mut E::Output)->Usually<()>
|
||||
>(L, R, PhantomData<E>);
|
||||
|
||||
impl<
|
||||
E: Engine,
|
||||
L: Send + Sync + Fn(E::Size)->Perhaps<E::Size>,
|
||||
R: Send + Sync + Fn(&mut E::Output)->Usually<()>
|
||||
> Widget<E, L, R> {
|
||||
pub fn new (layout: L, render: R) -> Self {
|
||||
Self(layout, render, Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<
|
||||
E: Engine,
|
||||
L: Send + Sync + Fn(E::Size)->Perhaps<E::Size>,
|
||||
R: Send + Sync + Fn(&mut E::Output)->Usually<()>
|
||||
> Render<E> for Widget<E, L, R> {
|
||||
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
self.0(to)
|
||||
}
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
self.1(to)
|
||||
}
|
||||
}
|
||||
23
crates/tek/src/core/pitch.rs
Normal file
23
crates/tek/src/core/pitch.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
use crate::*;
|
||||
use midly::num::u7;
|
||||
|
||||
pub fn to_note_name (n: usize) -> &'static str {
|
||||
if n > 127 {
|
||||
panic!("to_note_name({n}): must be 0-127");
|
||||
}
|
||||
MIDI_NOTE_NAMES[n]
|
||||
}
|
||||
|
||||
pub const MIDI_NOTE_NAMES: [&'static str;128] = [
|
||||
"C0", "C#0", "D0", "D#0", "E0", "F0", "F#0", "G0", "G#0", "A0", "A#0", "B0",
|
||||
"C1", "C#1", "D1", "D#1", "E1", "F1", "F#1", "G1", "G#1", "A1", "A#1", "B1",
|
||||
"C2", "C#2", "D2", "D#2", "E2", "F2", "F#2", "G2", "G#2", "A2", "A#2", "B2",
|
||||
"C3", "C#3", "D3", "D#3", "E3", "F3", "F#3", "G3", "G#3", "A3", "A#3", "B3",
|
||||
"C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4",
|
||||
"C5", "C#5", "D5", "D#5", "E5", "F5", "F#5", "G5", "G#5", "A5", "A#5", "B5",
|
||||
"C6", "C#6", "D6", "D#6", "E6", "F6", "F#6", "G6", "G#6", "A6", "A#6", "B6",
|
||||
"C7", "C#7", "D7", "D#7", "E7", "F7", "F#7", "G7", "G#7", "A7", "A#7", "B7",
|
||||
"C8", "C#8", "D8", "D#8", "E8", "F8", "F#8", "G8", "G#8", "A8", "A#8", "B8",
|
||||
"C9", "C#9", "D9", "D#9", "E9", "F9", "F#9", "G9", "G#9", "A9", "A#9", "B9",
|
||||
"C10", "C#10", "D10", "D#10", "E10", "F10", "F#10", "G10",
|
||||
];
|
||||
157
crates/tek/src/core/space.rs
Normal file
157
crates/tek/src/core/space.rs
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
use crate::*;
|
||||
|
||||
/// Standard numeric type.
|
||||
pub trait Coordinate: Send + Sync + Copy
|
||||
+ Add<Self, Output=Self>
|
||||
+ Sub<Self, Output=Self>
|
||||
+ Mul<Self, Output=Self>
|
||||
+ Div<Self, Output=Self>
|
||||
+ Ord + PartialEq + Eq
|
||||
+ Debug + Display + Default
|
||||
+ From<u16> + Into<u16>
|
||||
+ Into<usize>
|
||||
+ Into<f64>
|
||||
{
|
||||
fn minus (self, other: Self) -> Self {
|
||||
if self >= other {
|
||||
self - other
|
||||
} else {
|
||||
0.into()
|
||||
}
|
||||
}
|
||||
fn ZERO () -> Self {
|
||||
0.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Coordinate for T where T: Send + Sync + Copy
|
||||
+ Add<Self, Output=Self>
|
||||
+ Sub<Self, Output=Self>
|
||||
+ Mul<Self, Output=Self>
|
||||
+ Div<Self, Output=Self>
|
||||
+ Ord + PartialEq + Eq
|
||||
+ Debug + Display + Default
|
||||
+ From<u16> + Into<u16>
|
||||
+ Into<usize>
|
||||
+ Into<f64>
|
||||
{}
|
||||
|
||||
// TODO: return impl Point and impl Size instead of [N;x]
|
||||
// to disambiguate between usage of 2-"tuple"s
|
||||
|
||||
pub trait Size<N: Coordinate> {
|
||||
fn x (&self) -> N;
|
||||
fn y (&self) -> N;
|
||||
#[inline] fn w (&self) -> N { self.x() }
|
||||
#[inline] fn h (&self) -> N { self.y() }
|
||||
#[inline] fn wh (&self) -> [N;2] { [self.x(), self.y()] }
|
||||
#[inline] fn clip_w (&self, w: N) -> [N;2] { [self.w().min(w.into()), self.h()] }
|
||||
#[inline] fn clip_h (&self, h: N) -> [N;2] { [self.w(), self.h().min(h.into())] }
|
||||
#[inline] fn expect_min (&self, w: N, h: N) -> Usually<&Self> {
|
||||
if self.w() < w || self.h() < h {
|
||||
Err(format!("min {w}x{h}").into())
|
||||
} else {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<N: Coordinate> Size<N> for (N, N) {
|
||||
fn x (&self) -> N { self.0 }
|
||||
fn y (&self) -> N { self.1 }
|
||||
}
|
||||
impl<N: Coordinate> Size<N> for [N;2] {
|
||||
fn x (&self) -> N { self[0] }
|
||||
fn y (&self) -> N { self[1] }
|
||||
}
|
||||
|
||||
pub trait Area<N: Coordinate>: Copy {
|
||||
fn x (&self) -> N;
|
||||
fn y (&self) -> N;
|
||||
fn w (&self) -> N;
|
||||
fn h (&self) -> N;
|
||||
fn x2 (&self) -> N { self.x() + self.w() }
|
||||
fn y2 (&self) -> N { self.y() + self.h() }
|
||||
#[inline] fn wh (&self) -> [N;2] { [self.w(), self.h()] }
|
||||
#[inline] fn xywh (&self) -> [N;4] { [self.x(), self.y(), self.w(), self.h()] }
|
||||
#[inline] fn lrtb (&self) -> [N;4] { [self.x(), self.x2(), self.y(), self.y2()] }
|
||||
#[inline] fn push_x (&self, x: N) -> [N;4] { [self.x() + x, self.y(), self.w(), self.h()] }
|
||||
#[inline] fn push_y (&self, y: N) -> [N;4] { [self.x(), self.y() + y, self.w(), self.h()] }
|
||||
#[inline] fn shrink_x (&self, x: N) -> [N;4] { [self.x(), self.y(), self.w() - x, self.h()] }
|
||||
#[inline] fn shrink_y (&self, y: N) -> [N;4] { [self.x(), self.y(), self.w(), self.h() - y] }
|
||||
#[inline] fn set_w (&self, w: N) -> [N;4] { [self.x(), self.y(), w, self.h()] }
|
||||
#[inline] fn set_h (&self, h: N) -> [N;4] { [self.x(), self.y(), self.w(), h] }
|
||||
#[inline] fn clip_h (&self, h: N) -> [N;4] {
|
||||
[self.x(), self.y(), self.w(), self.h().min(h.into())]
|
||||
}
|
||||
#[inline] fn clip_w (&self, w: N) -> [N;4] {
|
||||
[self.x(), self.y(), self.w().min(w.into()), self.h()]
|
||||
}
|
||||
#[inline] fn clip (&self, wh: impl Size<N>) -> [N;4] {
|
||||
[self.x(), self.y(), wh.w(), wh.h()]
|
||||
}
|
||||
#[inline] fn expect_min (&self, w: N, h: N) -> Usually<&Self> {
|
||||
if self.w() < w || self.h() < h {
|
||||
Err(format!("min {w}x{h}").into())
|
||||
} else {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
#[inline] fn split_fixed (&self, direction: Direction, a: N) -> ([N;4],[N;4]) {
|
||||
match direction {
|
||||
Direction::Up => (
|
||||
[self.x(), (self.y()+self.h()).minus(a), self.w(), a],
|
||||
[self.x(), self.y(), self.w(), self.h().minus(a)],
|
||||
),
|
||||
Direction::Down => (
|
||||
[self.x(), self.y(), self.w(), a],
|
||||
[self.x(), self.y() + a, self.w(), self.h().minus(a)],
|
||||
),
|
||||
Direction::Right => (
|
||||
[self.x(), self.y(), a, self.h()],
|
||||
[self.x() + a, self.y(), self.w().minus(a), self.h()],
|
||||
),
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<N: Coordinate> Area<N> for (N, N, N, N) {
|
||||
#[inline] fn x (&self) -> N { self.0 }
|
||||
#[inline] fn y (&self) -> N { self.1 }
|
||||
#[inline] fn w (&self) -> N { self.2 }
|
||||
#[inline] fn h (&self) -> N { self.3 }
|
||||
}
|
||||
|
||||
impl<N: Coordinate> Area<N> for [N;4] {
|
||||
#[inline] fn x (&self) -> N { self[0] }
|
||||
#[inline] fn y (&self) -> N { self[1] }
|
||||
#[inline] fn w (&self) -> N { self[2] }
|
||||
#[inline] fn h (&self) -> N { self[3] }
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq)]
|
||||
pub enum Direction { Up, Down, Left, Right, }
|
||||
impl Direction {
|
||||
pub fn is_up (&self) -> bool { match self { Self::Up => true, _ => false } }
|
||||
pub fn is_down (&self) -> bool { match self { Self::Down => true, _ => false } }
|
||||
pub fn is_left (&self) -> bool { match self { Self::Left => true, _ => false } }
|
||||
pub fn is_right (&self) -> bool { match self { Self::Right => true, _ => false } }
|
||||
/// Return next direction clockwise
|
||||
pub fn cw (&self) -> Self {
|
||||
match self {
|
||||
Self::Up => Self::Right,
|
||||
Self::Down => Self::Left,
|
||||
Self::Left => Self::Up,
|
||||
Self::Right => Self::Down,
|
||||
}
|
||||
}
|
||||
/// Return next direction counterclockwise
|
||||
pub fn ccw (&self) -> Self {
|
||||
match self {
|
||||
Self::Up => Self::Left,
|
||||
Self::Down => Self::Right,
|
||||
Self::Left => Self::Down,
|
||||
Self::Right => Self::Up,
|
||||
}
|
||||
}
|
||||
}
|
||||
190
crates/tek/src/core/test.rs
Normal file
190
crates/tek/src/core/test.rs
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
use crate::*;
|
||||
|
||||
struct TestEngine([u16;4], Vec<Vec<char>>);
|
||||
|
||||
impl Engine for TestEngine {
|
||||
type Unit = u16;
|
||||
type Size = [Self::Unit;2];
|
||||
type Area = [Self::Unit;4];
|
||||
type Input = Self;
|
||||
type Handled = bool;
|
||||
fn exited (&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn area (&self) -> Self::Area {
|
||||
self.0
|
||||
}
|
||||
fn area_mut (&mut self) -> &mut Self::Area {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct TestArea(u16, u16);
|
||||
|
||||
impl Render for TestArea {
|
||||
type Engine = TestEngine;
|
||||
fn min_size (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
||||
Ok(Some([to[0], to[1], self.0, self.1]))
|
||||
}
|
||||
fn render (&self, to: &mut Self::Engine) -> Perhaps<[u16;4]> {
|
||||
if let Some(layout) = self.layout(to.area())? {
|
||||
for y in layout.y()..layout.y()+layout.h()-1 {
|
||||
for x in layout.x()..layout.x()+layout.w()-1 {
|
||||
to.1[y as usize][x as usize] = '*';
|
||||
}
|
||||
}
|
||||
Ok(Some(layout))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_plus_minus () -> Usually<()> {
|
||||
let area = [0, 0, 10, 10];
|
||||
let engine = TestEngine(area, vec![vec![' ';10];10]);
|
||||
let test = TestArea(4, 4);
|
||||
assert_eq!(test.layout(area)?, Some([0, 0, 4, 4]));
|
||||
assert_eq!(Push::X(1, test).layout(area)?, Some([1, 0, 4, 4]));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_outset_align () -> Usually<()> {
|
||||
let area = [0, 0, 10, 10];
|
||||
let engine = TestEngine(area, vec![vec![' ';10];10]);
|
||||
let test = TestArea(4, 4);
|
||||
assert_eq!(test.layout(area)?, Some([0, 0, 4, 4]));
|
||||
assert_eq!(Outset::X(1, test).layout(area)?, Some([0, 0, 6, 4]));
|
||||
assert_eq!(Align::X(test).layout(area)?, Some([3, 0, 4, 4]));
|
||||
assert_eq!(Align::X(Outset::X(1, test)).layout(area)?, Some([2, 0, 6, 4]));
|
||||
assert_eq!(Outset::X(1, Align::X(test)).layout(area)?, Some([2, 0, 6, 4]));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
//#[test]
|
||||
//fn test_misc () -> Usually<()> {
|
||||
//let area: [u16;4] = [0, 0, 10, 10];
|
||||
//let test = TestArea(4, 4);
|
||||
//assert_eq!(test.layout(area)?,
|
||||
//Some([0, 0, 4, 4]));
|
||||
//assert_eq!(Align::Center(test).layout(area)?,
|
||||
//Some([3, 3, 4, 4]));
|
||||
//assert_eq!(Align::Center(Stack::down(|add|{
|
||||
//add(&test)?;
|
||||
//add(&test)
|
||||
//})).layout(area)?,
|
||||
//Some([3, 1, 4, 8]));
|
||||
//assert_eq!(Align::Center(Stack::down(|add|{
|
||||
//add(&Outset::XY(2, 2, test))?;
|
||||
//add(&test)
|
||||
//})).layout(area)?,
|
||||
//Some([2, 0, 6, 10]));
|
||||
//assert_eq!(Align::Center(Stack::down(|add|{
|
||||
//add(&Outset::XY(2, 2, test))?;
|
||||
//add(&Inset::XY(2, 2, test))
|
||||
//})).layout(area)?,
|
||||
//Some([2, 1, 6, 8]));
|
||||
//assert_eq!(Stack::down(|add|{
|
||||
//add(&Outset::XY(2, 2, test))?;
|
||||
//add(&Inset::XY(2, 2, test))
|
||||
//}).layout(area)?,
|
||||
//Some([0, 0, 6, 8]));
|
||||
//assert_eq!(Stack::right(|add|{
|
||||
//add(&Stack::down(|add|{
|
||||
//add(&Outset::XY(2, 2, test))?;
|
||||
//add(&Inset::XY(2, 2, test))
|
||||
//}))?;
|
||||
//add(&Align::Center(TestArea(2 ,2)))
|
||||
//}).layout(area)?,
|
||||
//Some([0, 0, 8, 8]));
|
||||
//Ok(())
|
||||
//}
|
||||
|
||||
//#[test]
|
||||
//fn test_offset () -> Usually<()> {
|
||||
//let area: [u16;4] = [50, 50, 100, 100];
|
||||
//let test = TestArea(3, 3);
|
||||
//assert_eq!(Push::X(1, test).layout(area)?, Some([51, 50, 3, 3]));
|
||||
//assert_eq!(Push::Y(1, test).layout(area)?, Some([50, 51, 3, 3]));
|
||||
//assert_eq!(Push::XY(1, 1, test).layout(area)?, Some([51, 51, 3, 3]));
|
||||
//Ok(())
|
||||
//}
|
||||
|
||||
//#[test]
|
||||
//fn test_outset () -> Usually<()> {
|
||||
//let area: [u16;4] = [50, 50, 100, 100];
|
||||
//let test = TestArea(3, 3);
|
||||
//assert_eq!(Outset::X(1, test).layout(area)?, Some([49, 50, 5, 3]));
|
||||
//assert_eq!(Outset::Y(1, test).layout(area)?, Some([50, 49, 3, 5]));
|
||||
//assert_eq!(Outset::XY(1, 1, test).layout(area)?, Some([49, 49, 5, 5]));
|
||||
//Ok(())
|
||||
//}
|
||||
|
||||
//#[test]
|
||||
//fn test_inset () -> Usually<()> {
|
||||
//let area: [u16;4] = [50, 50, 100, 100];
|
||||
//let test = TestArea(3, 3);
|
||||
//assert_eq!(Inset::X(1, test).layout(area)?, Some([51, 50, 1, 3]));
|
||||
//assert_eq!(Inset::Y(1, test).layout(area)?, Some([50, 51, 3, 1]));
|
||||
//assert_eq!(Inset::XY(1, 1, test).layout(area)?, Some([51, 51, 1, 1]));
|
||||
//Ok(())
|
||||
//}
|
||||
|
||||
//#[test]
|
||||
//fn test_stuff () -> Usually<()> {
|
||||
//let area: [u16;4] = [0, 0, 100, 100];
|
||||
//assert_eq!("1".layout(area)?,
|
||||
//Some([0, 0, 1, 1]));
|
||||
//assert_eq!("333".layout(area)?,
|
||||
//Some([0, 0, 3, 1]));
|
||||
//assert_eq!(Layers::new(|add|{add(&"1")?;add(&"333")}).layout(area)?,
|
||||
//Some([0, 0, 3, 1]));
|
||||
//assert_eq!(Stack::down(|add|{add(&"1")?;add(&"333")}).layout(area)?,
|
||||
//Some([0, 0, 3, 2]));
|
||||
//assert_eq!(Stack::right(|add|{add(&"1")?;add(&"333")}).layout(area)?,
|
||||
//Some([0, 0, 4, 1]));
|
||||
//assert_eq!(Stack::down(|add|{
|
||||
//add(&Stack::right(|add|{add(&"1")?;add(&"333")}))?;
|
||||
//add(&"55555")
|
||||
//}).layout(area)?,
|
||||
//Some([0, 0, 5, 2]));
|
||||
//let area: [u16;4] = [1, 1, 100, 100];
|
||||
//assert_eq!(Outset::X(1, Stack::right(|add|{add(&"1")?;add(&"333")})).layout(area)?,
|
||||
//Some([0, 1, 6, 1]));
|
||||
//assert_eq!(Outset::Y(1, Stack::right(|add|{add(&"1")?;add(&"333")})).layout(area)?,
|
||||
//Some([1, 0, 4, 3]));
|
||||
//assert_eq!(Outset::XY(1, 1, Stack::right(|add|{add(&"1")?;add(&"333")})).layout(area)?,
|
||||
//Some([0, 0, 6, 3]));
|
||||
//assert_eq!(Stack::down(|add|{
|
||||
//add(&Outset::XY(1, 1, "1"))?;
|
||||
//add(&Outset::XY(1, 1, "333"))
|
||||
//}).layout(area)?,
|
||||
//Some([1, 1, 5, 6]));
|
||||
//let area: [u16;4] = [1, 1, 95, 100];
|
||||
//assert_eq!(Align::Center(Stack::down(|add|{
|
||||
//add(&Outset::XY(1, 1, "1"))?;
|
||||
//add(&Outset::XY(1, 1, "333"))
|
||||
//})).layout(area)?,
|
||||
//Some([46, 48, 5, 6]));
|
||||
//assert_eq!(Align::Center(Stack::down(|add|{
|
||||
//add(&Layers::new(|add|{
|
||||
////add(&Outset::XY(1, 1, Background(Color::Rgb(0,128,0))))?;
|
||||
//add(&Outset::XY(1, 1, "1"))?;
|
||||
//add(&Outset::XY(1, 1, "333"))?;
|
||||
////add(&Background(Color::Rgb(0,128,0)))?;
|
||||
//Ok(())
|
||||
//}))?;
|
||||
//add(&Layers::new(|add|{
|
||||
////add(&Outset::XY(1, 1, Background(Color::Rgb(0,0,128))))?;
|
||||
//add(&Outset::XY(1, 1, "555"))?;
|
||||
//add(&Outset::XY(1, 1, "777777"))?;
|
||||
////add(&Background(Color::Rgb(0,0,128)))?;
|
||||
//Ok(())
|
||||
//}))
|
||||
//})).layout(area)?,
|
||||
//Some([46, 48, 5, 6]));
|
||||
//Ok(())
|
||||
//}
|
||||
450
crates/tek/src/core/time.rs
Normal file
450
crates/tek/src/core/time.rs
Normal file
|
|
@ -0,0 +1,450 @@
|
|||
use crate::*;
|
||||
use std::iter::Iterator;
|
||||
|
||||
pub const DEFAULT_PPQ: f64 = 96.0;
|
||||
/// FIXME: remove this and use PPQ from timebase everywhere:
|
||||
pub const PPQ: usize = 96;
|
||||
|
||||
/// A unit of time, represented as an atomic 64-bit float.
|
||||
///
|
||||
/// According to https://stackoverflow.com/a/873367, as per IEEE754,
|
||||
/// every integer between 1 and 2^53 can be represented exactly.
|
||||
/// This should mean that, even at 192kHz sampling rate, over 1 year of audio
|
||||
/// can be clocked in microseconds with f64 without losing precision.
|
||||
pub trait TimeUnit {
|
||||
/// Returns current value
|
||||
fn get (&self) -> f64;
|
||||
/// Sets new value, returns old
|
||||
fn set (&self, value: f64) -> f64;
|
||||
}
|
||||
/// Implement arithmetic for a unit of time
|
||||
macro_rules! impl_op {
|
||||
($T:ident, $Op:ident, $method:ident, |$a:ident,$b:ident|{$impl:expr}) => {
|
||||
impl $Op<Self> for $T {
|
||||
type Output = Self; #[inline] fn $method (self, other: Self) -> Self::Output {
|
||||
let $a = self.get(); let $b = other.get(); Self($impl.into())
|
||||
}
|
||||
}
|
||||
impl $Op<usize> for $T {
|
||||
type Output = Self; #[inline] fn $method (self, other: usize) -> Self::Output {
|
||||
let $a = self.get(); let $b = other as f64; Self($impl.into())
|
||||
}
|
||||
}
|
||||
impl $Op<f64> for $T {
|
||||
type Output = Self; #[inline] fn $method (self, other: f64) -> Self::Output {
|
||||
let $a = self.get(); let $b = other; Self($impl.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Define and implement a unit of time
|
||||
macro_rules! impl_time_unit {
|
||||
($T:ident) => {
|
||||
impl TimeUnit for $T {
|
||||
fn get (&self) -> f64 { self.0.load(Ordering::Relaxed) }
|
||||
fn set (&self, value: f64) -> f64 {
|
||||
let old = self.get();
|
||||
self.0.store(value, Ordering::Relaxed);
|
||||
old
|
||||
}
|
||||
}
|
||||
impl_op!($T, Add, add, |a, b|{a + b});
|
||||
impl_op!($T, Sub, sub, |a, b|{a - b});
|
||||
impl_op!($T, Mul, mul, |a, b|{a * b});
|
||||
impl_op!($T, Div, div, |a, b|{a / b});
|
||||
impl_op!($T, Rem, rem, |a, b|{a % b});
|
||||
impl From<f64> for $T { fn from (value: f64) -> Self { Self(value.into()) } }
|
||||
impl From<usize> for $T { fn from (value: usize) -> Self { Self((value as f64).into()) } }
|
||||
impl Into<f64> for $T { fn into (self) -> f64 { self.get() } }
|
||||
impl Into<usize> for $T { fn into (self) -> usize { self.get() as usize } }
|
||||
impl Into<f64> for &$T { fn into (self) -> f64 { self.get() } }
|
||||
impl Into<usize> for &$T { fn into (self) -> usize { self.get() as usize } }
|
||||
impl Clone for $T { fn clone (&self) -> Self { Self(self.get().into()) } }
|
||||
}
|
||||
}
|
||||
|
||||
/// Audio sample rate in Hz (samples per second)
|
||||
#[derive(Debug, Default)] pub struct SampleRate(AtomicF64);
|
||||
impl_time_unit!(SampleRate);
|
||||
impl SampleRate {
|
||||
/// Return the duration of a sample in microseconds (floating)
|
||||
#[inline] pub fn usec_per_sample (&self) -> f64 {
|
||||
1_000_000f64 / self.get()
|
||||
}
|
||||
/// Return the duration of a sample in microseconds (floating)
|
||||
#[inline] pub fn sample_per_usec (&self) -> f64 {
|
||||
self.get() / 1_000_000f64
|
||||
}
|
||||
/// Convert a number of samples to microseconds (floating)
|
||||
#[inline] pub fn samples_to_usec (&self, samples: f64) -> f64 {
|
||||
self.usec_per_sample() * samples
|
||||
}
|
||||
/// Convert a number of microseconds to samples (floating)
|
||||
#[inline] pub fn usecs_to_sample (&self, usecs: f64) -> f64 {
|
||||
self.sample_per_usec() * usecs
|
||||
}
|
||||
}
|
||||
|
||||
/// Tempo in beats per minute
|
||||
#[derive(Debug, Default)] pub struct BeatsPerMinute(AtomicF64);
|
||||
impl_time_unit!(BeatsPerMinute);
|
||||
|
||||
/// MIDI resolution in PPQ (pulses per quarter note)
|
||||
#[derive(Debug, Default)] pub struct PulsesPerQuaver(AtomicF64);
|
||||
impl_time_unit!(PulsesPerQuaver);
|
||||
|
||||
/// Timestamp in microseconds
|
||||
#[derive(Debug, Default)] pub struct Microsecond(AtomicF64);
|
||||
impl_time_unit!(Microsecond);
|
||||
impl Microsecond {
|
||||
#[inline] pub fn format_msu (&self) -> String {
|
||||
let usecs = self.get() as usize;
|
||||
let (seconds, msecs) = (usecs / 1000000, usecs / 1000 % 1000);
|
||||
let (minutes, seconds) = (seconds / 60, seconds % 60);
|
||||
format!("{minutes}:{seconds:02}:{msecs:03}")
|
||||
}
|
||||
}
|
||||
|
||||
/// Timestamp in audio samples
|
||||
#[derive(Debug, Default)] pub struct SampleCount(AtomicF64);
|
||||
impl_time_unit!(SampleCount);
|
||||
|
||||
/// Timestamp in MIDI pulses
|
||||
#[derive(Debug, Default)] pub struct Pulse(AtomicF64);
|
||||
impl_time_unit!(Pulse);
|
||||
|
||||
/// Quantization setting for launching clips
|
||||
#[derive(Debug, Default)] pub struct LaunchSync(AtomicF64);
|
||||
impl_time_unit!(LaunchSync);
|
||||
impl LaunchSync {
|
||||
pub fn next (&self) -> f64 {
|
||||
next_note_length(self.get() as usize) as f64
|
||||
}
|
||||
pub fn prev (&self) -> f64 {
|
||||
prev_note_length(self.get() as usize) as f64
|
||||
}
|
||||
}
|
||||
|
||||
/// Quantization setting for notes
|
||||
#[derive(Debug, Default)] pub struct Quantize(AtomicF64);
|
||||
impl_time_unit!(Quantize);
|
||||
impl Quantize {
|
||||
pub fn next (&self) -> f64 {
|
||||
next_note_length(self.get() as usize) as f64
|
||||
}
|
||||
pub fn prev (&self) -> f64 {
|
||||
prev_note_length(self.get() as usize) as f64
|
||||
}
|
||||
}
|
||||
|
||||
/// Temporal resolutions: sample rate, tempo, MIDI pulses per quaver (beat)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Timebase {
|
||||
/// Audio samples per second
|
||||
pub sr: SampleRate,
|
||||
/// MIDI beats per minute
|
||||
pub bpm: BeatsPerMinute,
|
||||
/// MIDI ticks per beat
|
||||
pub ppq: PulsesPerQuaver,
|
||||
}
|
||||
impl Timebase {
|
||||
/// Specify sample rate, BPM and PPQ
|
||||
pub fn new (
|
||||
s: impl Into<SampleRate>,
|
||||
b: impl Into<BeatsPerMinute>,
|
||||
p: impl Into<PulsesPerQuaver>
|
||||
) -> Self {
|
||||
Self { sr: s.into(), bpm: b.into(), ppq: p.into() }
|
||||
}
|
||||
/// Iterate over ticks between start and end.
|
||||
#[inline] pub fn pulses_between_samples (&self, start: usize, end: usize) -> TicksIterator {
|
||||
TicksIterator { spp: self.samples_per_pulse(), sample: start, start, end }
|
||||
}
|
||||
/// Return the duration fo a beat in microseconds
|
||||
#[inline] pub fn usec_per_beat (&self) -> f64 { 60_000_000f64 / self.bpm.get() }
|
||||
/// Return the number of beats in a second
|
||||
#[inline] pub fn beat_per_second (&self) -> f64 { self.bpm.get() / 60f64 }
|
||||
/// Return the number of microseconds corresponding to a note of the given duration
|
||||
#[inline] pub fn note_to_usec (&self, (num, den): (f64, f64)) -> f64 {
|
||||
4.0 * self.usec_per_beat() * num / den
|
||||
}
|
||||
/// Return duration of a pulse in microseconds (BPM-dependent)
|
||||
#[inline] pub fn pulse_per_usec (&self) -> f64 { self.ppq.get() / self.usec_per_beat() }
|
||||
/// Return duration of a pulse in microseconds (BPM-dependent)
|
||||
#[inline] pub fn usec_per_pulse (&self) -> f64 { self.usec_per_beat() / self.ppq.get() }
|
||||
/// Return number of pulses to which a number of microseconds corresponds (BPM-dependent)
|
||||
#[inline] pub fn usecs_to_pulse (&self, usec: f64) -> f64 { usec * self.pulse_per_usec() }
|
||||
/// Convert a number of pulses to a sample number (SR- and BPM-dependent)
|
||||
#[inline] pub fn pulses_to_usec (&self, pulse: f64) -> f64 { pulse / self.usec_per_pulse() }
|
||||
/// Return number of pulses in a second (BPM-dependent)
|
||||
#[inline] pub fn pulses_per_second (&self) -> f64 { self.beat_per_second() * self.ppq.get() }
|
||||
/// Return fraction of a pulse to which a sample corresponds (SR- and BPM-dependent)
|
||||
#[inline] pub fn pulses_per_sample (&self) -> f64 {
|
||||
self.usec_per_pulse() / self.sr.usec_per_sample()
|
||||
}
|
||||
/// Return number of samples in a pulse (SR- and BPM-dependent)
|
||||
#[inline] pub fn samples_per_pulse (&self) -> f64 {
|
||||
self.sr.get() / self.pulses_per_second()
|
||||
}
|
||||
/// Convert a number of pulses to a sample number (SR- and BPM-dependent)
|
||||
#[inline] pub fn pulses_to_sample (&self, p: f64) -> f64 {
|
||||
self.pulses_per_sample() * p
|
||||
}
|
||||
/// Convert a number of samples to a pulse number (SR- and BPM-dependent)
|
||||
#[inline] pub fn samples_to_pulse (&self, s: f64) -> f64 {
|
||||
s / self.pulses_per_sample()
|
||||
}
|
||||
/// Return the number of samples corresponding to a note of the given duration
|
||||
#[inline] pub fn note_to_samples (&self, note: (f64, f64)) -> f64 {
|
||||
self.usec_to_sample(self.note_to_usec(note))
|
||||
}
|
||||
/// Return the number of samples corresponding to the given number of microseconds
|
||||
#[inline] pub fn usec_to_sample (&self, usec: f64) -> f64 {
|
||||
usec * self.sr.get() / 1000f64
|
||||
}
|
||||
/// Return the quantized position of a moment in time given a step
|
||||
#[inline] pub fn quantize (&self, step: (f64, f64), time: f64) -> (f64, f64) {
|
||||
let step = self.note_to_usec(step);
|
||||
(time / step, time % step)
|
||||
}
|
||||
/// Quantize a collection of events
|
||||
#[inline] pub fn quantize_into <E: Iterator<Item=(f64, f64)> + Sized, T> (
|
||||
&self, step: (f64, f64), events: E
|
||||
) -> Vec<(f64, f64)> {
|
||||
events.map(|(time, event)|(self.quantize(step, time).0, event)).collect()
|
||||
}
|
||||
/// Format a number of pulses into Beat.Bar.Pulse starting from 0
|
||||
#[inline] pub fn format_beats_0 (&self, pulse: f64) -> String {
|
||||
let pulse = pulse as usize;
|
||||
let ppq = self.ppq.get() as usize;
|
||||
let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) };
|
||||
format!("{}.{}.{pulses:02}", beats / 4, beats % 4)
|
||||
}
|
||||
/// Format a number of pulses into Beat.Bar starting from 0
|
||||
#[inline] pub fn format_beats_0_short (&self, pulse: f64) -> String {
|
||||
let pulse = pulse as usize;
|
||||
let ppq = self.ppq.get() as usize;
|
||||
let beats = if ppq > 0 { pulse / ppq } else { 0 };
|
||||
format!("{}.{}", beats / 4, beats % 4)
|
||||
}
|
||||
/// Format a number of pulses into Beat.Bar.Pulse starting from 1
|
||||
#[inline] pub fn format_beats_1 (&self, pulse: f64) -> String {
|
||||
let pulse = pulse as usize;
|
||||
let ppq = self.ppq.get() as usize;
|
||||
let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) };
|
||||
format!("{}.{}.{pulses:02}", beats / 4 + 1, beats % 4 + 1)
|
||||
}
|
||||
/// Format a number of pulses into Beat.Bar.Pulse starting from 1
|
||||
#[inline] pub fn format_beats_1_short (&self, pulse: f64) -> String {
|
||||
let pulse = pulse as usize;
|
||||
let ppq = self.ppq.get() as usize;
|
||||
let beats = if ppq > 0 { pulse / ppq } else { 0 };
|
||||
format!("{}.{}", beats / 4 + 1, beats % 4 + 1)
|
||||
}
|
||||
}
|
||||
impl Default for Timebase {
|
||||
fn default () -> Self { Self::new(48000f64, 150f64, DEFAULT_PPQ) }
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Moment2 {
|
||||
None,
|
||||
Zero,
|
||||
Usec(Microsecond),
|
||||
Sample(SampleCount),
|
||||
Pulse(Pulse),
|
||||
}
|
||||
|
||||
/// A point in time in all time scales (microsecond, sample, MIDI pulse)
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Moment {
|
||||
pub timebase: Arc<Timebase>,
|
||||
/// Current time in microseconds
|
||||
pub usec: Microsecond,
|
||||
/// Current time in audio samples
|
||||
pub sample: SampleCount,
|
||||
/// Current time in MIDI pulses
|
||||
pub pulse: Pulse,
|
||||
}
|
||||
impl Moment {
|
||||
pub fn zero (timebase: &Arc<Timebase>) -> Self {
|
||||
Self { usec: 0.into(), sample: 0.into(), pulse: 0.into(), timebase: timebase.clone() }
|
||||
}
|
||||
pub fn from_usec (timebase: &Arc<Timebase>, usec: f64) -> Self {
|
||||
Self {
|
||||
usec: usec.into(),
|
||||
sample: timebase.sr.usecs_to_sample(usec).into(),
|
||||
pulse: timebase.usecs_to_pulse(usec).into(),
|
||||
timebase: timebase.clone(),
|
||||
}
|
||||
}
|
||||
pub fn from_sample (timebase: &Arc<Timebase>, sample: f64) -> Self {
|
||||
Self {
|
||||
sample: sample.into(),
|
||||
usec: timebase.sr.samples_to_usec(sample).into(),
|
||||
pulse: timebase.samples_to_pulse(sample).into(),
|
||||
timebase: timebase.clone(),
|
||||
}
|
||||
}
|
||||
pub fn from_pulse (timebase: &Arc<Timebase>, pulse: f64) -> Self {
|
||||
Self {
|
||||
pulse: pulse.into(),
|
||||
sample: timebase.pulses_to_sample(pulse).into(),
|
||||
usec: timebase.pulses_to_usec(pulse).into(),
|
||||
timebase: timebase.clone(),
|
||||
}
|
||||
}
|
||||
#[inline] pub fn update_from_usec (&self, usec: f64) {
|
||||
self.usec.set(usec);
|
||||
self.pulse.set(self.timebase.usecs_to_pulse(usec));
|
||||
self.sample.set(self.timebase.sr.usecs_to_sample(usec));
|
||||
}
|
||||
#[inline] pub fn update_from_sample (&self, sample: f64) {
|
||||
self.usec.set(self.timebase.sr.samples_to_usec(sample));
|
||||
self.pulse.set(self.timebase.samples_to_pulse(sample));
|
||||
self.sample.set(sample);
|
||||
}
|
||||
#[inline] pub fn update_from_pulse (&self, pulse: f64) {
|
||||
self.usec.set(self.timebase.pulses_to_usec(pulse));
|
||||
self.pulse.set(pulse);
|
||||
self.sample.set(self.timebase.pulses_to_sample(pulse));
|
||||
}
|
||||
#[inline] pub fn format_beat (&self) -> String {
|
||||
self.timebase.format_beats_1(self.pulse.get())
|
||||
}
|
||||
}
|
||||
/// Iterator that emits subsequent ticks within a range.
|
||||
pub struct TicksIterator {
|
||||
spp: f64,
|
||||
sample: usize,
|
||||
start: usize,
|
||||
end: usize,
|
||||
}
|
||||
impl Iterator for TicksIterator {
|
||||
type Item = (usize, usize);
|
||||
fn next (&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
if self.sample > self.end { return None }
|
||||
let spp = self.spp;
|
||||
let sample = self.sample as f64;
|
||||
let start = self.start;
|
||||
let end = self.end;
|
||||
self.sample += 1;
|
||||
//println!("{spp} {sample} {start} {end}");
|
||||
let jitter = sample.rem_euclid(spp); // ramps
|
||||
let next_jitter = (sample + 1.0).rem_euclid(spp);
|
||||
if jitter > next_jitter { // at crossing:
|
||||
let time = (sample as usize) % (end as usize-start as usize);
|
||||
let tick = (sample / spp) as usize;
|
||||
return Some((time, tick))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// (pulses, name), assuming 96 PPQ
|
||||
pub const NOTE_DURATIONS: [(usize, &str);26] = [
|
||||
(1, "1/384"),
|
||||
(2, "1/192"),
|
||||
(3, "1/128"),
|
||||
(4, "1/96"),
|
||||
(6, "1/64"),
|
||||
(8, "1/48"),
|
||||
(12, "1/32"),
|
||||
(16, "1/24"),
|
||||
(24, "1/16"),
|
||||
(32, "1/12"),
|
||||
(48, "1/8"),
|
||||
(64, "1/6"),
|
||||
(96, "1/4"),
|
||||
(128, "1/3"),
|
||||
(192, "1/2"),
|
||||
(256, "2/3"),
|
||||
(384, "1/1"),
|
||||
(512, "4/3"),
|
||||
(576, "3/2"),
|
||||
(768, "2/1"),
|
||||
(1152, "3/1"),
|
||||
(1536, "4/1"),
|
||||
(2304, "6/1"),
|
||||
(3072, "8/1"),
|
||||
(3456, "9/1"),
|
||||
(6144, "16/1"),
|
||||
];
|
||||
/// Returns the next shorter length
|
||||
pub fn prev_note_length (pulses: usize) -> usize {
|
||||
for i in 1..=16 { let length = NOTE_DURATIONS[16-i].0; if length < pulses { return length } }
|
||||
pulses
|
||||
}
|
||||
/// Returns the next longer length
|
||||
pub fn next_note_length (pulses: usize) -> usize {
|
||||
for (length, _) in &NOTE_DURATIONS { if *length > pulses { return *length } }
|
||||
pulses
|
||||
}
|
||||
pub fn pulses_to_name (pulses: usize) -> &'static str {
|
||||
for (length, name) in &NOTE_DURATIONS { if *length == pulses { return name } }
|
||||
""
|
||||
}
|
||||
|
||||
/// Performance counter
|
||||
pub struct PerfModel {
|
||||
pub enabled: bool,
|
||||
clock: quanta::Clock,
|
||||
// In nanoseconds
|
||||
used: AtomicF64,
|
||||
// In microseconds
|
||||
period: AtomicF64,
|
||||
}
|
||||
|
||||
impl Default for PerfModel {
|
||||
fn default () -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
clock: quanta::Clock::new(),
|
||||
used: Default::default(),
|
||||
period: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PerfModel {
|
||||
pub fn get_t0 (&self) -> Option<u64> {
|
||||
if self.enabled {
|
||||
Some(self.clock.raw())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
pub fn update (&self, t0: Option<u64>, scope: &jack::ProcessScope) {
|
||||
if let Some(t0) = t0 {
|
||||
let t1 = self.clock.raw();
|
||||
self.used.store(
|
||||
self.clock.delta_as_nanos(t0, t1) as f64,
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
self.period.store(
|
||||
scope.cycle_times().unwrap().period_usecs as f64,
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
}
|
||||
}
|
||||
pub fn percentage (&self) -> Option<f64> {
|
||||
let period = self.period.load(Ordering::Relaxed) * 1000.0;
|
||||
if period > 0.0 {
|
||||
let used = self.used.load(Ordering::Relaxed);
|
||||
Some(100.0 * used / period)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//#[cfg(test)]
|
||||
//mod test {
|
||||
//use super::*;
|
||||
//#[test]
|
||||
//fn test_samples_to_ticks () {
|
||||
//let ticks = Ticks(12.3).between_samples(0, 100).collect::<Vec<_>>();
|
||||
//println!("{ticks:?}");
|
||||
//}
|
||||
//}
|
||||
0
crates/tek/src/core/tui.rs
Normal file
0
crates/tek/src/core/tui.rs
Normal file
|
|
@ -1,99 +0,0 @@
|
|||
//! Project file format.
|
||||
//!
|
||||
//! This module `impl`s the `from_edn`, `load_edn`, etc. methods
|
||||
//! of structs that are defined in other modules. See:
|
||||
//!
|
||||
//! * [App::from_edn]
|
||||
//! * [App::load_edn]
|
||||
//! * [App::load_edn_one]
|
||||
//! * [Scene::load_edn]
|
||||
//! * [Track::load_edn]
|
||||
//! * [Phrase::load_edn]
|
||||
//! * [Sampler::load_edn]
|
||||
//! * [Sample::load_edn]
|
||||
//! * [LV2Plugin::load_edn]
|
||||
|
||||
use crate::*;
|
||||
|
||||
impl App {
|
||||
|
||||
pub fn from_edn (src: &str) -> Usually<Self> {
|
||||
let mut app = Self::new()?;
|
||||
app.load_edn(src)?;
|
||||
Ok(app)
|
||||
}
|
||||
|
||||
pub fn load_edn (&mut self, mut src: &str) -> Usually<&mut Self> {
|
||||
loop {
|
||||
match read(src) {
|
||||
Ok((edn, rest)) => {
|
||||
self.load_edn_one(edn)?;
|
||||
if rest.len() > 0 {
|
||||
src = rest;
|
||||
} else {
|
||||
break
|
||||
}
|
||||
},
|
||||
Err(EdnError { ptr: None, .. }) => {
|
||||
break
|
||||
},
|
||||
Err(e) => {
|
||||
panic!("{e:?}");
|
||||
},
|
||||
}
|
||||
}
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
fn load_edn_one <'e> (&mut self, edn: Edn<'e>) -> Usually<()> {
|
||||
match edn {
|
||||
Edn::List(items) => {
|
||||
match items.get(0) {
|
||||
Some(Edn::Symbol("bpm")) => {
|
||||
match items.get(1) {
|
||||
Some(Edn::Int(b)) =>
|
||||
self.transport.timebase.set_bpm(*b as f64),
|
||||
Some(Edn::Double(b)) =>
|
||||
self.transport.timebase.set_bpm(f64::from(*b)),
|
||||
_ => panic!("unspecified bpm")
|
||||
}
|
||||
},
|
||||
Some(Edn::Symbol("scene")) => {
|
||||
tek_sequencer::Scene::from_edn(&items[1..])?;
|
||||
},
|
||||
Some(Edn::Symbol("track")) => {
|
||||
tek_mixer::Track::from_edn(&items[1..])?;
|
||||
},
|
||||
Some(Edn::Symbol("midi-in")) => {
|
||||
self.midi_ins = items[1..].iter().map(|x|match x {
|
||||
Edn::Str(n) => n.to_string(),
|
||||
_ => panic!("unexpected midi-in")
|
||||
}).collect::<Vec<_>>();
|
||||
},
|
||||
Some(Edn::Symbol("audio-out")) => {
|
||||
let client = self.client();
|
||||
self.audio_outs = items[1..].iter().map(|x|match x {
|
||||
Edn::Str(n) => n.to_string(),
|
||||
_ => panic!("unexpected midi-in")
|
||||
}).collect::<Vec<_>>()
|
||||
.iter()
|
||||
.map(|name|client
|
||||
.ports(Some(name), None, PortFlags::empty())
|
||||
.get(0)
|
||||
.map(|name|client.port_by_name(name)))
|
||||
.flatten()
|
||||
.filter_map(|x|x)
|
||||
.map(Arc::new)
|
||||
.collect();
|
||||
},
|
||||
_ => panic!("unexpected edn: {:?}", items.get(0))
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
panic!("unexpected edn: {edn:?}");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
//! Help modal / command palette.
|
||||
|
||||
use crate::*;
|
||||
|
||||
/// Command palette.
|
||||
pub struct HelpModal {
|
||||
cursor: usize,
|
||||
search: Option<String>,
|
||||
exited: bool,
|
||||
}
|
||||
impl HelpModal {
|
||||
pub fn new () -> Self {
|
||||
Self { cursor: 0, search: None, exited: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl Exit for HelpModal {
|
||||
fn exited (&self) -> bool {
|
||||
self.exited
|
||||
}
|
||||
fn exit (&mut self) {
|
||||
self.exited = true
|
||||
}
|
||||
}
|
||||
|
||||
render!(HelpModal |self, buf, area|{
|
||||
make_dim(buf);
|
||||
let area = center_box(area, 64, 20);
|
||||
fill_fg(buf, area, Color::Reset);
|
||||
fill_bg(buf, area, Nord::bg_lo(true, true));
|
||||
fill_char(buf, area, ' ');
|
||||
let x = area.x + 2;
|
||||
let y = area.y + 1;
|
||||
"Command:"
|
||||
.blit(buf, x, y, Some(Style::default().bold()))?;
|
||||
" ".repeat(area.width as usize - 13)
|
||||
.blit(buf, x + 9, y, Some(Style::default().bg(Color::Reset)))?;
|
||||
if let Some(search) = self.search.as_ref() {
|
||||
search.blit(buf, x + 9, y, Some(Style::default().not_dim()))?;
|
||||
}
|
||||
let y = y + 1;
|
||||
fill_char(buf, Rect { y, height: 1, ..area }, '-');
|
||||
let y = y + 1;
|
||||
for i in 0..area.height-3 {
|
||||
let y = y + i;
|
||||
if let Some(command) = crate::control::KEYMAP_FOCUS.get(i as usize) {
|
||||
format!("{:?}", command.0).blit(buf, x, y, Some(Style::default().white().bold()))?;
|
||||
command.2.blit(buf, x + 11, y, Some(Style::default().white().bold()))?;
|
||||
command.3.blit(buf, x + 26, y, Some(Style::default().white().dim()))?;
|
||||
} else if let Some(command) = crate::control::KEYMAP_GLOBAL.get((i as usize) - crate::control::KEYMAP_FOCUS.len()) {
|
||||
format!("{:?}", command.0).blit(buf, x, y, Some(Style::default().white().bold()))?;
|
||||
command.2.blit(buf, x + 11, y, Some(Style::default().white().bold()))?;
|
||||
command.3.blit(buf, x + 26, y, Some(Style::default().white().dim()))?;
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
let hi_area = Rect { x: area.x + 1, width: area.width - 2, y: area.y + 3 + self.cursor as u16, height: 1 };
|
||||
fill_bg(buf, hi_area, Nord::bg_hi(true, true));
|
||||
fill_fg(buf, hi_area, Color::White);
|
||||
Lozenge(Style::default()).draw(buf, area)
|
||||
});
|
||||
|
||||
handle!(HelpModal |self, e| {
|
||||
if handle_keymap(self, e, KEYMAP_HELP)? {
|
||||
return Ok(true)
|
||||
}
|
||||
Ok(match e {
|
||||
AppEvent::Input(Event::Key(KeyEvent {
|
||||
code: KeyCode::Char(c),
|
||||
modifiers: KeyModifiers::NONE, ..
|
||||
})) => {
|
||||
if self.search.is_none() {
|
||||
self.search = Some(String::new());
|
||||
}
|
||||
self.search.as_mut().unwrap().push(*c);
|
||||
true
|
||||
},
|
||||
_ => true
|
||||
})
|
||||
});
|
||||
|
||||
pub const KEYMAP_HELP: &'static [KeyBinding<HelpModal>] = keymap!(HelpModal {
|
||||
[Esc, NONE, "help_close", "close help dialog", |modal: &mut HelpModal|{
|
||||
modal.exit();
|
||||
Ok(true)
|
||||
}],
|
||||
[Up, NONE, "help_prev", "select previous command", |modal: &mut HelpModal|{
|
||||
modal.cursor = modal.cursor.saturating_sub(1);
|
||||
Ok(true)
|
||||
}],
|
||||
[Down, NONE, "help_next", "select next command", |modal: &mut HelpModal|{
|
||||
modal.cursor = modal.cursor + 1;
|
||||
Ok(true)
|
||||
}],
|
||||
});
|
||||
1
crates/tek/src/layout/README.md
Normal file
1
crates/tek/src/layout/README.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
manja s grozde i ikebana s chiaroscuro
|
||||
102
crates/tek/src/layout/align.rs
Normal file
102
crates/tek/src/layout/align.rs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
use crate::*;
|
||||
|
||||
impl<E: Engine> LayoutAlign<E> for E {}
|
||||
|
||||
pub trait LayoutAlign<E: Engine> {
|
||||
fn center_x <W: Render<E>> (w: W) -> Align<W> { Align::X(w) }
|
||||
fn center_y <W: Render<E>> (w: W) -> Align<W> { Align::Y(w) }
|
||||
fn center <W: Render<E>> (w: W) -> Align<W> { Align::Center(w) }
|
||||
fn at_n <W: Render<E>> (w: W) -> Align<W> { Align::N(w) }
|
||||
fn at_s <W: Render<E>> (w: W) -> Align<W> { Align::S(w) }
|
||||
fn at_e <W: Render<E>> (w: W) -> Align<W> { Align::E(w) }
|
||||
fn at_w <W: Render<E>> (w: W) -> Align<W> { Align::W(w) }
|
||||
fn at_nw <W: Render<E>> (w: W) -> Align<W> { Align::NW(w) }
|
||||
fn at_sw <W: Render<E>> (w: W) -> Align<W> { Align::SW(w) }
|
||||
fn at_ne <W: Render<E>> (w: W) -> Align<W> { Align::NE(w) }
|
||||
fn at_se <W: Render<E>> (w: W) -> Align<W> { Align::SE(w) }
|
||||
}
|
||||
|
||||
/// Override X and Y coordinates, aligning to corner, side, or center of area
|
||||
pub enum Align<L> {
|
||||
/// Draw at center of container
|
||||
Center(L),
|
||||
/// Draw at center of X axis
|
||||
X(L),
|
||||
/// Draw at center of Y axis
|
||||
Y(L),
|
||||
/// Draw at upper left corner of contaier
|
||||
NW(L),
|
||||
/// Draw at center of upper edge of container
|
||||
N(L),
|
||||
/// Draw at right left corner of contaier
|
||||
NE(L),
|
||||
/// Draw at center of left edge of container
|
||||
W(L),
|
||||
/// Draw at center of right edge of container
|
||||
E(L),
|
||||
/// Draw at lower left corner of container
|
||||
SW(L),
|
||||
/// Draw at center of lower edge of container
|
||||
S(L),
|
||||
/// Draw at lower right edge of container
|
||||
SE(L)
|
||||
}
|
||||
|
||||
impl<T> Align<T> {
|
||||
pub fn inner (&self) -> &T {
|
||||
match self {
|
||||
Self::Center(inner) => inner,
|
||||
Self::X(inner) => inner,
|
||||
Self::Y(inner) => inner,
|
||||
Self::NW(inner) => inner,
|
||||
Self::N(inner) => inner,
|
||||
Self::NE(inner) => inner,
|
||||
Self::W(inner) => inner,
|
||||
Self::E(inner) => inner,
|
||||
Self::SW(inner) => inner,
|
||||
Self::S(inner) => inner,
|
||||
Self::SE(inner) => inner,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn align<T, N: Coordinate, R: Area<N> + From<[N;4]>> (align: &Align<T>, outer: R, inner: R) -> Option<R> {
|
||||
if outer.w() < inner.w() || outer.h() < inner.h() {
|
||||
None
|
||||
} else {
|
||||
let [ox, oy, ow, oh] = outer.xywh();
|
||||
let [ix, iy, iw, ih] = inner.xywh();
|
||||
Some(match align {
|
||||
Align::Center(_) => [ox + (ow - iw) / 2.into(), oy + (oh - ih) / 2.into(), iw, ih,].into(),
|
||||
Align::X(_) => [ox + (ow - iw) / 2.into(), iy, iw, ih,].into(),
|
||||
Align::Y(_) => [ix, oy + (oh - ih) / 2.into(), iw, ih,].into(),
|
||||
Align::NW(_) => [ox, oy, iw, ih,].into(),
|
||||
Align::N(_) => [ox + (ow - iw) / 2.into(), oy, iw, ih,].into(),
|
||||
Align::NE(_) => [ox + ow - iw, oy, iw, ih,].into(),
|
||||
Align::W(_) => [ox, oy + (oh - ih) / 2.into(), iw, ih,].into(),
|
||||
Align::E(_) => [ox + ow - iw, oy + (oh - ih) / 2.into(), iw, ih,].into(),
|
||||
Align::SW(_) => [ox, oy + oh - ih, iw, ih,].into(),
|
||||
Align::S(_) => [ox + (ow - iw) / 2.into(), oy + oh - ih, iw, ih,].into(),
|
||||
Align::SE(_) => [ox + ow - iw, oy + oh - ih, iw, ih,].into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine, T: Render<E>> Render<E> for Align<T> {
|
||||
fn min_size (&self, outer_area: E::Size) -> Perhaps<E::Size> {
|
||||
self.inner().min_size(outer_area)
|
||||
}
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
let outer_area = to.area();
|
||||
Ok(if let Some(inner_size) = self.min_size(outer_area.wh().into())? {
|
||||
let inner_area = outer_area.clip(inner_size);
|
||||
if let Some(aligned) = align(&self, outer_area.into(), inner_area.into()) {
|
||||
to.render_in(aligned, self.inner())?
|
||||
} else {
|
||||
()
|
||||
}
|
||||
} else {
|
||||
()
|
||||
})
|
||||
}
|
||||
}
|
||||
87
crates/tek/src/layout/bsp.rs
Normal file
87
crates/tek/src/layout/bsp.rs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
use crate::*;
|
||||
|
||||
impl<E: Engine> LayoutBspStatic<E> for E {}
|
||||
|
||||
pub trait LayoutBspStatic<E: Engine>: {
|
||||
fn over <A: Render<E>, B: Render<E>> (a: A, b: B) -> Over<E, A, B> {
|
||||
Over(Default::default(), a, b)
|
||||
}
|
||||
fn under <A: Render<E>, B: Render<E>> (a: A, b: B) -> Under<E, A, B> {
|
||||
Under(Default::default(), a, b)
|
||||
}
|
||||
fn to_north <A: Render<E>, B: Render<E>> (a: A, b: B) -> ToNorth<E, A, B> {
|
||||
ToNorth(None, a, b)
|
||||
}
|
||||
fn to_south <A: Render<E>, B: Render<E>> (a: A, b: B) -> ToSouth<E, A, B> {
|
||||
ToSouth(None, a, b)
|
||||
}
|
||||
fn to_east <A: Render<E>, B: Render<E>> (a: A, b: B) -> ToEast<E, A, B> {
|
||||
ToEast(None, a, b)
|
||||
}
|
||||
fn to_west <A: Render<E>, B: Render<E>> (a: A, b: B) -> ToWest<E, A, B> {
|
||||
ToWest(None, a, b)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait LayoutBspFixedStatic<E: Engine>: {
|
||||
fn to_north <A: Render<E>, B: Render<E>> (n: E::Unit, a: A, b: B) -> ToNorth<E, A, B> {
|
||||
ToNorth(Some(n), a, b)
|
||||
}
|
||||
fn to_south <A: Render<E>, B: Render<E>> (n: E::Unit, a: A, b: B) -> ToSouth<E, A, B> {
|
||||
ToSouth(Some(n), a, b)
|
||||
}
|
||||
fn to_east <A: Render<E>, B: Render<E>> (n: E::Unit, a: A, b: B) -> ToEast<E, A, B> {
|
||||
ToEast(Some(n), a, b)
|
||||
}
|
||||
fn to_west <A: Render<E>, B: Render<E>> (n: E::Unit, a: A, b: B) -> ToWest<E, A, B> {
|
||||
ToWest(Some(n), a, b)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Over<E: Engine, A: Render<E>, B: Render<E>>(PhantomData<E>, A, B);
|
||||
|
||||
pub struct Under<E: Engine, A: Render<E>, B: Render<E>>(PhantomData<E>, A, B);
|
||||
|
||||
pub struct ToNorth<E: Engine, A: Render<E>, B: Render<E>>(Option<E::Unit>, A, B);
|
||||
|
||||
pub struct ToSouth<E: Engine, A: Render<E>, B: Render<E>>(Option<E::Unit>, A, B);
|
||||
|
||||
pub struct ToEast<E: Engine, A, B>(Option<E::Unit>, A, B);
|
||||
|
||||
pub struct ToWest<E: Engine, A: Render<E>, B: Render<E>>(Option<E::Unit>, A, B);
|
||||
|
||||
impl<E: Engine, A: Render<E>, B: Render<E>> Render<E> for ToNorth<E, A, B> {
|
||||
fn min_size (&self, _: E::Size) -> Perhaps<E::Size> {
|
||||
todo!();
|
||||
}
|
||||
fn render (&self, _: &mut E::Output) -> Usually<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine, A: Render<E>, B: Render<E>> Render<E> for ToSouth<E, A, B> {
|
||||
fn min_size (&self, _: E::Size) -> Perhaps<E::Size> {
|
||||
todo!();
|
||||
}
|
||||
fn render (&self, _: &mut E::Output) -> Usually<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine, A: Render<E>, B: Render<E>> Render<E> for ToWest<E, A, B> {
|
||||
fn min_size (&self, _: E::Size) -> Perhaps<E::Size> {
|
||||
todo!();
|
||||
}
|
||||
fn render (&self, _: &mut E::Output) -> Usually<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine, A: Render<E>, B: Render<E>> Render<E> for ToEast<E, A, B> {
|
||||
fn min_size (&self, _: E::Size) -> Perhaps<E::Size> {
|
||||
todo!();
|
||||
}
|
||||
fn render (&self, _: &mut E::Output) -> Usually<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
67
crates/tek/src/layout/cond.rs
Normal file
67
crates/tek/src/layout/cond.rs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
use crate::*;
|
||||
|
||||
impl<E: Engine, R: Render<E>> LayoutCond<E> for R {}
|
||||
|
||||
pub trait LayoutCond<E: Engine>: Render<E> + Sized {
|
||||
fn when (self, cond: bool) -> If<E, Self> {
|
||||
If(Default::default(), cond, self)
|
||||
}
|
||||
fn or <B: Render<E>> (self, cond: bool, other: B) -> Either<E, Self, B> {
|
||||
Either(Default::default(), cond, self, other)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine> LayoutCondStatic<E> for E {}
|
||||
|
||||
pub trait LayoutCondStatic<E: Engine> {
|
||||
fn either <A: Render<E>, B: Render<E>> (
|
||||
condition: bool,
|
||||
a: A,
|
||||
b: B,
|
||||
) -> Either<E, A, B> {
|
||||
Either(Default::default(), condition, a, b)
|
||||
}
|
||||
}
|
||||
|
||||
/// Render widget if predicate is true
|
||||
pub struct If<E: Engine, A: Render<E>>(PhantomData<E>, bool, A);
|
||||
|
||||
impl<E: Engine, A: Render<E>> Render<E> for If<E, A> {
|
||||
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
if self.1 {
|
||||
return self.2.min_size(to)
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
if self.1 {
|
||||
return self.2.render(to)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Render widget A if predicate is true, otherwise widget B
|
||||
pub struct Either<E: Engine, A: Render<E>, B: Render<E>>(
|
||||
PhantomData<E>,
|
||||
bool,
|
||||
A,
|
||||
B,
|
||||
);
|
||||
|
||||
impl<E: Engine, A: Render<E>, B: Render<E>> Render<E> for Either<E, A, B> {
|
||||
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
if self.1 {
|
||||
return self.2.min_size(to)
|
||||
} else {
|
||||
return self.3.min_size(to)
|
||||
}
|
||||
}
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
if self.1 {
|
||||
return self.2.render(to)
|
||||
} else {
|
||||
return self.3.render(to)
|
||||
}
|
||||
}
|
||||
}
|
||||
11
crates/tek/src/layout/debug.rs
Normal file
11
crates/tek/src/layout/debug.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
use crate::*;
|
||||
|
||||
impl<E: Engine, W: Render<E>> LayoutDebug<E> for W {}
|
||||
|
||||
pub trait LayoutDebug<E: Engine>: Render<E> + Sized {
|
||||
fn debug (self) -> DebugOverlay<E, Self> {
|
||||
DebugOverlay(Default::default(), self)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DebugOverlay<E: Engine, W: Render<E>>(PhantomData<E>, pub W);
|
||||
52
crates/tek/src/layout/fill.rs
Normal file
52
crates/tek/src/layout/fill.rs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
use crate::*;
|
||||
|
||||
impl<E: Engine> LayoutFill<E> for E {}
|
||||
|
||||
pub trait LayoutFill<E: Engine> {
|
||||
fn fill_x <W: Render<E>> (fill: W) -> Fill<E, W> {
|
||||
Fill::X(fill)
|
||||
}
|
||||
fn fill_y <W: Render<E>> (fill: W) -> Fill<E, W> {
|
||||
Fill::Y(fill)
|
||||
}
|
||||
fn fill_xy <W: Render<E>> (fill: W) -> Fill<E, W> {
|
||||
Fill::XY(fill)
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Fill<E: Engine, W: Render<E>> {
|
||||
X(W),
|
||||
Y(W),
|
||||
XY(W),
|
||||
_Unused(PhantomData<E>)
|
||||
}
|
||||
|
||||
impl<E: Engine, W: Render<E>> Fill<E, W> {
|
||||
fn inner (&self) -> &W {
|
||||
match self {
|
||||
Self::X(inner) => &inner,
|
||||
Self::Y(inner) => &inner,
|
||||
Self::XY(inner) => &inner,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine, W: Render<E>> Render<E> for Fill<E, W> {
|
||||
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
let area = self.inner().min_size(to.into())?;
|
||||
if let Some(area) = area {
|
||||
Ok(Some(match self {
|
||||
Self::X(_) => [to.w().into(), area.h()],
|
||||
Self::Y(_) => [area.w(), to.h().into()],
|
||||
Self::XY(_) => [to.w().into(), to.h().into()],
|
||||
_ => unreachable!(),
|
||||
}.into()))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
self.inner().render(to)
|
||||
}
|
||||
}
|
||||
58
crates/tek/src/layout/fixed.rs
Normal file
58
crates/tek/src/layout/fixed.rs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
use crate::*;
|
||||
|
||||
impl<E: Engine, W: Render<E>> LayoutFixed<E> for W {}
|
||||
|
||||
pub trait LayoutFixed<E: Engine> {
|
||||
fn fixed_x <W: Render<E>> (x: E::Unit, w: W) -> Fixed<E, W> {
|
||||
Fixed::X(x, w)
|
||||
}
|
||||
fn fixed_y <W: Render<E>> (y: E::Unit, w: W) -> Fixed<E, W> {
|
||||
Fixed::Y(y, w)
|
||||
}
|
||||
fn fixed_xy <W: Render<E>> (x: E::Unit, y: E::Unit, w: W) -> Fixed<E, W> {
|
||||
Fixed::XY(x, y, w)
|
||||
}
|
||||
}
|
||||
|
||||
/// Enforce fixed size of drawing area
|
||||
pub enum Fixed<E: Engine, T> {
|
||||
_Unused(PhantomData<E>),
|
||||
/// Enforce fixed width
|
||||
X(E::Unit, T),
|
||||
/// Enforce fixed height
|
||||
Y(E::Unit, T),
|
||||
/// Enforce fixed width and height
|
||||
XY(E::Unit, E::Unit, T),
|
||||
}
|
||||
|
||||
impl<E: Engine, T> Fixed<E, T> {
|
||||
pub fn inner (&self) -> &T {
|
||||
match self {
|
||||
Self::X(_, i) => i,
|
||||
Self::Y(_, i) => i,
|
||||
Self::XY(_, _, i) => i,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<E: Engine, T: Render<E>> Render<E> for Fixed<E, T> {
|
||||
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
Ok(match self {
|
||||
Self::X(w, _) =>
|
||||
if to.w() >= *w { Some([*w, to.h()].into()) } else { None },
|
||||
Self::Y(h, _) =>
|
||||
if to.h() >= *h { Some([to.w(), *h].into()) } else { None },
|
||||
Self::XY(w, h, _)
|
||||
=> if to.w() >= *w && to.h() >= *h { Some([*w, *h].into()) } else { None },
|
||||
_ => unreachable!(),
|
||||
})
|
||||
}
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
// 🡘 🡙 ←🡙→
|
||||
if let Some(size) = self.min_size(to.area().wh().into())? {
|
||||
to.render_in(to.area().clip(size).into(), self.inner())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
99
crates/tek/src/layout/inset_outset.rs
Normal file
99
crates/tek/src/layout/inset_outset.rs
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
use crate::*;
|
||||
|
||||
impl<E: Engine + LayoutPushPull<E> + LayoutShrinkGrow<E>> LayoutInsetOutset<E> for E {}
|
||||
|
||||
pub trait LayoutInsetOutset<E: Engine>: LayoutPushPull<E> + LayoutShrinkGrow<E> {
|
||||
fn inset_x <W: Render<E>> (x: E::Unit, w: W) -> Inset<E, W> {
|
||||
Inset::X(x, w)
|
||||
}
|
||||
fn inset_y <W: Render<E>> (y: E::Unit, w: W) -> Inset<E, W> {
|
||||
Inset::Y(y, w)
|
||||
}
|
||||
fn inset_xy <W: Render<E>> (x: E::Unit, y: E::Unit, w: W) -> Inset<E, W> {
|
||||
Inset::XY(x, y, w)
|
||||
}
|
||||
fn outset_x <W: Render<E>> (x: E::Unit, w: W) -> Outset<E, W> {
|
||||
Outset::X(x, w)
|
||||
}
|
||||
fn outset_y <W: Render<E>> (y: E::Unit, w: W) -> Outset<E, W> {
|
||||
Outset::Y(y, w)
|
||||
}
|
||||
fn outset_xy <W: Render<E>> (x: E::Unit, y: E::Unit, w: W) -> Outset<E, W> {
|
||||
Outset::XY(x, y, w)
|
||||
}
|
||||
}
|
||||
|
||||
/// Shrink from each side
|
||||
pub enum Inset<E: Engine, T> {
|
||||
_Unused(PhantomData<E>),
|
||||
/// Decrease width
|
||||
X(E::Unit, T),
|
||||
/// Decrease height
|
||||
Y(E::Unit, T),
|
||||
/// Decrease width and height
|
||||
XY(E::Unit, E::Unit, T),
|
||||
}
|
||||
|
||||
impl<E: Engine, T: Render<E>> Inset<E, T> {
|
||||
pub fn inner (&self) -> &T {
|
||||
match self {
|
||||
Self::X(_, i) => i,
|
||||
Self::Y(_, i) => i,
|
||||
Self::XY(_, _, i) => i,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine, T: Render<E>> Render<E> for Inset<E, T> {
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
match self {
|
||||
Self::X(x, inner) => E::push_x(*x, E::shrink_x(*x, inner)),
|
||||
Self::Y(y, inner) => E::push_y(*y, E::shrink_y(*y, inner)),
|
||||
Self::XY(x, y, inner) => E::push_xy(*x, *y, E::shrink_xy(*x, *y, inner)),
|
||||
_ => unreachable!(),
|
||||
}.render(to)
|
||||
}
|
||||
}
|
||||
|
||||
/// Grow on each side
|
||||
pub enum Outset<E: Engine, T: Render<E>> {
|
||||
_Unused(PhantomData<E>),
|
||||
/// Increase width
|
||||
X(E::Unit, T),
|
||||
/// Increase height
|
||||
Y(E::Unit, T),
|
||||
/// Increase width and height
|
||||
XY(E::Unit, E::Unit, T),
|
||||
}
|
||||
|
||||
|
||||
impl<E: Engine, T: Render<E>> Outset<E, T> {
|
||||
pub fn inner (&self) -> &T {
|
||||
match self {
|
||||
Self::X(_, i) => i,
|
||||
Self::Y(_, i) => i,
|
||||
Self::XY(_, _, i) => i,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine, T: Render<E>> Render<E> for Outset<E, T> {
|
||||
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
match *self {
|
||||
Self::X(x, ref inner) => E::grow_x(x + x, inner),
|
||||
Self::Y(y, ref inner) => E::grow_y(y + y, inner),
|
||||
Self::XY(x, y, ref inner) => E::grow_xy(x + x, y + y, inner),
|
||||
_ => unreachable!(),
|
||||
}.min_size(to)
|
||||
}
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
match *self {
|
||||
Self::X(x, ref inner) => E::push_x(x, inner),
|
||||
Self::Y(y, ref inner) => E::push_y(y, inner),
|
||||
Self::XY(x, y, ref inner) => E::push_xy(x, y, inner),
|
||||
_ => unreachable!(),
|
||||
}.render(to)
|
||||
}
|
||||
}
|
||||
41
crates/tek/src/layout/layers.rs
Normal file
41
crates/tek/src/layout/layers.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
use crate::*;
|
||||
|
||||
pub struct Layers<
|
||||
E: Engine,
|
||||
F: Send + Sync + Fn(&mut dyn FnMut(&dyn Render<E>)->Usually<()>)->Usually<()>
|
||||
>(pub F, PhantomData<E>);
|
||||
|
||||
impl<
|
||||
E: Engine,
|
||||
F: Send + Sync + Fn(&mut dyn FnMut(&dyn Render<E>)->Usually<()>)->Usually<()>
|
||||
> Layers<E, F> {
|
||||
#[inline]
|
||||
pub fn new (build: F) -> Self {
|
||||
Self(build, Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine, F> Render<E> for Layers<E, F>
|
||||
where
|
||||
F: Send + Sync + Fn(&mut dyn FnMut(&dyn Render<E>)->Usually<()>)->Usually<()>
|
||||
{
|
||||
fn min_size (&self, area: E::Size) -> Perhaps<E::Size> {
|
||||
let mut w: E::Unit = 0.into();
|
||||
let mut h: E::Unit = 0.into();
|
||||
(self.0)(&mut |layer| {
|
||||
if let Some(layer_area) = layer.min_size(area)? {
|
||||
w = w.max(layer_area.w());
|
||||
h = h.max(layer_area.h());
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(Some([w, h].into()))
|
||||
}
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
if let Some(size) = self.min_size(to.area().wh().into())? {
|
||||
(self.0)(&mut |layer|to.render_in(to.area().clip(size).into(), layer))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
33
crates/tek/src/layout/map_reduce.rs
Normal file
33
crates/tek/src/layout/map_reduce.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
use crate::*;
|
||||
|
||||
impl<E: Engine, I: Iterator<Item=T>+Send+Sync, T, R: Render<E>> LayoutMapReduce<E, I, T, R> for E {}
|
||||
|
||||
pub trait LayoutMapReduce<E: Engine, I: Iterator<Item = T>+Send+Sync, T, R: Render<E>> {
|
||||
fn map <F: Fn(T)->R> (iterator: I, callback: F) -> Map<E, T, I, R, F> {
|
||||
Map(Default::default(), iterator, callback)
|
||||
}
|
||||
fn reduce <F: Fn(&dyn Render<E>, T)->R+Send+Sync> (iterator: I, callback: F) -> Reduce<E, T, I, R, F> {
|
||||
Reduce(Default::default(), iterator, callback)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Map<E: Engine, T, I: Iterator<Item=T>, R: Render<E>, F: Fn(T)->R>(
|
||||
PhantomData<E>,
|
||||
I,
|
||||
F
|
||||
);
|
||||
|
||||
pub struct Reduce<E: Engine, T, I: Iterator<Item=T>, R: Render<E>, F: Fn(&dyn Render<E>, T)->R>(
|
||||
PhantomData<(E, R)>,
|
||||
I,
|
||||
F
|
||||
);
|
||||
|
||||
impl<E: Engine, T, I: Iterator<Item=T>+Send+Sync, R: Render<E>, F: Fn(&dyn Render<E>, T)->R+Send+Sync> Render<E> for Reduce<E, T, I, R, F> {
|
||||
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
todo!()
|
||||
}
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
46
crates/tek/src/layout/measure.rs
Normal file
46
crates/tek/src/layout/measure.rs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
use crate::*;
|
||||
|
||||
/// A widget that tracks its render width and height
|
||||
pub struct Measure<E: Engine>(PhantomData<E>, AtomicUsize, AtomicUsize);
|
||||
|
||||
impl<E: Engine> Clone for Measure<E> {
|
||||
fn clone (&self) -> Self {
|
||||
Self(
|
||||
Default::default(),
|
||||
AtomicUsize::from(self.1.load(Ordering::Relaxed)),
|
||||
AtomicUsize::from(self.2.load(Ordering::Relaxed)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine> std::fmt::Debug for Measure<E> {
|
||||
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||
f.debug_struct("Measure")
|
||||
.field("width", &self.0)
|
||||
.field("height", &self.1)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine> Measure<E> {
|
||||
pub fn w (&self) -> usize { self.1.load(Ordering::Relaxed) }
|
||||
pub fn h (&self) -> usize { self.2.load(Ordering::Relaxed) }
|
||||
pub fn wh (&self) -> [usize;2] { [self.w(), self.h()] }
|
||||
pub fn set_w (&self, w: impl Into<usize>) { self.1.store(w.into(), Ordering::Relaxed) }
|
||||
pub fn set_h (&self, h: impl Into<usize>) { self.2.store(h.into(), Ordering::Relaxed) }
|
||||
pub fn set_wh (&self, w: impl Into<usize>, h: impl Into<usize>) { self.set_w(w); self.set_h(h); }
|
||||
pub fn new () -> Self { Self(PhantomData::default(), 0.into(), 0.into()) }
|
||||
pub fn format (&self) -> String { format!("{}x{}", self.w(), self.h()) }
|
||||
}
|
||||
|
||||
impl<E: Engine> Render<E> for Measure<E> {
|
||||
fn min_size (&self, _: E::Size) -> Perhaps<E::Size> {
|
||||
Ok(Some([0u16.into(), 0u16.into()].into()))
|
||||
}
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
self.set_w(to.area().w());
|
||||
self.set_h(to.area().h());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
85
crates/tek/src/layout/min_max.rs
Normal file
85
crates/tek/src/layout/min_max.rs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
use crate::*;
|
||||
|
||||
impl<E: Engine> LayoutMinMax<E> for E {}
|
||||
|
||||
pub trait LayoutMinMax<E: Engine> {
|
||||
fn min_x <W: Render<E>> (x: E::Unit, w: W) -> Min<E::Unit, W> {
|
||||
Min::X(x, w)
|
||||
}
|
||||
fn min_y <W: Render<E>> (y: E::Unit, w: W) -> Min<E::Unit, W> {
|
||||
Min::Y(y, w)
|
||||
}
|
||||
fn min_xy <W: Render<E>> (x: E::Unit, y: E::Unit, w: W) -> Min<E::Unit, W> {
|
||||
Min::XY(x, y, w)
|
||||
}
|
||||
fn max_x <W: Render<E>> (x: E::Unit, w: W) -> Max<E::Unit, W> {
|
||||
Max::X(x, w)
|
||||
}
|
||||
fn max_y <W: Render<E>> (y: E::Unit, w: W) -> Max<E::Unit, W> {
|
||||
Max::Y(y, w)
|
||||
}
|
||||
fn max_xy <W: Render<E>> (x: E::Unit, y: E::Unit, w: W) -> Max<E::Unit, W> {
|
||||
Max::XY(x, y, w)
|
||||
}
|
||||
}
|
||||
|
||||
/// Enforce minimum size of drawing area
|
||||
pub enum Min<U: Coordinate, T> {
|
||||
/// Enforce minimum width
|
||||
X(U, T),
|
||||
/// Enforce minimum height
|
||||
Y(U, T),
|
||||
/// Enforce minimum width and height
|
||||
XY(U, U, T),
|
||||
}
|
||||
impl<N: Coordinate, T> Min<N, T> {
|
||||
pub fn inner (&self) -> &T {
|
||||
match self { Self::X(_, i) => i, Self::Y(_, i) => i, Self::XY(_, _, i) => i, }
|
||||
}
|
||||
}
|
||||
impl<E: Engine, T: Render<E>> Render<E> for Min<E::Unit, T> {
|
||||
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
Ok(self.inner().min_size(to)?.map(|to|match *self {
|
||||
Self::X(w, _) => [to.w().max(w), to.h()],
|
||||
Self::Y(h, _) => [to.w(), to.h().max(h)],
|
||||
Self::XY(w, h, _) => [to.w().max(w), to.h().max(h)],
|
||||
}.into()))
|
||||
}
|
||||
// TODO: 🡘 🡙 ←🡙→
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
Ok(self.min_size(to.area().wh().into())?
|
||||
.map(|size|to.render_in(to.area().clip(size).into(), self.inner()))
|
||||
.transpose()?.unwrap_or(()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Enforce maximum size of drawing area
|
||||
pub enum Max<U: Coordinate, T> {
|
||||
/// Enforce maximum width
|
||||
X(U, T),
|
||||
/// Enforce maximum height
|
||||
Y(U, T),
|
||||
/// Enforce maximum width and height
|
||||
XY(U, U, T),
|
||||
}
|
||||
|
||||
impl<N: Coordinate, T> Max<N, T> {
|
||||
fn inner (&self) -> &T {
|
||||
match self { Self::X(_, i) => i, Self::Y(_, i) => i, Self::XY(_, _, i) => i, }
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine, T: Render<E>> Render<E> for Max<E:: Unit, T> {
|
||||
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
Ok(self.inner().min_size(to)?.map(|to|match *self {
|
||||
Self::X(w, _) => [to.w().min(w), to.h()],
|
||||
Self::Y(h, _) => [to.w(), to.h().min(h)],
|
||||
Self::XY(w, h, _) => [to.w().min(w), to.h().min(h)],
|
||||
}.into()))
|
||||
}
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
Ok(self.min_size(to.area().wh().into())?
|
||||
.map(|size|to.render_in(to.area().clip(size).into(), self.inner()))
|
||||
.transpose()?.unwrap_or(()))
|
||||
}
|
||||
}
|
||||
24
crates/tek/src/layout/mod.rs
Normal file
24
crates/tek/src/layout/mod.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
use crate::*;
|
||||
|
||||
submod! {
|
||||
align
|
||||
bsp
|
||||
cond
|
||||
debug
|
||||
fill
|
||||
fixed
|
||||
inset_outset
|
||||
layers
|
||||
map_reduce
|
||||
measure
|
||||
min_max
|
||||
push_pull
|
||||
scroll
|
||||
shrink_grow
|
||||
split
|
||||
stack
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! lay {
|
||||
($($expr:expr),* $(,)?) => { Layers::new(move|add|{ $(add(&$expr)?;)* Ok(()) }) }
|
||||
}
|
||||
132
crates/tek/src/layout/push_pull.rs
Normal file
132
crates/tek/src/layout/push_pull.rs
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
use crate::*;
|
||||
|
||||
impl<E: Engine> LayoutPushPull<E> for E {}
|
||||
|
||||
pub trait LayoutPushPull<E: Engine> {
|
||||
fn push_x <W: Render<E>> (x: E::Unit, w: W) -> Push<E, W> {
|
||||
Push::X(x, w)
|
||||
}
|
||||
fn push_y <W: Render<E>> (y: E::Unit, w: W) -> Push<E, W> {
|
||||
Push::Y(y, w)
|
||||
}
|
||||
fn push_xy <W: Render<E>> (x: E::Unit, y: E::Unit, w: W) -> Push<E, W> {
|
||||
Push::XY(x, y, w)
|
||||
}
|
||||
fn pull_x <W: Render<E>> (x: E::Unit, w: W) -> Pull<E, W> {
|
||||
Pull::X(x, w)
|
||||
}
|
||||
fn pull_y <W: Render<E>> (y: E::Unit, w: W) -> Pull<E, W> {
|
||||
Pull::Y(y, w)
|
||||
}
|
||||
fn pull_xy <W: Render<E>> (x: E::Unit, y: E::Unit, w: W) -> Pull<E, W> {
|
||||
Pull::XY(x, y, w)
|
||||
}
|
||||
}
|
||||
|
||||
/// Increment origin point of drawing area
|
||||
pub enum Push<E: Engine, T: Render<E>> {
|
||||
/// Move origin to the right
|
||||
X(E::Unit, T),
|
||||
/// Move origin downwards
|
||||
Y(E::Unit, T),
|
||||
/// Move origin to the right and downwards
|
||||
XY(E::Unit, E::Unit, T),
|
||||
}
|
||||
|
||||
impl<E: Engine, T: Render<E>> Push<E, T> {
|
||||
pub fn inner (&self) -> &T {
|
||||
match self {
|
||||
Self::X(_, i) => i,
|
||||
Self::Y(_, i) => i,
|
||||
Self::XY(_, _, i) => i,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
pub fn x (&self) -> E::Unit {
|
||||
match self {
|
||||
Self::X(x, _) => *x,
|
||||
Self::Y(_, _) => E::Unit::default(),
|
||||
Self::XY(x, _, _) => *x,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
pub fn y (&self) -> E::Unit {
|
||||
match self {
|
||||
Self::X(_, _) => E::Unit::default(),
|
||||
Self::Y(y, _) => *y,
|
||||
Self::XY(_, y, _) => *y,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine, T: Render<E>> Render<E> for Push<E, T> {
|
||||
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
self.inner().min_size(to)
|
||||
}
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
let area = to.area();
|
||||
Ok(self.min_size(area.wh().into())?
|
||||
.map(|size|to.render_in(match *self {
|
||||
Self::X(x, _) => [area.x() + x, area.y(), size.w(), size.h()],
|
||||
Self::Y(y, _) => [area.x(), area.y() + y, size.w(), size.h()],
|
||||
Self::XY(x, y, _) => [area.x() + x, area.y() + y, size.w(), size.h()],
|
||||
_ => unreachable!(),
|
||||
}.into(), self.inner())).transpose()?.unwrap_or(()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Decrement origin point of drawing area
|
||||
pub enum Pull<E: Engine, T: Render<E>> {
|
||||
_Unused(PhantomData<E>),
|
||||
/// Move origin to the right
|
||||
X(E::Unit, T),
|
||||
/// Move origin downwards
|
||||
Y(E::Unit, T),
|
||||
/// Move origin to the right and downwards
|
||||
XY(E::Unit, E::Unit, T),
|
||||
}
|
||||
|
||||
impl<E: Engine, T: Render<E>> Pull<E, T> {
|
||||
pub fn inner (&self) -> &T {
|
||||
match self {
|
||||
Self::X(_, i) => i,
|
||||
Self::Y(_, i) => i,
|
||||
Self::XY(_, _, i) => i,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
pub fn x (&self) -> E::Unit {
|
||||
match self {
|
||||
Self::X(x, _) => *x,
|
||||
Self::Y(_, _) => E::Unit::default(),
|
||||
Self::XY(x, _, _) => *x,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
pub fn y (&self) -> E::Unit {
|
||||
match self {
|
||||
Self::X(_, _) => E::Unit::default(),
|
||||
Self::Y(y, _) => *y,
|
||||
Self::XY(_, y, _) => *y,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine, T: Render<E>> Render<E> for Pull<E, T> {
|
||||
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
self.inner().min_size(to)
|
||||
}
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
let area = to.area();
|
||||
Ok(self.min_size(area.wh().into())?
|
||||
.map(|size|to.render_in(match *self {
|
||||
Self::X(x, _) => [area.x().minus(x), area.y(), size.w(), size.h()],
|
||||
Self::Y(y, _) => [area.x(), area.y().minus(y), size.w(), size.h()],
|
||||
Self::XY(x, y, _) => [area.x().minus(x), area.y().minus(y), size.w(), size.h()],
|
||||
_ => unreachable!(),
|
||||
}.into(), self.inner())).transpose()?.unwrap_or(()))
|
||||
}
|
||||
}
|
||||
|
||||
8
crates/tek/src/layout/scroll.rs
Normal file
8
crates/tek/src/layout/scroll.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
use crate::*;
|
||||
|
||||
/// A scrollable area.
|
||||
pub struct Scroll<
|
||||
E: Engine,
|
||||
F: Send + Sync + Fn(&mut dyn FnMut(&dyn Render<E>)->Usually<()>)->Usually<()>
|
||||
>(pub F, pub Direction, pub u64, PhantomData<E>);
|
||||
|
||||
102
crates/tek/src/layout/shrink_grow.rs
Normal file
102
crates/tek/src/layout/shrink_grow.rs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
use crate::*;
|
||||
|
||||
impl<E: Engine> LayoutShrinkGrow<E> for E {}
|
||||
|
||||
pub trait LayoutShrinkGrow<E: Engine> {
|
||||
fn shrink_x <W: Render<E>> (x: E::Unit, w: W) -> Shrink<E, W> {
|
||||
Shrink::X(x, w)
|
||||
}
|
||||
fn shrink_y <W: Render<E>> (y: E::Unit, w: W) -> Shrink<E, W> {
|
||||
Shrink::Y(y, w)
|
||||
}
|
||||
fn shrink_xy <W: Render<E>> (x: E::Unit, y: E::Unit, w: W) -> Shrink<E, W> {
|
||||
Shrink::XY(x, y, w)
|
||||
}
|
||||
fn grow_x <W: Render<E>> (x: E::Unit, w: W) -> Grow<E::Unit, W> {
|
||||
Grow::X(x, w)
|
||||
}
|
||||
fn grow_y <W: Render<E>> (y: E::Unit, w: W) -> Grow<E::Unit, W> {
|
||||
Grow::Y(y, w)
|
||||
}
|
||||
fn grow_xy <W: Render<E>> (x: E::Unit, y: E::Unit, w: W) -> Grow<E::Unit, W> {
|
||||
Grow::XY(x, y, w)
|
||||
}
|
||||
}
|
||||
|
||||
/// Shrink drawing area
|
||||
pub enum Shrink<E: Engine, T> {
|
||||
_Unused(PhantomData<E>),
|
||||
/// Decrease width
|
||||
X(E::Unit, T),
|
||||
/// Decrease height
|
||||
Y(E::Unit, T),
|
||||
/// Decrease width and height
|
||||
XY(E::Unit, E::Unit, T),
|
||||
}
|
||||
|
||||
impl<E: Engine, T: Render<E>> Shrink<E, T> {
|
||||
fn inner (&self) -> &T {
|
||||
match self {
|
||||
Self::X(_, i) => i,
|
||||
Self::Y(_, i) => i,
|
||||
Self::XY(_, _, i) => i,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine, T: Render<E>> Render<E> for Shrink<E, T> {
|
||||
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
Ok(self.inner().min_size(to)?.map(|to|match *self {
|
||||
Self::X(w, _) => [
|
||||
if to.w() > w { to.w() - w } else { 0.into() },
|
||||
to.h()
|
||||
],
|
||||
Self::Y(h, _) => [
|
||||
to.w(),
|
||||
if to.h() > h { to.h() - h } else { 0.into() }
|
||||
],
|
||||
Self::XY(w, h, _) => [
|
||||
if to.w() > w { to.w() - w } else { 0.into() },
|
||||
if to.h() > h { to.h() - h } else { 0.into() }
|
||||
],
|
||||
_ => unreachable!(),
|
||||
}.into()))
|
||||
}
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
Ok(self.min_size(to.area().wh().into())?
|
||||
.map(|size|to.render_in(to.area().clip(size).into(), self.inner()))
|
||||
.transpose()?.unwrap_or(()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Expand drawing area
|
||||
pub enum Grow<N: Coordinate, T> {
|
||||
/// Increase width
|
||||
X(N, T),
|
||||
/// Increase height
|
||||
Y(N, T),
|
||||
/// Increase width and height
|
||||
XY(N, N, T)
|
||||
}
|
||||
|
||||
impl<N: Coordinate, T> Grow<N, T> {
|
||||
fn inner (&self) -> &T {
|
||||
match self { Self::X(_, i) => i, Self::Y(_, i) => i, Self::XY(_, _, i) => i, }
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine, T: Render<E>> Render<E> for Grow<E::Unit, T> {
|
||||
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
Ok(self.inner().min_size(to)?.map(|to|match *self {
|
||||
Self::X(w, _) => [to.w() + w, to.h()],
|
||||
Self::Y(h, _) => [to.w(), to.h() + h],
|
||||
Self::XY(w, h, _) => [to.w() + w, to.h() + h],
|
||||
}.into()))
|
||||
}
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
Ok(self.min_size(to.area().wh().into())?
|
||||
.map(|size|to.render_in(to.area().clip(size).into(), self.inner()))
|
||||
.transpose()?.unwrap_or(()))
|
||||
}
|
||||
}
|
||||
45
crates/tek/src/layout/split.rs
Normal file
45
crates/tek/src/layout/split.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
use crate::*;
|
||||
|
||||
pub trait LayoutSplit<E: Engine>: Render<E> + Sized {
|
||||
fn split <W: Render<E>> (
|
||||
self, direction: Direction, amount: E::Unit, other: W
|
||||
) -> Split<E, Self, W> { Split::new(direction, amount, self, other) }
|
||||
fn split_flip <W: Render<E>> (
|
||||
self, direction: Direction, amount: E::Unit, other: W
|
||||
) -> Split<E, W, Self> { Split::new(direction, amount, other, self) }
|
||||
}
|
||||
|
||||
/// A binary split with fixed proportion
|
||||
pub struct Split<E: Engine, A: Render<E>, B: Render<E>>(
|
||||
pub Direction, pub E::Unit, A, B, PhantomData<E>
|
||||
);
|
||||
|
||||
impl<E: Engine, A: Render<E>, B: Render<E>> Split<E, A, B> {
|
||||
pub fn new (direction: Direction, proportion: E::Unit, a: A, b: B) -> Self {
|
||||
Self(direction, proportion, a, b, Default::default())
|
||||
}
|
||||
pub fn up (proportion: E::Unit, a: A, b: B) -> Self {
|
||||
Self(Direction::Up, proportion, a, b, Default::default())
|
||||
}
|
||||
pub fn down (proportion: E::Unit, a: A, b: B) -> Self {
|
||||
Self(Direction::Down, proportion, a, b, Default::default())
|
||||
}
|
||||
pub fn left (proportion: E::Unit, a: A, b: B) -> Self {
|
||||
Self(Direction::Left, proportion, a, b, Default::default())
|
||||
}
|
||||
pub fn right (proportion: E::Unit, a: A, b: B) -> Self {
|
||||
Self(Direction::Right, proportion, a, b, Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine, A: Render<E>, B: Render<E>> Render<E> for Split<E, A, B> {
|
||||
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
Ok(Some(to))
|
||||
}
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
let (a, b) = to.area().split_fixed(self.0, self.1);
|
||||
to.render_in(a.into(), &self.2)?;
|
||||
to.render_in(b.into(), &self.3)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
179
crates/tek/src/layout/stack.rs
Normal file
179
crates/tek/src/layout/stack.rs
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
use crate::*;
|
||||
|
||||
#[macro_export] macro_rules! col {
|
||||
($($expr:expr),* $(,)?) => { Stack::down(move|add|{ $(add(&$expr)?;)* Ok(()) }) };
|
||||
($pat:pat in $collection:expr => $item:expr) => {
|
||||
Stack::down(move |add|{
|
||||
for $pat in $collection { add(&$item)?; }
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
#[macro_export] macro_rules! col_up {
|
||||
($($expr:expr),* $(,)?) => { Stack::down(move|add|{ $(add(&$expr)?;)* Ok(()) }) };
|
||||
($pat:pat in $collection:expr => $item:expr) => {
|
||||
Stack::up(move |add|{
|
||||
for $pat in $collection { add(&$item)?; }
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
#[macro_export] macro_rules! row {
|
||||
($($expr:expr),* $(,)?) => { Stack::right(move|add|{ $(add(&$expr)?;)* Ok(()) }) };
|
||||
($pat:pat in $collection:expr => $item:expr) => {
|
||||
Stack::right(move |add|{
|
||||
for $pat in $collection { add(&$item)?; }
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Stack<
|
||||
E: Engine,
|
||||
F: Send + Sync + Fn(&mut dyn FnMut(&dyn Render<E>)->Usually<()>)->Usually<()>
|
||||
>(pub F, pub Direction, PhantomData<E>);
|
||||
|
||||
impl<
|
||||
E: Engine,
|
||||
F: Send + Sync + Fn(&mut dyn FnMut(&dyn Render<E>)->Usually<()>)->Usually<()>
|
||||
> Stack<E, F> {
|
||||
#[inline] pub fn new (direction: Direction, build: F) -> Self {
|
||||
Self(build, direction, Default::default())
|
||||
}
|
||||
#[inline] pub fn right (build: F) -> Self {
|
||||
Self::new(Direction::Right, build)
|
||||
}
|
||||
#[inline] pub fn down (build: F) -> Self {
|
||||
Self::new(Direction::Down, build)
|
||||
}
|
||||
#[inline] pub fn up (build: F) -> Self {
|
||||
Self::new(Direction::Up, build)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Engine, F> Render<E> for Stack<E, F>
|
||||
where
|
||||
F: Send + Sync + Fn(&mut dyn FnMut(&dyn Render<E>)->Usually<()>)->Usually<()>
|
||||
{
|
||||
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
match self.1 {
|
||||
|
||||
Direction::Down => {
|
||||
let mut w: E::Unit = 0.into();
|
||||
let mut h: E::Unit = 0.into();
|
||||
(self.0)(&mut |component: &dyn Render<E>| {
|
||||
let max = to.h().minus(h);
|
||||
if max > E::Unit::ZERO() {
|
||||
let item = E::max_y(max, E::push_y(h, component));
|
||||
let size = item.min_size(to)?.map(|size|size.wh());
|
||||
if let Some([width, height]) = size {
|
||||
h = h + height.into();
|
||||
w = w.max(width);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(Some([w, h].into()))
|
||||
},
|
||||
|
||||
Direction::Right => {
|
||||
let mut w: E::Unit = 0.into();
|
||||
let mut h: E::Unit = 0.into();
|
||||
(self.0)(&mut |component: &dyn Render<E>| {
|
||||
let max = to.w().minus(w);
|
||||
if max > E::Unit::ZERO() {
|
||||
let item = E::max_x(max, E::push_x(h, component));
|
||||
let size = item.min_size(to)?.map(|size|size.wh());
|
||||
if let Some([width, height]) = size {
|
||||
w = w + width.into();
|
||||
h = h.max(height);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(Some([w, h].into()))
|
||||
},
|
||||
|
||||
Direction::Up => {
|
||||
let mut w: E::Unit = 0.into();
|
||||
let mut h: E::Unit = 0.into();
|
||||
(self.0)(&mut |component: &dyn Render<E>| {
|
||||
let max = to.h().minus(h);
|
||||
if max > E::Unit::ZERO() {
|
||||
let item = component.max_y(to.h() - h);
|
||||
let size = item.min_size(to)?.map(|size|size.wh());
|
||||
if let Some([width, height]) = size {
|
||||
h = h + height.into();
|
||||
w = w.max(width);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(Some([w, h].into()))
|
||||
},
|
||||
|
||||
Direction::Left => {
|
||||
let mut w: E::Unit = 0.into();
|
||||
let mut h: E::Unit = 0.into();
|
||||
(self.0)(&mut |component: &dyn Render<E>| {
|
||||
if w < to.w() {
|
||||
todo!();
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(Some([w, h].into()))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
let area = to.area();
|
||||
let mut w = 0.into();
|
||||
let mut h = 0.into();
|
||||
match self.1 {
|
||||
Direction::Down => {
|
||||
(self.0)(&mut |item| {
|
||||
if h < area.h() {
|
||||
let item = item.push_y(h).max_y(area.h() - h);
|
||||
let show = item.min_size(area.wh().into())?.map(|s|s.wh());
|
||||
if let Some([width, height]) = show {
|
||||
item.render(to)?;
|
||||
h = h + height;
|
||||
if width > w { w = width }
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
},
|
||||
Direction::Right => {
|
||||
(self.0)(&mut |item| {
|
||||
if w < area.w() {
|
||||
let item = item.push_x(w).max_x(area.w() - w);
|
||||
let show = item.min_size(area.wh().into())?.map(|s|s.wh());
|
||||
if let Some([width, height]) = show {
|
||||
item.render(to)?;
|
||||
w = width + w;
|
||||
if height > h { h = height }
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
},
|
||||
Direction::Up => {
|
||||
(self.0)(&mut |item| {
|
||||
if h < area.h() {
|
||||
let show = item.min_size([area.w(), area.h().minus(h)].into())?.map(|s|s.wh());
|
||||
if let Some([width, height]) = show {
|
||||
item.push_y(area.h() - height).shrink_y(height).render(to)?;
|
||||
h = h + height;
|
||||
if width > w { w = width }
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
},
|
||||
_ => todo!()
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
75
crates/tek/src/lib.rs
Normal file
75
crates/tek/src/lib.rs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
pub(crate) use ratatui;
|
||||
|
||||
pub(crate) use jack;
|
||||
pub(crate) use jack::{
|
||||
Client, ProcessScope, Control, CycleTimes,
|
||||
Port, MidiIn, MidiOut, AudioIn, AudioOut, Unowned,
|
||||
Transport, TransportState, MidiIter, RawMidi
|
||||
};
|
||||
|
||||
pub(crate) use midly;
|
||||
pub(crate) use midly::{
|
||||
*, live::LiveEvent, num::u7
|
||||
};
|
||||
|
||||
pub(crate) use clap;
|
||||
|
||||
pub(crate) use std::sync::{Arc, Mutex, RwLock};
|
||||
pub(crate) use std::sync::atomic::{Ordering, AtomicBool, AtomicUsize};
|
||||
pub(crate) use std::collections::BTreeMap;
|
||||
pub(crate) use std::marker::PhantomData;
|
||||
pub(crate) use std::error::Error;
|
||||
|
||||
pub(crate) use crossterm::{ExecutableCommand};
|
||||
pub(crate) use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode};
|
||||
pub(crate) use crossterm::event::{KeyCode, KeyModifiers, KeyEvent, KeyEventKind, KeyEventState};
|
||||
pub(crate) use tek_core::midly::{num::u7, MidiMessage};
|
||||
pub(crate) use std::sync::{Arc, RwLock};
|
||||
pub(crate) use std::path::PathBuf;
|
||||
pub(crate) use std::ffi::OsString;
|
||||
pub(crate) use better_panic::{Settings, Verbosity};
|
||||
pub(crate) use std::thread::{spawn, JoinHandle};
|
||||
pub(crate) use std::time::Duration;
|
||||
pub(crate) use ratatui::prelude::{Style, Color, Buffer};
|
||||
pub(crate) use ratatui::style::{Stylize, Modifier};
|
||||
pub(crate) use ratatui::backend::{Backend, CrosstermBackend, ClearType};
|
||||
pub(crate) use std::io::{Stdout, stdout};
|
||||
|
||||
pub(crate) use atomic_float::*;
|
||||
pub(crate) use palette::{*, convert::*, okhsl::*};
|
||||
|
||||
use std::ops::{Add, Sub, Mul, Div, Rem};
|
||||
use std::cmp::{Ord, Eq, PartialEq};
|
||||
use std::fmt::{Debug, Display};
|
||||
|
||||
/// Standard result type.
|
||||
pub type Usually<T> = Result<T, Box<dyn Error>>;
|
||||
|
||||
/// Standard optional result type.
|
||||
pub type Perhaps<T> = Result<Option<T>, Box<dyn Error>>;
|
||||
|
||||
/// Define and reexport submodules.
|
||||
#[macro_export] macro_rules! submod {
|
||||
($($name:ident)*) => { $(mod $name; pub use self::$name::*;)* };
|
||||
}
|
||||
|
||||
/// Define public modules.
|
||||
#[macro_export] macro_rules! pubmod {
|
||||
($($name:ident)*) => { $(pub mod $name;)* };
|
||||
}
|
||||
|
||||
/// Define test modules.
|
||||
#[macro_export] macro_rules! testmod {
|
||||
($($name:ident)*) => { $(#[cfg(test)] mod $name;)* };
|
||||
}
|
||||
|
||||
submod! {
|
||||
api
|
||||
core
|
||||
layout
|
||||
tui
|
||||
}
|
||||
|
||||
testmod! {
|
||||
test
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
//! ***Tek*** is a MIDI sequencer, sampler, and audio plugin host for the Linux terminal.
|
||||
|
||||
//#[global_allocator]
|
||||
//static A: rlsf::SmallGlobalTlsf = rlsf::SmallGlobalTlsf::new();
|
||||
//#![feature(fn_traits)]
|
||||
//#![feature(unboxed_closures)]
|
||||
#![allow(macro_expanded_macro_exports_accessed_by_absolute_paths)]
|
||||
#![allow(ambiguous_glob_reexports)]
|
||||
|
||||
pub(crate) use tek_core::{*, jack::*};
|
||||
pub(crate) use tek_sequencer::*;
|
||||
pub(crate) use tek_mixer::*;
|
||||
pub(crate) use microxdg::XdgApp;
|
||||
|
||||
submod! {
|
||||
app
|
||||
app_focus
|
||||
app_paths
|
||||
cli
|
||||
control
|
||||
edn
|
||||
help
|
||||
setup
|
||||
}
|
||||
|
||||
/// Global modal dialog
|
||||
pub static MODAL: Lazy<Arc<Mutex<Option<Box<dyn ExitableComponent>>>>> =
|
||||
Lazy::new(||Arc::new(Mutex::new(None)));
|
||||
|
||||
/// Application entrypoint.
|
||||
pub fn main () -> Usually<()> {
|
||||
run(App::from_edn(include_str!("../example.edn"))?
|
||||
.activate(Some(|app: &Arc<RwLock<App>>|Ok({
|
||||
let (midi_in, mut midi_outs) = {
|
||||
let app = app.read().unwrap();
|
||||
let jack = app.jack.as_ref().unwrap();
|
||||
let midi_in = jack.register_port("midi-in", MidiIn)?;
|
||||
let midi_outs = app.arranger.tracks.iter()
|
||||
.map(|t|Some(jack.register_port(&t.name.read().unwrap(), MidiOut).unwrap()))
|
||||
.collect::<Vec<_>>();
|
||||
(midi_in, midi_outs)
|
||||
};
|
||||
{
|
||||
let mut app = app.write().unwrap();
|
||||
let jack = app.jack.as_ref().unwrap();
|
||||
for name in app.midi_ins.iter() {
|
||||
let ports = jack.client().ports(Some(name), None, PortFlags::empty());
|
||||
for port in ports.iter() {
|
||||
if let Some(port) = jack.client().port_by_name(port) {
|
||||
jack.client().connect_ports(&port, &midi_in)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
app.midi_in = Some(Arc::new(midi_in));
|
||||
for (index, track) in app.arranger.tracks.iter_mut().enumerate() {
|
||||
track.midi_out = midi_outs[index].take();
|
||||
}
|
||||
//for track in app.arranger.tracks.iter() {
|
||||
//track.connect_first_device()?;
|
||||
//track.connect_last_device(&app)?;
|
||||
//}
|
||||
};
|
||||
})))?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
//! Inital setup dialog (TODO: make this the options dialog too?)
|
||||
|
||||
use crate::*;
|
||||
|
||||
/// Appears on first run (i.e. if state dir is missing).
|
||||
pub struct SetupModal(pub Option<Arc<XdgApp>>, pub bool);
|
||||
|
||||
render!(SetupModal |self, buf, area| {
|
||||
for cell in buf.content.iter_mut() {
|
||||
cell.fg = ratatui::style::Color::Gray;
|
||||
cell.modifier = ratatui::style::Modifier::DIM;
|
||||
}
|
||||
let lines = [
|
||||
(" ", Style::default().white().on_black().not_dim().bold()),
|
||||
(" Welcome to TEK! ", Style::default().white().on_black().not_dim().bold()),
|
||||
(" ", Style::default().white().on_black().not_dim().bold()),
|
||||
(" Press ENTER to create the ", Style::default().white().on_black().not_dim()),
|
||||
(" following directories: ", Style::default().white().on_black().not_dim()),
|
||||
(" ", Style::default().white().on_black().not_dim().bold()),
|
||||
(" Configuration directory: ", Style::default().white().on_black().not_dim()),
|
||||
(" ~/.config/tek ", Style::default().white().on_black().not_dim().bold()),
|
||||
(" ", Style::default().white().on_black().not_dim()),
|
||||
(" Data directory: ", Style::default().white().on_black().not_dim()),
|
||||
(" ~/.local/share/tek ", Style::default().white().on_black().not_dim().bold()),
|
||||
(" ", Style::default().white().on_black().not_dim().bold()),
|
||||
(" Or press CTRL-C to exit. ", Style::default().white().on_black().not_dim()),
|
||||
(" ", Style::default().white().on_black().not_dim()),
|
||||
];
|
||||
let width = lines[0].0.len() as u16;
|
||||
let x = area.x + (area.width - width) / 2;
|
||||
for (i, (line, style)) in lines.iter().enumerate() {
|
||||
line.blit(buf, x, area.y + area.height / 2 - (lines.len() / 2) as u16 + i as u16, Some(*style))?;
|
||||
}
|
||||
Ok(area)
|
||||
});
|
||||
handle!(SetupModal |self, e| {
|
||||
if let AppEvent::Input(Event::Key(KeyEvent {
|
||||
code: KeyCode::Enter,
|
||||
..
|
||||
})) = e {
|
||||
AppPaths::new(&self.0.as_ref().unwrap())?.create()?;
|
||||
self.exit();
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
});
|
||||
impl Exit for SetupModal {
|
||||
fn exited (&self) -> bool {
|
||||
self.1
|
||||
}
|
||||
fn exit (&mut self) {
|
||||
self.1 = true
|
||||
}
|
||||
}
|
||||
0
crates/tek/src/test.rs
Normal file
0
crates/tek/src/test.rs
Normal file
25
crates/tek/src/todo_cli_mixer.rs
Normal file
25
crates/tek/src/todo_cli_mixer.rs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
use tek_core::{*, clap::{self, Parser}};
|
||||
|
||||
pub fn main () -> Usually<()> {
|
||||
MixerCli::parse().run()
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)] #[command(version, about, long_about = None)] pub struct MixerCli {
|
||||
/// Name of JACK client
|
||||
#[arg(short, long)] name: Option<String>,
|
||||
/// Number of tracks
|
||||
#[arg(short, long)] channels: Option<usize>,
|
||||
}
|
||||
|
||||
impl MixerCli {
|
||||
fn run (&self) -> Usually<()> {
|
||||
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)?;
|
||||
}
|
||||
Ok(mixer)
|
||||
})?)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
26
crates/tek/src/todo_cli_plugin.rs
Normal file
26
crates/tek/src/todo_cli_plugin.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
use tek_core::{*, clap::{self, Parser}};
|
||||
|
||||
pub fn main () -> Usually<()> {
|
||||
PluginCli::parse().run()
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)] #[command(version, about, long_about = None)] pub struct PluginCli {
|
||||
/// Name of JACK client
|
||||
#[arg(short, long)] name: Option<String>,
|
||||
/// Path to plugin
|
||||
#[arg(short, long)] path: Option<String>,
|
||||
}
|
||||
|
||||
impl PluginCli {
|
||||
fn run (&self) -> Usually<()> {
|
||||
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"),
|
||||
self.path.as_ref().expect("pass --path /to/lv2/plugin.so")
|
||||
)?;
|
||||
Ok(plugin)
|
||||
})?)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
26
crates/tek/src/todo_cli_sampler.rs
Normal file
26
crates/tek/src/todo_cli_sampler.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
use tek_core::{*, clap::{self, Parser}};
|
||||
|
||||
pub fn main () -> Usually<()> {
|
||||
SamplerCli::parse().run()
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)] #[command(version, about, long_about = None)] pub struct SamplerCli {
|
||||
/// Name of JACK client
|
||||
#[arg(short, long)] name: Option<String>,
|
||||
/// Path to plugin
|
||||
#[arg(short, long)] path: Option<String>,
|
||||
}
|
||||
|
||||
impl SamplerCli {
|
||||
fn run (&self) -> Usually<()> {
|
||||
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"),
|
||||
None,
|
||||
)?;
|
||||
Ok(plugin)
|
||||
})?)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
251
crates/tek/src/tui/_todo_tui_mixer.rs
Normal file
251
crates/tek/src/tui/_todo_tui_mixer.rs
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
use crate::*;
|
||||
|
||||
pub struct Mixer<E: Engine> {
|
||||
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
||||
pub jack: Arc<RwLock<JackClient>>,
|
||||
pub name: String,
|
||||
pub tracks: Vec<Track<E>>,
|
||||
pub selected_track: usize,
|
||||
pub selected_column: usize,
|
||||
}
|
||||
impl<E: Engine> Mixer<E> {
|
||||
pub fn new (jack: &Arc<RwLock<JackClient>>, name: &str) -> Usually<Self> {
|
||||
Ok(Self {
|
||||
jack: jack.clone(),
|
||||
name: name.into(),
|
||||
selected_column: 0,
|
||||
selected_track: 1,
|
||||
tracks: vec![],
|
||||
})
|
||||
}
|
||||
pub fn track_add (&mut self, name: &str, channels: usize) -> Usually<&mut Self> {
|
||||
let track = Track::new(name)?;
|
||||
self.tracks.push(track);
|
||||
Ok(self)
|
||||
}
|
||||
pub fn track (&self) -> Option<&Track<E>> {
|
||||
self.tracks.get(self.selected_track)
|
||||
}
|
||||
}
|
||||
|
||||
//pub const ACTIONS: [(&'static str, &'static str);2] = [
|
||||
//("+/-", "Adjust"),
|
||||
//("Ins/Del", "Add/remove track"),
|
||||
//];
|
||||
|
||||
|
||||
/// A sequencer track.
|
||||
#[derive(Debug)]
|
||||
pub struct Track<E: Engine> {
|
||||
pub name: String,
|
||||
/// Inputs and outputs of 1st and last device
|
||||
pub ports: JackPorts,
|
||||
/// Device chain
|
||||
pub devices: Vec<JackDevice<E>>,
|
||||
/// Device selector
|
||||
pub device: usize,
|
||||
}
|
||||
|
||||
impl<E: Engine> Track<E> {
|
||||
pub fn new (name: &str) -> Usually<Self> {
|
||||
Ok(Self {
|
||||
name: name.to_string(),
|
||||
ports: JackPorts::default(),
|
||||
devices: vec![],
|
||||
device: 0,
|
||||
})
|
||||
}
|
||||
fn get_device_mut (&self, i: usize) -> Option<RwLockWriteGuard<Box<dyn AudioComponent<E>>>> {
|
||||
self.devices.get(i).map(|d|d.state.write().unwrap())
|
||||
}
|
||||
pub fn device_mut (&self) -> Option<RwLockWriteGuard<Box<dyn AudioComponent<E>>>> {
|
||||
self.get_device_mut(self.device)
|
||||
}
|
||||
/// Add a device to the end of the chain.
|
||||
pub fn append_device (&mut self, device: JackDevice<E>) -> Usually<&mut JackDevice<E>> {
|
||||
self.devices.push(device);
|
||||
let index = self.devices.len() - 1;
|
||||
Ok(&mut self.devices[index])
|
||||
}
|
||||
pub fn add_device (&mut self, device: JackDevice<E>) {
|
||||
self.devices.push(device);
|
||||
}
|
||||
//pub fn connect_first_device (&self) -> Usually<()> {
|
||||
//if let (Some(port), Some(device)) = (&self.midi_out, self.devices.get(0)) {
|
||||
//device.client.as_client().connect_ports(&port, &device.midi_ins()?[0])?;
|
||||
//}
|
||||
//Ok(())
|
||||
//}
|
||||
//pub fn connect_last_device (&self, app: &Track) -> Usually<()> {
|
||||
//Ok(match self.devices.get(self.devices.len().saturating_sub(1)) {
|
||||
//Some(device) => {
|
||||
//app.audio_out(0).map(|left|device.connect_audio_out(0, &left)).transpose()?;
|
||||
//app.audio_out(1).map(|right|device.connect_audio_out(1, &right)).transpose()?;
|
||||
//()
|
||||
//},
|
||||
//None => ()
|
||||
//})
|
||||
//}
|
||||
}
|
||||
|
||||
pub struct TrackView<'a, E: Engine> {
|
||||
pub chain: Option<&'a Track<E>>,
|
||||
pub direction: Direction,
|
||||
pub focused: bool,
|
||||
pub entered: bool,
|
||||
}
|
||||
|
||||
impl<'a> Render<Tui> for TrackView<'a, Tui> {
|
||||
fn min_size (&self, area: [u16;2]) -> Perhaps<[u16;2]> {
|
||||
todo!()
|
||||
}
|
||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||
todo!();
|
||||
//let mut area = to.area();
|
||||
//if let Some(chain) = self.chain {
|
||||
//match self.direction {
|
||||
//Direction::Down => area.width = area.width.min(40),
|
||||
//Direction::Right => area.width = area.width.min(10),
|
||||
//_ => { unimplemented!() },
|
||||
//}
|
||||
//to.fill_bg(to.area(), Nord::bg_lo(self.focused, self.entered));
|
||||
//let mut split = Stack::new(self.direction);
|
||||
//for device in chain.devices.as_slice().iter() {
|
||||
//split = split.add_ref(device);
|
||||
//}
|
||||
//let (area, areas) = split.render_areas(to)?;
|
||||
//if self.focused && self.entered && areas.len() > 0 {
|
||||
//Corners(Style::default().green().not_dim()).draw(to.with_rect(areas[0]))?;
|
||||
//}
|
||||
//Ok(Some(area))
|
||||
//} else {
|
||||
//let [x, y, width, height] = area;
|
||||
//let label = "No chain selected";
|
||||
//let x = x + (width - label.len() as u16) / 2;
|
||||
//let y = y + height / 2;
|
||||
//to.blit(&label, x, y, Some(Style::default().dim().bold()))?;
|
||||
//Ok(Some(area))
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
impl Content<Tui> for Mixer<Tui> {
|
||||
fn content (&self) -> impl Render<Tui> {
|
||||
Stack::right(|add| {
|
||||
for channel in self.tracks.iter() {
|
||||
add(channel)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Content<Tui> for Track<Tui> {
|
||||
fn content (&self) -> impl Render<Tui> {
|
||||
TrackView {
|
||||
chain: Some(&self),
|
||||
direction: tek_core::Direction::Right,
|
||||
focused: true,
|
||||
entered: true,
|
||||
//pub channels: u8,
|
||||
//pub input_ports: Vec<Port<AudioIn>>,
|
||||
//pub pre_gain_meter: f64,
|
||||
//pub gain: f64,
|
||||
//pub insert_ports: Vec<Port<AudioOut>>,
|
||||
//pub return_ports: Vec<Port<AudioIn>>,
|
||||
//pub post_gain_meter: f64,
|
||||
//pub post_insert_meter: f64,
|
||||
//pub level: f64,
|
||||
//pub pan: f64,
|
||||
//pub output_ports: Vec<Port<AudioOut>>,
|
||||
//pub post_fader_meter: f64,
|
||||
//pub route: String,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Handle<Tui> for Mixer<Tui> {
|
||||
fn handle (&mut self, engine: &TuiInput) -> Perhaps<bool> {
|
||||
if let TuiEvent::Input(crossterm::event::Event::Key(event)) = engine.event() {
|
||||
|
||||
match event.code {
|
||||
//KeyCode::Char('c') => {
|
||||
//if event.modifiers == KeyModifiers::CONTROL {
|
||||
//self.exit();
|
||||
//}
|
||||
//},
|
||||
KeyCode::Down => {
|
||||
self.selected_track = (self.selected_track + 1) % self.tracks.len();
|
||||
println!("{}", self.selected_track);
|
||||
return Ok(Some(true))
|
||||
},
|
||||
KeyCode::Up => {
|
||||
if self.selected_track == 0 {
|
||||
self.selected_track = self.tracks.len() - 1;
|
||||
} else {
|
||||
self.selected_track -= 1;
|
||||
}
|
||||
println!("{}", self.selected_track);
|
||||
return Ok(Some(true))
|
||||
},
|
||||
KeyCode::Left => {
|
||||
if self.selected_column == 0 {
|
||||
self.selected_column = 6
|
||||
} else {
|
||||
self.selected_column -= 1;
|
||||
}
|
||||
return Ok(Some(true))
|
||||
},
|
||||
KeyCode::Right => {
|
||||
if self.selected_column == 6 {
|
||||
self.selected_column = 0
|
||||
} else {
|
||||
self.selected_column += 1;
|
||||
}
|
||||
return Ok(Some(true))
|
||||
},
|
||||
_ => {
|
||||
println!("\n{event:?}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
impl Handle<Tui> for Track<Tui> {
|
||||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||
match from.event() {
|
||||
//, NONE, "chain_cursor_up", "move cursor up", || {
|
||||
key!(KeyCode::Up) => {
|
||||
Ok(Some(true))
|
||||
},
|
||||
// , NONE, "chain_cursor_down", "move cursor down", || {
|
||||
key!(KeyCode::Down) => {
|
||||
Ok(Some(true))
|
||||
},
|
||||
// Left, NONE, "chain_cursor_left", "move cursor left", || {
|
||||
key!(KeyCode::Left) => {
|
||||
//if let Some(track) = app.arranger.track_mut() {
|
||||
//track.device = track.device.saturating_sub(1);
|
||||
//return Ok(true)
|
||||
//}
|
||||
Ok(Some(true))
|
||||
},
|
||||
// , NONE, "chain_cursor_right", "move cursor right", || {
|
||||
key!(KeyCode::Right) => {
|
||||
//if let Some(track) = app.arranger.track_mut() {
|
||||
//track.device = (track.device + 1).min(track.devices.len().saturating_sub(1));
|
||||
//return Ok(true)
|
||||
//}
|
||||
Ok(Some(true))
|
||||
},
|
||||
// , NONE, "chain_mode_switch", "switch the display mode", || {
|
||||
key!(KeyCode::Char('`')) => {
|
||||
//app.chain_mode = !app.chain_mode;
|
||||
Ok(Some(true))
|
||||
},
|
||||
_ => Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
148
crates/tek/src/tui/_todo_tui_plugin.rs
Normal file
148
crates/tek/src/tui/_todo_tui_plugin.rs
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
use crate::*;
|
||||
|
||||
/// A plugin device.
|
||||
pub struct Plugin<E> {
|
||||
_engine: PhantomData<E>,
|
||||
/// 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 ports: JackPorts,
|
||||
}
|
||||
|
||||
impl<E> Plugin<E> {
|
||||
/// Create a plugin host device.
|
||||
pub fn new (
|
||||
jack: &Arc<RwLock<JackClient>>,
|
||||
name: &str,
|
||||
) -> Usually<Self> {
|
||||
Ok(Self {
|
||||
_engine: Default::default(),
|
||||
jack: jack.clone(),
|
||||
name: name.into(),
|
||||
path: None,
|
||||
plugin: None,
|
||||
selected: 0,
|
||||
mapping: false,
|
||||
ports: JackPorts::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
impl Render<Tui> for Plugin<Tui> {
|
||||
fn min_size (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
||||
Ok(Some(to))
|
||||
}
|
||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||
let area = to.area();
|
||||
let [x, y, _, height] = area;
|
||||
let mut width = 20u16;
|
||||
match &self.plugin {
|
||||
Some(PluginKind::LV2(LV2Plugin { port_list, instance, .. })) => {
|
||||
let start = self.selected.saturating_sub((height as usize / 2).saturating_sub(1));
|
||||
let end = start + height as usize - 2;
|
||||
//draw_box(buf, Rect { x, y, width, height });
|
||||
for i in start..end {
|
||||
if let Some(port) = port_list.get(i) {
|
||||
let value = if let Some(value) = instance.control_input(port.index) {
|
||||
value
|
||||
} else {
|
||||
port.default_value
|
||||
};
|
||||
//let label = &format!("C·· M·· {:25} = {value:.03}", port.name);
|
||||
let label = &format!("{:25} = {value:.03}", port.name);
|
||||
width = width.max(label.len() as u16 + 4);
|
||||
let style = if i == self.selected {
|
||||
Some(Style::default().green())
|
||||
} else {
|
||||
None
|
||||
} ;
|
||||
to.blit(&label, x + 2, y + 1 + i as u16 - start as u16, style);
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
};
|
||||
draw_header(self, to, x, y, width)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_header <E> (state: &Plugin<E>, to: &mut TuiOutput, x: u16, y: u16, w: u16) -> Usually<Rect> {
|
||||
let style = Style::default().gray();
|
||||
let label1 = format!(" {}", state.name);
|
||||
to.blit(&label1, x + 1, y, Some(style.white().bold()));
|
||||
if let Some(ref path) = state.path {
|
||||
let label2 = format!("{}…", &path[..((w as usize - 10).min(path.len()))]);
|
||||
to.blit(&label2, x + 2 + label1.len() as u16, y, Some(style.not_dim()));
|
||||
}
|
||||
Ok(Rect { x, y, width: w, height: 1 })
|
||||
}
|
||||
|
||||
impl Handle<Tui> for Plugin<Tui> {
|
||||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||
match from.event() {
|
||||
key!(KeyCode::Up) => {
|
||||
self.selected = self.selected.saturating_sub(1);
|
||||
Ok(Some(true))
|
||||
},
|
||||
key!(KeyCode::Down) => {
|
||||
self.selected = (self.selected + 1).min(match &self.plugin {
|
||||
Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1,
|
||||
_ => unimplemented!()
|
||||
});
|
||||
Ok(Some(true))
|
||||
},
|
||||
key!(KeyCode::PageUp) => {
|
||||
self.selected = self.selected.saturating_sub(8);
|
||||
Ok(Some(true))
|
||||
},
|
||||
key!(KeyCode::PageDown) => {
|
||||
self.selected = (self.selected + 10).min(match &self.plugin {
|
||||
Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1,
|
||||
_ => unimplemented!()
|
||||
});
|
||||
Ok(Some(true))
|
||||
},
|
||||
key!(KeyCode::Char(',')) => {
|
||||
match self.plugin.as_mut() {
|
||||
Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => {
|
||||
let index = port_list[self.selected].index;
|
||||
if let Some(value) = instance.control_input(index) {
|
||||
instance.set_control_input(index, value - 0.01);
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
Ok(Some(true))
|
||||
},
|
||||
key!(KeyCode::Char('.')) => {
|
||||
match self.plugin.as_mut() {
|
||||
Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => {
|
||||
let index = port_list[self.selected].index;
|
||||
if let Some(value) = instance.control_input(index) {
|
||||
instance.set_control_input(index, value + 0.01);
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
Ok(Some(true))
|
||||
},
|
||||
key!(KeyCode::Char('g')) => {
|
||||
match self.plugin {
|
||||
Some(PluginKind::LV2(ref mut plugin)) => {
|
||||
plugin.ui_thread = Some(run_lv2_ui(LV2PluginUI::new()?)?);
|
||||
},
|
||||
Some(_) => unreachable!(),
|
||||
None => {}
|
||||
}
|
||||
Ok(Some(true))
|
||||
},
|
||||
_ => Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
46
crates/tek/src/tui/_todo_tui_plugin_lv2.rs
Normal file
46
crates/tek/src/tui/_todo_tui_plugin_lv2.rs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
use super::*;
|
||||
use ::livi::{
|
||||
World,
|
||||
Instance,
|
||||
Plugin as LiviPlugin,
|
||||
Features,
|
||||
FeaturesBuilder,
|
||||
Port,
|
||||
event::LV2AtomSequence,
|
||||
};
|
||||
use std::thread::JoinHandle;
|
||||
|
||||
/// A LV2 plugin.
|
||||
pub struct LV2Plugin {
|
||||
pub world: World,
|
||||
pub instance: Instance,
|
||||
pub plugin: LiviPlugin,
|
||||
pub features: Arc<Features>,
|
||||
pub port_list: Vec<Port>,
|
||||
pub input_buffer: Vec<LV2AtomSequence>,
|
||||
pub ui_thread: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl LV2Plugin {
|
||||
const INPUT_BUFFER: usize = 1024;
|
||||
pub fn new (uri: &str) -> Usually<Self> {
|
||||
// Get 1st plugin at URI
|
||||
let world = World::with_load_bundle(&uri);
|
||||
let features = FeaturesBuilder { min_block_length: 1, max_block_length: 65536 };
|
||||
let features = world.build_features(features);
|
||||
let mut plugin = None;
|
||||
if let Some(p) = world.iter_plugins().next() { plugin = Some(p); }
|
||||
let plugin = plugin.expect("plugin not found");
|
||||
let err = &format!("init {uri}");
|
||||
let instance = unsafe { plugin.instantiate(features.clone(), 48000.0).expect(&err) };
|
||||
let mut port_list = vec![];
|
||||
for port in plugin.ports() {
|
||||
port_list.push(port);
|
||||
}
|
||||
let input_buffer = Vec::with_capacity(Self::INPUT_BUFFER);
|
||||
// Instantiate
|
||||
Ok(Self {
|
||||
world, instance, port_list, plugin, features, input_buffer, ui_thread: None
|
||||
})
|
||||
}
|
||||
}
|
||||
58
crates/tek/src/tui/_todo_tui_plugin_lv2_gui.rs
Normal file
58
crates/tek/src/tui/_todo_tui_plugin_lv2_gui.rs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
use crate::*;
|
||||
use std::thread::{spawn, JoinHandle};
|
||||
use ::winit::{
|
||||
application::ApplicationHandler,
|
||||
event::WindowEvent,
|
||||
event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
|
||||
window::{Window, WindowId},
|
||||
platform::x11::EventLoopBuilderExtX11
|
||||
};
|
||||
|
||||
//pub struct LV2PluginUI {
|
||||
//write: (),
|
||||
//controller: (),
|
||||
//widget: (),
|
||||
//features: (),
|
||||
//transfer: (),
|
||||
//}
|
||||
|
||||
pub fn run_lv2_ui (mut ui: LV2PluginUI) -> Usually<JoinHandle<()>> {
|
||||
Ok(spawn(move||{
|
||||
let event_loop = EventLoop::builder().with_x11().with_any_thread(true).build().unwrap();
|
||||
event_loop.set_control_flow(ControlFlow::Wait);
|
||||
event_loop.run_app(&mut ui).unwrap()
|
||||
}))
|
||||
}
|
||||
|
||||
/// A LV2 plugin's X11 UI.
|
||||
pub struct LV2PluginUI {
|
||||
pub window: Option<Window>
|
||||
}
|
||||
|
||||
impl LV2PluginUI {
|
||||
pub fn new () -> Usually<Self> {
|
||||
Ok(Self { window: None })
|
||||
}
|
||||
}
|
||||
|
||||
impl ApplicationHandler for LV2PluginUI {
|
||||
fn resumed (&mut self, event_loop: &ActiveEventLoop) {
|
||||
self.window = Some(event_loop.create_window(Window::default_attributes()).unwrap());
|
||||
}
|
||||
fn window_event (&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) {
|
||||
match event {
|
||||
WindowEvent::CloseRequested => {
|
||||
self.window.as_ref().unwrap().set_visible(false);
|
||||
event_loop.exit();
|
||||
},
|
||||
WindowEvent::RedrawRequested => {
|
||||
self.window.as_ref().unwrap().request_redraw();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn lv2_ui_instantiate (kind: &str) {
|
||||
//let host = Suil
|
||||
}
|
||||
13
crates/tek/src/tui/_todo_tui_plugin_vst2.rs
Normal file
13
crates/tek/src/tui/_todo_tui_plugin_vst2.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
use crate::*;
|
||||
|
||||
impl<E: Engine> ::vst::host::Host for Plugin<E> {}
|
||||
|
||||
fn set_vst_plugin <E: Engine> (host: &Arc<Mutex<Plugin<E>>>, _path: &str) -> Usually<PluginKind> {
|
||||
let mut loader = ::vst::host::PluginLoader::load(
|
||||
&std::path::Path::new("/nix/store/ij3sz7nqg5l7v2dygdvzy3w6cj62bd6r-helm-0.9.0/lib/lxvst/helm.so"),
|
||||
host.clone()
|
||||
)?;
|
||||
Ok(PluginKind::VST2 {
|
||||
instance: loader.instance()?
|
||||
})
|
||||
}
|
||||
1
crates/tek/src/tui/_todo_tui_plugin_vst3.rs
Normal file
1
crates/tek/src/tui/_todo_tui_plugin_vst3.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
//! TODO
|
||||
412
crates/tek/src/tui/_todo_tui_sampler.rs
Normal file
412
crates/tek/src/tui/_todo_tui_sampler.rs
Normal file
|
|
@ -0,0 +1,412 @@
|
|||
use crate::*;
|
||||
|
||||
/// The sampler plugin plays sounds.
|
||||
pub struct SamplerView<E: Engine> {
|
||||
_engine: PhantomData<E>,
|
||||
pub state: Sampler,
|
||||
pub cursor: (usize, usize),
|
||||
pub editing: Option<Arc<RwLock<Sample>>>,
|
||||
pub buffer: Vec<Vec<f32>>,
|
||||
pub modal: Arc<Mutex<Option<Box<dyn Exit + Send>>>>,
|
||||
}
|
||||
|
||||
impl<E: Engine> SamplerView<E> {
|
||||
pub fn new (
|
||||
jack: &Arc<RwLock<JackClient>>,
|
||||
name: &str,
|
||||
mapped: Option<BTreeMap<u7, Arc<RwLock<Sample>>>>
|
||||
) -> Usually<JackDevice<E>> {
|
||||
Jack::new(name)?
|
||||
.midi_in("midi")
|
||||
.audio_in("recL")
|
||||
.audio_in("recR")
|
||||
.audio_out("outL")
|
||||
.audio_out("outR")
|
||||
.run(|ports|Box::new(Self {
|
||||
_engine: Default::default(),
|
||||
jack: jack.clone(),
|
||||
name: name.into(),
|
||||
cursor: (0, 0),
|
||||
editing: None,
|
||||
mapped: mapped.unwrap_or_else(||BTreeMap::new()),
|
||||
unmapped: vec![],
|
||||
voices: Arc::new(RwLock::new(vec![])),
|
||||
ports,
|
||||
buffer: vec![vec![0.0;16384];2],
|
||||
output_gain: 0.5,
|
||||
modal: Default::default()
|
||||
}))
|
||||
}
|
||||
/// Immutable reference to sample at cursor.
|
||||
pub fn sample (&self) -> Option<&Arc<RwLock<Sample>>> {
|
||||
for (i, sample) in self.mapped.values().enumerate() {
|
||||
if i == self.cursor.0 {
|
||||
return Some(sample)
|
||||
}
|
||||
}
|
||||
for (i, sample) in self.unmapped.iter().enumerate() {
|
||||
if i + self.mapped.len() == self.cursor.0 {
|
||||
return Some(sample)
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>,
|
||||
}
|
||||
|
||||
/// Load sample from WAV and assign to MIDI note.
|
||||
#[macro_export] macro_rules! sample {
|
||||
($note:expr, $name:expr, $src:expr) => {{
|
||||
let (end, data) = read_sample_data($src)?;
|
||||
(
|
||||
u7::from_int_lossy($note).into(),
|
||||
Sample::new($name, 0, end, data).into()
|
||||
)
|
||||
}};
|
||||
}
|
||||
|
||||
use std::fs::File;
|
||||
use symphonia::core::codecs::CODEC_TYPE_NULL;
|
||||
use symphonia::core::errors::Error;
|
||||
use symphonia::core::io::MediaSourceStream;
|
||||
use symphonia::core::probe::Hint;
|
||||
use symphonia::core::audio::SampleBuffer;
|
||||
use symphonia::default::get_codecs;
|
||||
|
||||
pub struct AddSampleModal {
|
||||
exited: bool,
|
||||
dir: PathBuf,
|
||||
subdirs: Vec<OsString>,
|
||||
files: Vec<OsString>,
|
||||
cursor: usize,
|
||||
offset: usize,
|
||||
sample: Arc<RwLock<Sample>>,
|
||||
voices: Arc<RwLock<Vec<Voice>>>,
|
||||
_search: Option<String>,
|
||||
}
|
||||
|
||||
impl Exit for AddSampleModal {
|
||||
fn exited (&self) -> bool {
|
||||
self.exited
|
||||
}
|
||||
fn exit (&mut self) {
|
||||
self.exited = true
|
||||
}
|
||||
}
|
||||
|
||||
impl AddSampleModal {
|
||||
pub fn new (
|
||||
sample: &Arc<RwLock<Sample>>,
|
||||
voices: &Arc<RwLock<Vec<Voice>>>
|
||||
) -> Usually<Self> {
|
||||
let dir = std::env::current_dir()?;
|
||||
let (subdirs, files) = scan(&dir)?;
|
||||
Ok(Self {
|
||||
exited: false,
|
||||
dir,
|
||||
subdirs,
|
||||
files,
|
||||
cursor: 0,
|
||||
offset: 0,
|
||||
sample: sample.clone(),
|
||||
voices: voices.clone(),
|
||||
_search: None
|
||||
})
|
||||
}
|
||||
fn rescan (&mut self) -> Usually<()> {
|
||||
scan(&self.dir).map(|(subdirs, files)|{
|
||||
self.subdirs = subdirs;
|
||||
self.files = files;
|
||||
})
|
||||
}
|
||||
fn prev (&mut self) {
|
||||
self.cursor = self.cursor.saturating_sub(1);
|
||||
}
|
||||
fn next (&mut self) {
|
||||
self.cursor = self.cursor + 1;
|
||||
}
|
||||
fn try_preview (&mut self) -> Usually<()> {
|
||||
if let Some(path) = self.cursor_file() {
|
||||
if let Ok(sample) = Sample::from_file(&path) {
|
||||
*self.sample.write().unwrap() = sample;
|
||||
self.voices.write().unwrap().push(
|
||||
Sample::play(&self.sample, 0, &u7::from(100u8))
|
||||
);
|
||||
}
|
||||
//load_sample(&path)?;
|
||||
//let src = std::fs::File::open(&path)?;
|
||||
//let mss = MediaSourceStream::new(Box::new(src), Default::default());
|
||||
//let mut hint = Hint::new();
|
||||
//if let Some(ext) = path.extension() {
|
||||
//hint.with_extension(&ext.to_string_lossy());
|
||||
//}
|
||||
//let meta_opts: MetadataOptions = Default::default();
|
||||
//let fmt_opts: FormatOptions = Default::default();
|
||||
//if let Ok(mut probed) = symphonia::default::get_probe()
|
||||
//.format(&hint, mss, &fmt_opts, &meta_opts)
|
||||
//{
|
||||
//panic!("{:?}", probed.format.metadata());
|
||||
//};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn cursor_dir (&self) -> Option<PathBuf> {
|
||||
if self.cursor < self.subdirs.len() {
|
||||
Some(self.dir.join(&self.subdirs[self.cursor]))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
fn cursor_file (&self) -> Option<PathBuf> {
|
||||
if self.cursor < self.subdirs.len() {
|
||||
return None
|
||||
}
|
||||
let index = self.cursor.saturating_sub(self.subdirs.len());
|
||||
if index < self.files.len() {
|
||||
Some(self.dir.join(&self.files[index]))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
fn pick (&mut self) -> Usually<bool> {
|
||||
if self.cursor == 0 {
|
||||
if let Some(parent) = self.dir.parent() {
|
||||
self.dir = parent.into();
|
||||
self.rescan()?;
|
||||
self.cursor = 0;
|
||||
return Ok(false)
|
||||
}
|
||||
}
|
||||
if let Some(dir) = self.cursor_dir() {
|
||||
self.dir = dir;
|
||||
self.rescan()?;
|
||||
self.cursor = 0;
|
||||
return Ok(false)
|
||||
}
|
||||
if let Some(path) = self.cursor_file() {
|
||||
let (end, channels) = read_sample_data(&path.to_string_lossy())?;
|
||||
let mut sample = self.sample.write().unwrap();
|
||||
sample.name = path.file_name().unwrap().to_string_lossy().into();
|
||||
sample.end = end;
|
||||
sample.channels = channels;
|
||||
return Ok(true)
|
||||
}
|
||||
return Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}]
|
||||
});
|
||||
|
||||
fn scan (dir: &PathBuf) -> Usually<(Vec<OsString>, Vec<OsString>)> {
|
||||
let (mut subdirs, mut files) = read_dir(dir)?
|
||||
.fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{
|
||||
let entry = entry.expect("failed to read drectory entry");
|
||||
let meta = entry.metadata().expect("failed to read entry metadata");
|
||||
if meta.is_file() {
|
||||
files.push(entry.file_name());
|
||||
} else if meta.is_dir() {
|
||||
subdirs.push(entry.file_name());
|
||||
}
|
||||
(subdirs, files)
|
||||
});
|
||||
subdirs.sort();
|
||||
files.sort();
|
||||
Ok((subdirs, files))
|
||||
}
|
||||
|
||||
impl Sample {
|
||||
fn from_file (path: &PathBuf) -> Usually<Self> {
|
||||
let mut sample = Self::default();
|
||||
sample.name = path.file_name().unwrap().to_string_lossy().into();
|
||||
// Use file extension if present
|
||||
let mut hint = Hint::new();
|
||||
if let Some(ext) = path.extension() {
|
||||
hint.with_extension(&ext.to_string_lossy());
|
||||
}
|
||||
let probed = symphonia::default::get_probe().format(
|
||||
&hint,
|
||||
MediaSourceStream::new(
|
||||
Box::new(File::open(path)?),
|
||||
Default::default(),
|
||||
),
|
||||
&Default::default(),
|
||||
&Default::default()
|
||||
)?;
|
||||
let mut format = probed.format;
|
||||
let mut decoder = get_codecs().make(
|
||||
&format.tracks().iter()
|
||||
.find(|t| t.codec_params.codec != CODEC_TYPE_NULL)
|
||||
.expect("no tracks found")
|
||||
.codec_params,
|
||||
&Default::default()
|
||||
)?;
|
||||
loop {
|
||||
match format.next_packet() {
|
||||
Ok(packet) => {
|
||||
// Decode a packet
|
||||
let decoded = match decoder.decode(&packet) {
|
||||
Ok(decoded) => decoded,
|
||||
Err(err) => { return Err(err.into()); }
|
||||
};
|
||||
// Determine sample rate
|
||||
let spec = *decoded.spec();
|
||||
if let Some(rate) = sample.rate {
|
||||
if rate != spec.rate as usize {
|
||||
panic!("sample rate changed");
|
||||
}
|
||||
} else {
|
||||
sample.rate = Some(spec.rate as usize);
|
||||
}
|
||||
// Determine channel count
|
||||
while sample.channels.len() < spec.channels.count() {
|
||||
sample.channels.push(vec![]);
|
||||
}
|
||||
// Load sample
|
||||
let mut samples = SampleBuffer::new(
|
||||
decoded.frames() as u64,
|
||||
spec
|
||||
);
|
||||
if samples.capacity() > 0 {
|
||||
samples.copy_interleaved_ref(decoded);
|
||||
for frame in samples.samples().chunks(spec.channels.count()) {
|
||||
for (chan, frame) in frame.iter().enumerate() {
|
||||
sample.channels[chan].push(*frame)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(Error::IoError(_)) => break decoder.last_decoded(),
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
};
|
||||
sample.end = sample.channels.iter().fold(0, |l, c|l + c.len());
|
||||
Ok(sample)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render<Tui> for SamplerView<Tui> {
|
||||
fn min_size (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
||||
todo!()
|
||||
}
|
||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||
tui_render_sampler(self, to)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tui_render_sampler (sampler: &SamplerView<Tui>, to: &mut TuiOutput) -> Usually<()> {
|
||||
let [x, y, _, height] = to.area();
|
||||
let style = Style::default().gray();
|
||||
let title = format!(" {} ({})", sampler.name, sampler.voices.read().unwrap().len());
|
||||
to.blit(&title, x+1, y, Some(style.white().bold().not_dim()));
|
||||
let mut width = title.len() + 2;
|
||||
let mut y1 = 1;
|
||||
let mut j = 0;
|
||||
for (note, sample) in sampler.mapped.iter()
|
||||
.map(|(note, sample)|(Some(note), sample))
|
||||
.chain(sampler.unmapped.iter().map(|sample|(None, sample)))
|
||||
{
|
||||
if y1 >= height {
|
||||
break
|
||||
}
|
||||
let active = j == sampler.cursor.0;
|
||||
width = width.max(
|
||||
draw_sample(to, x, y + y1, note, &*sample.read().unwrap(), active)?
|
||||
);
|
||||
y1 = y1 + 1;
|
||||
j = j + 1;
|
||||
}
|
||||
let height = ((2 + y1) as u16).min(height);
|
||||
//Ok(Some([x, y, (width as u16).min(to.area().w()), height]))
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw_sample (
|
||||
to: &mut TuiOutput, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool
|
||||
) -> Usually<usize> {
|
||||
let style = if focus { Style::default().green() } else { Style::default() };
|
||||
if focus {
|
||||
to.blit(&"🬴", x+1, y, Some(style.bold()));
|
||||
}
|
||||
let label1 = format!("{:3} {:12}",
|
||||
note.map(|n|n.to_string()).unwrap_or(String::default()),
|
||||
sample.name);
|
||||
let label2 = format!("{:>6} {:>6} +0.0",
|
||||
sample.start,
|
||||
sample.end);
|
||||
to.blit(&label1, x+2, y, Some(style.bold()));
|
||||
to.blit(&label2, x+3+label1.len()as u16, y, Some(style));
|
||||
Ok(label1.len() + label2.len() + 4)
|
||||
}
|
||||
|
||||
impl Render<Tui> for AddSampleModal {
|
||||
fn min_size (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
||||
todo!()
|
||||
//Align::Center(()).layout(to)
|
||||
}
|
||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||
todo!()
|
||||
//let area = to.area();
|
||||
//to.make_dim();
|
||||
//let area = center_box(
|
||||
//area,
|
||||
//64.max(area.w().saturating_sub(8)),
|
||||
//20.max(area.w().saturating_sub(8)),
|
||||
//);
|
||||
//to.fill_fg(area, Color::Reset);
|
||||
//to.fill_bg(area, Nord::bg_lo(true, true));
|
||||
//to.fill_char(area, ' ');
|
||||
//to.blit(&format!("{}", &self.dir.to_string_lossy()), area.x()+2, area.y()+1, Some(Style::default().bold()))?;
|
||||
//to.blit(&"Select sample:", area.x()+2, area.y()+2, Some(Style::default().bold()))?;
|
||||
//for (i, (is_dir, name)) in self.subdirs.iter()
|
||||
//.map(|path|(true, path))
|
||||
//.chain(self.files.iter().map(|path|(false, path)))
|
||||
//.enumerate()
|
||||
//.skip(self.offset)
|
||||
//{
|
||||
//if i >= area.h() as usize - 4 {
|
||||
//break
|
||||
//}
|
||||
//let t = if is_dir { "" } else { "" };
|
||||
//let line = format!("{t} {}", name.to_string_lossy());
|
||||
//let line = &line[..line.len().min(area.w() as usize - 4)];
|
||||
//to.blit(&line, area.x() + 2, area.y() + 3 + i as u16, Some(if i == self.cursor {
|
||||
//Style::default().green()
|
||||
//} else {
|
||||
//Style::default().white()
|
||||
//}))?;
|
||||
//}
|
||||
//Lozenge(Style::default()).draw(to)
|
||||
}
|
||||
}
|
||||
52
crates/tek/src/tui/_todo_tui_sampler_cmd.rs
Normal file
52
crates/tek/src/tui/_todo_tui_sampler_cmd.rs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
use crate::*;
|
||||
impl Handle<Tui> for Sampler<Tui> {
|
||||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||
match from.event() {
|
||||
key!(KeyCode::Up) => {
|
||||
self.cursor.0 = if self.cursor.0 == 0 {
|
||||
self.mapped.len() + self.unmapped.len() - 1
|
||||
} else {
|
||||
self.cursor.0 - 1
|
||||
};
|
||||
Ok(Some(true))
|
||||
},
|
||||
key!(KeyCode::Down) => {
|
||||
self.cursor.0 = (self.cursor.0 + 1) % (self.mapped.len() + self.unmapped.len());
|
||||
Ok(Some(true))
|
||||
},
|
||||
key!(KeyCode::Char('p')) => {
|
||||
if let Some(sample) = self.sample() {
|
||||
self.voices.write().unwrap().push(Sample::play(sample, 0, &100.into()));
|
||||
}
|
||||
Ok(Some(true))
|
||||
},
|
||||
key!(KeyCode::Char('a')) => {
|
||||
let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![])));
|
||||
*self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &self.voices)?));
|
||||
self.unmapped.push(sample);
|
||||
Ok(Some(true))
|
||||
},
|
||||
key!(KeyCode::Char('r')) => {
|
||||
if let Some(sample) = self.sample() {
|
||||
*self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &self.voices)?));
|
||||
}
|
||||
Ok(Some(true))
|
||||
},
|
||||
key!(KeyCode::Enter) => {
|
||||
if let Some(sample) = self.sample() {
|
||||
self.editing = Some(sample.clone());
|
||||
}
|
||||
Ok(Some(true))
|
||||
}
|
||||
_ => Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Handle<Tui> for AddSampleModal {
|
||||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||
if from.handle_keymap(self, KEYMAP_ADD_SAMPLE)? {
|
||||
return Ok(Some(true))
|
||||
}
|
||||
Ok(Some(true))
|
||||
}
|
||||
}
|
||||
239
crates/tek/src/tui/app_arranger.rs
Normal file
239
crates/tek/src/tui/app_arranger.rs
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
use crate::*;
|
||||
|
||||
/// Root view for standalone `tek_arranger`
|
||||
pub struct ArrangerTui {
|
||||
pub jack: Arc<RwLock<JackClient>>,
|
||||
pub clock: ClockModel,
|
||||
pub phrases: PhraseListModel,
|
||||
pub tracks: Vec<ArrangerTrack>,
|
||||
pub scenes: Vec<ArrangerScene>,
|
||||
pub name: Arc<RwLock<String>>,
|
||||
pub splits: [u16;2],
|
||||
pub selected: ArrangerSelection,
|
||||
pub mode: ArrangerMode,
|
||||
pub color: ItemColor,
|
||||
pub entered: bool,
|
||||
pub size: Measure<Tui>,
|
||||
pub cursor: (usize, usize),
|
||||
pub menu_bar: Option<MenuBar<Tui, Self, ArrangerCommand>>,
|
||||
pub status_bar: Option<ArrangerStatus>,
|
||||
pub history: Vec<ArrangerCommand>,
|
||||
pub note_buf: Vec<u8>,
|
||||
pub midi_buf: Vec<Vec<Vec<u8>>>,
|
||||
pub editor: PhraseEditorModel,
|
||||
pub focus: FocusState<ArrangerFocus>,
|
||||
pub perf: PerfModel,
|
||||
}
|
||||
|
||||
impl TryFrom<&Arc<RwLock<JackClient>>> for ArrangerTui {
|
||||
type Error = Box<dyn std::error::Error>;
|
||||
fn try_from (jack: &Arc<RwLock<JackClient>>) -> Usually<Self> {
|
||||
Ok(Self {
|
||||
jack: jack.clone(),
|
||||
clock: ClockModel::from(jack),
|
||||
phrases: PhraseListModel::default(),
|
||||
editor: PhraseEditorModel::default(),
|
||||
selected: ArrangerSelection::Clip(0, 0),
|
||||
scenes: vec![],
|
||||
tracks: vec![],
|
||||
color: Color::Rgb(28, 35, 25).into(),
|
||||
history: vec![],
|
||||
mode: ArrangerMode::Vertical(2),
|
||||
name: Arc::new(RwLock::new(String::new())),
|
||||
size: Measure::new(),
|
||||
cursor: (0, 0),
|
||||
splits: [20, 20],
|
||||
entered: false,
|
||||
menu_bar: None,
|
||||
status_bar: None,
|
||||
midi_buf: vec![vec![];65536],
|
||||
note_buf: vec![],
|
||||
perf: PerfModel::default(),
|
||||
focus: FocusState::Entered(ArrangerFocus::Transport(TransportFocus::PlayPause)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl HasClock for ArrangerTui {
|
||||
fn clock (&self) -> &ClockModel {
|
||||
&self.clock
|
||||
}
|
||||
}
|
||||
impl HasClock for ArrangerTrack {
|
||||
fn clock (&self) -> &ClockModel {
|
||||
&self.player.clock()
|
||||
}
|
||||
}
|
||||
impl HasPhrases for ArrangerTui {
|
||||
fn phrases (&self) -> &Vec<Arc<RwLock<Phrase>>> {
|
||||
&self.phrases.phrases
|
||||
}
|
||||
fn phrases_mut (&mut self) -> &mut Vec<Arc<RwLock<Phrase>>> {
|
||||
&mut self.phrases.phrases
|
||||
}
|
||||
}
|
||||
|
||||
/// Sections in the arranger app that may be focused
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
pub enum ArrangerFocus {
|
||||
/// The transport (toolbar) is focused
|
||||
Transport(TransportFocus),
|
||||
/// The arrangement (grid) is focused
|
||||
Arranger,
|
||||
/// The phrase list (pool) is focused
|
||||
Phrases,
|
||||
/// The phrase editor (sequencer) is focused
|
||||
PhraseEditor,
|
||||
}
|
||||
|
||||
impl_focus!(ArrangerTui ArrangerFocus [
|
||||
//&[
|
||||
//Menu,
|
||||
//Menu,
|
||||
//Menu,
|
||||
//Menu,
|
||||
//Menu,
|
||||
//],
|
||||
&[
|
||||
Transport(TransportFocus::PlayPause),
|
||||
Transport(TransportFocus::Bpm),
|
||||
Transport(TransportFocus::Sync),
|
||||
Transport(TransportFocus::Quant),
|
||||
Transport(TransportFocus::Clock),
|
||||
], &[
|
||||
Arranger,
|
||||
Arranger,
|
||||
Arranger,
|
||||
Arranger,
|
||||
Arranger,
|
||||
], &[
|
||||
Phrases,
|
||||
Phrases,
|
||||
PhraseEditor,
|
||||
PhraseEditor,
|
||||
PhraseEditor,
|
||||
],
|
||||
]);
|
||||
|
||||
/// Status bar for arranger app
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum ArrangerStatus {
|
||||
Transport,
|
||||
ArrangerMix,
|
||||
ArrangerTrack,
|
||||
ArrangerScene,
|
||||
ArrangerClip,
|
||||
PhrasePool,
|
||||
PhraseView,
|
||||
PhraseEdit,
|
||||
}
|
||||
|
||||
impl StatusBar 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!(|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, Tui::fill_x(Tui::to_east(mode, commands)))
|
||||
|
||||
});
|
||||
211
crates/tek/src/tui/app_sequencer.rs
Normal file
211
crates/tek/src/tui/app_sequencer.rs
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
use crate::*;
|
||||
|
||||
/// Root view for standalone `tek_sequencer`.
|
||||
pub struct SequencerTui {
|
||||
pub jack: Arc<RwLock<JackClient>>,
|
||||
pub clock: ClockModel,
|
||||
pub phrases: PhraseListModel,
|
||||
pub player: PhrasePlayerModel,
|
||||
pub editor: PhraseEditorModel,
|
||||
pub size: Measure<Tui>,
|
||||
pub cursor: (usize, usize),
|
||||
pub split: u16,
|
||||
pub entered: bool,
|
||||
pub note_buf: Vec<u8>,
|
||||
pub midi_buf: Vec<Vec<Vec<u8>>>,
|
||||
pub focus: FocusState<SequencerFocus>,
|
||||
pub perf: PerfModel,
|
||||
}
|
||||
|
||||
impl TryFrom<&Arc<RwLock<JackClient>>> for SequencerTui {
|
||||
type Error = Box<dyn std::error::Error>;
|
||||
fn try_from (jack: &Arc<RwLock<JackClient>>) -> Usually<Self> {
|
||||
let clock = ClockModel::from(jack);
|
||||
Ok(Self {
|
||||
jack: jack.clone(),
|
||||
phrases: PhraseListModel::default(),
|
||||
player: PhrasePlayerModel::from(&clock),
|
||||
editor: PhraseEditorModel::default(),
|
||||
size: Measure::new(),
|
||||
cursor: (0, 0),
|
||||
entered: false,
|
||||
split: 20,
|
||||
midi_buf: vec![vec![];65536],
|
||||
note_buf: vec![],
|
||||
clock,
|
||||
perf: PerfModel::default(),
|
||||
focus: FocusState::Focused(SequencerFocus::Transport(TransportFocus::PlayPause))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl HasClock for SequencerTui {
|
||||
fn clock (&self) -> &ClockModel {
|
||||
&self.clock
|
||||
}
|
||||
}
|
||||
|
||||
impl HasPhrases for SequencerTui {
|
||||
fn phrases (&self) -> &Vec<Arc<RwLock<Phrase>>> {
|
||||
&self.phrases.phrases
|
||||
}
|
||||
fn phrases_mut (&mut self) -> &mut Vec<Arc<RwLock<Phrase>>> {
|
||||
&mut self.phrases.phrases
|
||||
}
|
||||
}
|
||||
|
||||
/// Sections in the sequencer app that may be focused
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
pub enum SequencerFocus {
|
||||
/// The transport (toolbar) is focused
|
||||
Transport(TransportFocus),
|
||||
/// The phrase list (pool) is focused
|
||||
PhraseList,
|
||||
/// The phrase editor (sequencer) is focused
|
||||
PhraseEditor,
|
||||
|
||||
PhrasePlay,
|
||||
PhraseNext,
|
||||
}
|
||||
|
||||
impl_focus!(SequencerTui SequencerFocus [
|
||||
//&[
|
||||
//Menu,
|
||||
//Menu,
|
||||
//Menu,
|
||||
//Menu,
|
||||
//Menu,
|
||||
//],
|
||||
&[
|
||||
Transport(TransportFocus::PlayPause),
|
||||
Transport(TransportFocus::Bpm),
|
||||
Transport(TransportFocus::Sync),
|
||||
Transport(TransportFocus::Quant),
|
||||
Transport(TransportFocus::Clock),
|
||||
],
|
||||
&[
|
||||
PhrasePlay,
|
||||
PhrasePlay,
|
||||
PhraseEditor,
|
||||
PhraseEditor,
|
||||
PhraseEditor,
|
||||
],
|
||||
&[
|
||||
PhraseNext,
|
||||
PhraseNext,
|
||||
PhraseEditor,
|
||||
PhraseEditor,
|
||||
PhraseEditor,
|
||||
],
|
||||
&[
|
||||
PhraseList,
|
||||
PhraseList,
|
||||
PhraseEditor,
|
||||
PhraseEditor,
|
||||
PhraseEditor,
|
||||
],
|
||||
] => [self: {
|
||||
if self.focus.is_entered() && self.focus.inner() == SequencerFocus::PhraseEditor {
|
||||
self.editor.edit_mode = PhraseEditMode::Note
|
||||
} else {
|
||||
self.editor.edit_mode = PhraseEditMode::Scroll
|
||||
}
|
||||
}]);
|
||||
|
||||
/// Status bar for sequencer app
|
||||
#[derive(Clone)]
|
||||
pub struct SequencerStatusBar {
|
||||
pub(crate) cpu: Option<String>,
|
||||
pub(crate) width: usize,
|
||||
pub(crate) size: String,
|
||||
pub(crate) res: String,
|
||||
pub(crate) mode: &'static str,
|
||||
pub(crate) help: &'static [(&'static str, &'static str, &'static str)]
|
||||
}
|
||||
|
||||
impl StatusBar for SequencerStatusBar {
|
||||
type State = SequencerTui;
|
||||
fn hotkey_fg () -> Color {
|
||||
TuiTheme::hotkey_fg()
|
||||
}
|
||||
fn update (&mut self, state: &SequencerTui) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&SequencerTui> for SequencerStatusBar {
|
||||
fn from (state: &SequencerTui) -> Self {
|
||||
use SequencerFocus::*;
|
||||
use TransportFocus::*;
|
||||
let samples = state.clock.chunk.load(Ordering::Relaxed);
|
||||
let rate = state.clock.timebase.sr.get() as f64;
|
||||
let buffer = samples as f64 / rate;
|
||||
let width = state.size.w();
|
||||
let default_help = &[("", "⏎", " enter"), ("", "✣", " navigate")];
|
||||
Self {
|
||||
width,
|
||||
cpu: state.perf.percentage().map(|cpu|format!("│{cpu:.01}%")),
|
||||
size: format!("{}x{}│", width, state.size.h()),
|
||||
res: format!("│{}s│{:.1}kHz│{:.1}ms│", samples, rate / 1000., buffer * 1000.),
|
||||
mode: match state.focused() {
|
||||
Transport(PlayPause) => " PLAY/PAUSE ",
|
||||
Transport(Bpm) => " TEMPO ",
|
||||
Transport(Sync) => " LAUNCH SYNC ",
|
||||
Transport(Quant) => " REC QUANT ",
|
||||
Transport(Clock) => " SEEK ",
|
||||
PhrasePlay => " TO PLAY ",
|
||||
PhraseNext => " UP NEXT ",
|
||||
PhraseList => " PHRASES ",
|
||||
PhraseEditor => match state.editor.edit_mode {
|
||||
PhraseEditMode::Note => " EDIT MIDI ",
|
||||
PhraseEditMode::Scroll => " VIEW MIDI ",
|
||||
},
|
||||
},
|
||||
help: match state.focused() {
|
||||
Transport(PlayPause) => &[
|
||||
("", "⏎", " play/pause"),
|
||||
("", "✣", " navigate"),
|
||||
],
|
||||
Transport(Bpm) => &[
|
||||
("", ".,", " inc/dec"),
|
||||
("", "><", " fine"),
|
||||
],
|
||||
Transport(Sync) => &[
|
||||
("", ".,", " inc/dec"),
|
||||
],
|
||||
Transport(Quant) => &[
|
||||
("", ".,", " inc/dec"),
|
||||
],
|
||||
Transport(Clock) => &[
|
||||
("", ".,", " by beat"),
|
||||
("", "<>", " by time"),
|
||||
],
|
||||
PhraseList => if state.entered() {
|
||||
&[
|
||||
("", "↕", " pick"),
|
||||
("", ".,", " move"),
|
||||
("", "⏎", " play"),
|
||||
("", "e", " edit"),
|
||||
]
|
||||
} else {
|
||||
default_help
|
||||
},
|
||||
PhraseEditor => match state.editor.edit_mode {
|
||||
PhraseEditMode::Note => &[
|
||||
("", "✣", " cursor"),
|
||||
],
|
||||
PhraseEditMode::Scroll => &[
|
||||
("", "✣", " scroll"),
|
||||
],
|
||||
}
|
||||
_ => if state.entered() {
|
||||
&[
|
||||
("", "Esc", " exit")
|
||||
]
|
||||
} else {
|
||||
default_help
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
101
crates/tek/src/tui/app_transport.rs
Normal file
101
crates/tek/src/tui/app_transport.rs
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
use crate::*;
|
||||
|
||||
/// Stores and displays time-related info.
|
||||
pub struct TransportTui {
|
||||
pub jack: Arc<RwLock<JackClient>>,
|
||||
pub clock: ClockModel,
|
||||
pub size: Measure<Tui>,
|
||||
pub cursor: (usize, usize),
|
||||
pub focus: FocusState<TransportFocus>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for TransportTui {
|
||||
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||
f.debug_struct("TransportTui")
|
||||
.field("jack", &self.jack)
|
||||
.field("size", &self.size)
|
||||
.field("cursor", &self.cursor)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create app state from JACK handle.
|
||||
impl TryFrom<&Arc<RwLock<JackClient>>> for TransportTui {
|
||||
type Error = Box<dyn std::error::Error>;
|
||||
fn try_from (jack: &Arc<RwLock<JackClient>>) -> Usually<Self> {
|
||||
Ok(Self {
|
||||
jack: jack.clone(),
|
||||
clock: ClockModel::from(jack),
|
||||
size: Measure::new(),
|
||||
cursor: (0, 0),
|
||||
focus: FocusState::Entered(TransportFocus::PlayPause)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl HasClock for TransportTui {
|
||||
fn clock (&self) -> &ClockModel {
|
||||
&self.clock
|
||||
}
|
||||
}
|
||||
|
||||
/// Which item of the transport toolbar is focused
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum TransportFocus {
|
||||
Bpm,
|
||||
Sync,
|
||||
PlayPause,
|
||||
Clock,
|
||||
Quant,
|
||||
}
|
||||
|
||||
impl FocusWrap<TransportFocus> for TransportFocus {
|
||||
fn wrap <'a, W: Render<Tui>> (self, focus: TransportFocus, content: &'a W)
|
||||
-> impl Render<Tui> + 'a
|
||||
{
|
||||
let focused = focus == self;
|
||||
let corners = focused.then_some(CORNERS);
|
||||
//let highlight = focused.then_some(Tui::bg(Color::Rgb(60, 70, 50)));
|
||||
lay!(corners, /*highlight,*/ *content)
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusWrap<TransportFocus> for Option<TransportFocus> {
|
||||
fn wrap <'a, W: Render<Tui>> (self, focus: TransportFocus, content: &'a W)
|
||||
-> impl Render<Tui> + 'a
|
||||
{
|
||||
let focused = Some(focus) == self;
|
||||
let corners = focused.then_some(CORNERS);
|
||||
//let highlight = focused.then_some(Background(Color::Rgb(60, 70, 50)));
|
||||
lay!(corners, /*highlight,*/ *content)
|
||||
}
|
||||
}
|
||||
|
||||
impl_focus!(TransportTui TransportFocus [
|
||||
//&[Menu],
|
||||
&[
|
||||
PlayPause,
|
||||
Bpm,
|
||||
Sync,
|
||||
Quant,
|
||||
Clock,
|
||||
],
|
||||
]);
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct TransportStatusBar;
|
||||
|
||||
impl StatusBar for TransportStatusBar {
|
||||
type State = ();
|
||||
fn hotkey_fg () -> Color {
|
||||
TuiTheme::hotkey_fg()
|
||||
}
|
||||
fn update (&mut self, state: &()) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
render!(|self: TransportStatusBar|{
|
||||
todo!();
|
||||
""
|
||||
});
|
||||
258
crates/tek/src/tui/ctrl_arranger.rs
Normal file
258
crates/tek/src/tui/ctrl_arranger.rs
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
use crate::*;
|
||||
|
||||
impl Handle<Tui> for ArrangerTui {
|
||||
fn handle (&mut self, i: &TuiInput) -> Perhaps<bool> {
|
||||
ArrangerCommand::execute_with_state(self, i)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ArrangerCommand {
|
||||
Focus(FocusCommand),
|
||||
Undo,
|
||||
Redo,
|
||||
Clear,
|
||||
Color(ItemColor),
|
||||
Clock(ClockCommand),
|
||||
Scene(ArrangerSceneCommand),
|
||||
Track(ArrangerTrackCommand),
|
||||
Clip(ArrangerClipCommand),
|
||||
Select(ArrangerSelection),
|
||||
Zoom(usize),
|
||||
Phrases(PhrasesCommand),
|
||||
Editor(PhraseCommand),
|
||||
}
|
||||
|
||||
impl Command<ArrangerTui> for ArrangerCommand {
|
||||
fn execute (self, state: &mut ArrangerTui) -> Perhaps<Self> {
|
||||
use ArrangerCommand::*;
|
||||
Ok(match self {
|
||||
Focus(cmd) => cmd.execute(state)?.map(Focus),
|
||||
Scene(cmd) => cmd.execute(state)?.map(Scene),
|
||||
Track(cmd) => cmd.execute(state)?.map(Track),
|
||||
Clip(cmd) => cmd.execute(state)?.map(Clip),
|
||||
Phrases(cmd) => cmd.execute(&mut state.phrases)?.map(Phrases),
|
||||
Editor(cmd) => cmd.execute(&mut state.editor)?.map(Editor),
|
||||
Clock(cmd) => cmd.execute(state)?.map(Clock),
|
||||
Zoom(zoom) => { todo!(); },
|
||||
Select(selected) => {
|
||||
*state.selected_mut() = selected;
|
||||
None
|
||||
},
|
||||
_ => { todo!() }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Command<ArrangerTui> for ArrangerSceneCommand {
|
||||
fn execute (self, state: &mut ArrangerTui) -> Perhaps<Self> {
|
||||
todo!();
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl Command<ArrangerTui> for ArrangerTrackCommand {
|
||||
fn execute (self, state: &mut ArrangerTui) -> Perhaps<Self> {
|
||||
todo!();
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl Command<ArrangerTui> for ArrangerClipCommand {
|
||||
fn execute (self, state: &mut ArrangerTui) -> Perhaps<Self> {
|
||||
todo!();
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ArrangerControl: TransportControl {
|
||||
fn selected (&self) -> ArrangerSelection;
|
||||
fn selected_mut (&mut self) -> &mut ArrangerSelection;
|
||||
fn activate (&mut self) -> Usually<()>;
|
||||
fn selected_phrase (&self) -> Option<Arc<RwLock<Phrase>>>;
|
||||
fn toggle_loop (&mut self);
|
||||
fn randomize_color (&mut self);
|
||||
}
|
||||
|
||||
impl ArrangerControl for ArrangerTui {
|
||||
fn selected (&self) -> ArrangerSelection {
|
||||
self.selected
|
||||
}
|
||||
fn selected_mut (&mut self) -> &mut ArrangerSelection {
|
||||
&mut self.selected
|
||||
}
|
||||
fn activate (&mut self) -> Usually<()> {
|
||||
if let ArrangerSelection::Scene(s) = self.selected {
|
||||
for (t, track) in self.tracks.iter_mut().enumerate() {
|
||||
let phrase = self.scenes[s].clips[t].clone();
|
||||
if track.player.play_phrase.is_some() || phrase.is_some() {
|
||||
track.player.enqueue_next(phrase.as_ref());
|
||||
}
|
||||
}
|
||||
if self.clock().is_stopped() {
|
||||
self.clock().play_from(Some(0))?;
|
||||
}
|
||||
} else if let ArrangerSelection::Clip(t, s) = self.selected {
|
||||
let phrase = self.scenes()[s].clips[t].clone();
|
||||
self.tracks_mut()[t].player.enqueue_next(phrase.as_ref());
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
fn selected_phrase (&self) -> Option<Arc<RwLock<Phrase>>> {
|
||||
self.selected_scene()?.clips.get(self.selected.track()?)?.clone()
|
||||
}
|
||||
fn toggle_loop (&mut self) {
|
||||
if let Some(phrase) = self.selected_phrase() {
|
||||
phrase.write().unwrap().toggle_loop()
|
||||
}
|
||||
}
|
||||
fn randomize_color (&mut self) {
|
||||
match self.selected {
|
||||
ArrangerSelection::Mix => {
|
||||
self.color = ItemColor::random_dark()
|
||||
},
|
||||
ArrangerSelection::Track(t) => {
|
||||
self.tracks_mut()[t].color = ItemColor::random()
|
||||
},
|
||||
ArrangerSelection::Scene(s) => {
|
||||
self.scenes_mut()[s].color = ItemColor::random()
|
||||
},
|
||||
ArrangerSelection::Clip(t, s) => {
|
||||
if let Some(phrase) = &self.scenes_mut()[s].clips[t] {
|
||||
phrase.write().unwrap().color = ItemColorTriplet::random();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl InputToCommand<Tui, ArrangerTui> for ArrangerCommand {
|
||||
fn input_to_command (state: &ArrangerTui, input: &TuiInput) -> Option<Self> {
|
||||
to_arranger_command(state, input)
|
||||
.or_else(||to_focus_command(input).map(ArrangerCommand::Focus))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn to_arranger_command (state: &ArrangerTui, input: &TuiInput) -> Option<ArrangerCommand> {
|
||||
use ArrangerCommand as Cmd;
|
||||
use KeyCode::Char;
|
||||
if !state.entered() {
|
||||
return None
|
||||
}
|
||||
Some(match input.event() {
|
||||
key!(Char('e')) => Cmd::Editor(PhraseCommand::Show(Some(
|
||||
state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone()
|
||||
))),
|
||||
_ => match state.focused() {
|
||||
ArrangerFocus::Transport(_) => {
|
||||
match TransportCommand::input_to_command(state, input)? {
|
||||
TransportCommand::Clock(command) => Cmd::Clock(command),
|
||||
_ => return None,
|
||||
}
|
||||
},
|
||||
ArrangerFocus::PhraseEditor => {
|
||||
Cmd::Editor(PhraseCommand::input_to_command(&state.editor, input)?)
|
||||
},
|
||||
ArrangerFocus::Phrases => {
|
||||
Cmd::Phrases(PhrasesCommand::input_to_command(&state.phrases, input)?)
|
||||
},
|
||||
ArrangerFocus::Arranger => {
|
||||
use ArrangerSelection::*;
|
||||
match input.event() {
|
||||
key!(Char('l')) => Cmd::Clip(ArrangerClipCommand::SetLoop(false)),
|
||||
key!(Char('+')) => Cmd::Zoom(0), // TODO
|
||||
key!(Char('=')) => Cmd::Zoom(0), // TODO
|
||||
key!(Char('_')) => Cmd::Zoom(0), // TODO
|
||||
key!(Char('-')) => Cmd::Zoom(0), // TODO
|
||||
key!(Char('`')) => { todo!("toggle state mode") },
|
||||
key!(Ctrl-Char('a')) => Cmd::Scene(ArrangerSceneCommand::Add),
|
||||
key!(Ctrl-Char('t')) => Cmd::Track(ArrangerTrackCommand::Add),
|
||||
_ => match state.selected() {
|
||||
Mix => to_arranger_mix_command(input)?,
|
||||
Track(t) => to_arranger_track_command(input, t)?,
|
||||
Scene(s) => to_arranger_scene_command(input, s)?,
|
||||
Clip(t, s) => to_arranger_clip_command(input, t, s)?,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn to_arranger_mix_command (input: &TuiInput) -> Option<ArrangerCommand> {
|
||||
use KeyCode::{Char, Down, Right, Delete};
|
||||
use ArrangerCommand as Cmd;
|
||||
use ArrangerSelection as Select;
|
||||
Some(match input.event() {
|
||||
key!(Down) => Cmd::Select(Select::Scene(0)),
|
||||
key!(Right) => Cmd::Select(Select::Track(0)),
|
||||
key!(Char(',')) => Cmd::Zoom(0),
|
||||
key!(Char('.')) => Cmd::Zoom(0),
|
||||
key!(Char('<')) => Cmd::Zoom(0),
|
||||
key!(Char('>')) => Cmd::Zoom(0),
|
||||
key!(Delete) => Cmd::Clear,
|
||||
key!(Char('c')) => Cmd::Color(ItemColor::random()),
|
||||
_ => return None
|
||||
})
|
||||
}
|
||||
|
||||
fn to_arranger_track_command (input: &TuiInput, t: usize) -> Option<ArrangerCommand> {
|
||||
use KeyCode::{Char, Down, Left, Right, Delete};
|
||||
use ArrangerCommand as Cmd;
|
||||
use ArrangerSelection as Select;
|
||||
use ArrangerTrackCommand as Track;
|
||||
Some(match input.event() {
|
||||
key!(Down) => Cmd::Select(Select::Clip(t, 0)),
|
||||
key!(Left) => Cmd::Select(if t > 0 { Select::Track(t - 1) } else { Select::Mix }),
|
||||
key!(Right) => Cmd::Select(Select::Track(t + 1)),
|
||||
key!(Char(',')) => Cmd::Track(Track::Swap(t, t - 1)),
|
||||
key!(Char('.')) => Cmd::Track(Track::Swap(t, t + 1)),
|
||||
key!(Char('<')) => Cmd::Track(Track::Swap(t, t - 1)),
|
||||
key!(Char('>')) => Cmd::Track(Track::Swap(t, t + 1)),
|
||||
key!(Delete) => Cmd::Track(Track::Delete(t)),
|
||||
//key!(Char('c')) => Cmd::Track(Track::Color(t, ItemColor::random())),
|
||||
_ => return None
|
||||
})
|
||||
}
|
||||
|
||||
fn to_arranger_scene_command (input: &TuiInput, s: usize) -> Option<ArrangerCommand> {
|
||||
use KeyCode::{Char, Up, Down, Right, Enter, Delete};
|
||||
use ArrangerCommand as Cmd;
|
||||
use ArrangerSelection as Select;
|
||||
use ArrangerSceneCommand as Scene;
|
||||
Some(match input.event() {
|
||||
key!(Up) => Cmd::Select(if s > 0 { Select::Scene(s - 1) } else { Select::Mix }),
|
||||
key!(Down) => Cmd::Select(Select::Scene(s + 1)),
|
||||
key!(Right) => Cmd::Select(Select::Clip(0, s)),
|
||||
key!(Char(',')) => Cmd::Scene(Scene::Swap(s, s - 1)),
|
||||
key!(Char('.')) => Cmd::Scene(Scene::Swap(s, s + 1)),
|
||||
key!(Char('<')) => Cmd::Scene(Scene::Swap(s, s - 1)),
|
||||
key!(Char('>')) => Cmd::Scene(Scene::Swap(s, s + 1)),
|
||||
key!(Enter) => Cmd::Scene(Scene::Play(s)),
|
||||
key!(Delete) => Cmd::Scene(Scene::Delete(s)),
|
||||
//key!(Char('c')) => Cmd::Track(Scene::Color(s, ItemColor::random())),
|
||||
_ => return None
|
||||
})
|
||||
}
|
||||
|
||||
fn to_arranger_clip_command (input: &TuiInput, t: usize, s: usize) -> Option<ArrangerCommand> {
|
||||
use KeyCode::{Char, Up, Down, Left, Right, Delete};
|
||||
use ArrangerCommand as Cmd;
|
||||
use ArrangerSelection as Select;
|
||||
use ArrangerClipCommand as Clip;
|
||||
Some(match input.event() {
|
||||
key!(Up) => Cmd::Select(if s > 0 { Select::Clip(t, s - 1) } else { Select::Track(t) }),
|
||||
key!(Down) => Cmd::Select(Select::Clip(t, s + 1)),
|
||||
key!(Left) => Cmd::Select(if t > 0 { Select::Clip(t - 1, s) } else { Select::Scene(s) }),
|
||||
key!(Right) => Cmd::Select(Select::Clip(t + 1, s)),
|
||||
key!(Char(',')) => Cmd::Clip(Clip::Set(t, s, None)),
|
||||
key!(Char('.')) => Cmd::Clip(Clip::Set(t, s, None)),
|
||||
key!(Char('<')) => Cmd::Clip(Clip::Set(t, s, None)),
|
||||
key!(Char('>')) => Cmd::Clip(Clip::Set(t, s, None)),
|
||||
key!(Delete) => Cmd::Clip(Clip::Set(t, s, None)),
|
||||
//key!(Char('c')) => Cmd::Clip(Clip::Color(t, s, ItemColor::random())),
|
||||
//key!(Char('g')) => Cmd::Clip(Clip(Clip::Get(t, s))),
|
||||
//key!(Char('s')) => Cmd::Clip(Clip(Clip::Set(t, s))),
|
||||
_ => return None
|
||||
})
|
||||
}
|
||||
116
crates/tek/src/tui/ctrl_file_browser.rs
Normal file
116
crates/tek/src/tui/ctrl_file_browser.rs
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
use crate::*;
|
||||
|
||||
/// Commands supported by [FileBrowser]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum FileBrowserCommand {
|
||||
Begin,
|
||||
Cancel,
|
||||
Confirm,
|
||||
Select(usize),
|
||||
Chdir(PathBuf),
|
||||
Filter(String),
|
||||
}
|
||||
|
||||
impl Command<PhraseListModel> for FileBrowserCommand {
|
||||
fn execute (self, state: &mut PhraseListModel) -> Perhaps<Self> {
|
||||
use FileBrowserCommand::*;
|
||||
use PhrasesMode::{Import, Export};
|
||||
let mode = state.phrases_mode_mut();
|
||||
match mode {
|
||||
Some(Import(index, ref mut browser)) => match self {
|
||||
Cancel => {
|
||||
*mode = None;
|
||||
},
|
||||
Chdir(cwd) => {
|
||||
*mode = Some(Import(*index, FileBrowser::new(Some(cwd))?));
|
||||
},
|
||||
Select(index) => {
|
||||
browser.index = index;
|
||||
},
|
||||
Confirm => {
|
||||
if browser.is_file() {
|
||||
let index = *index;
|
||||
let path = browser.path();
|
||||
*mode = None;
|
||||
PhrasePoolCommand::Import(index, path).execute(state)?;
|
||||
} else if browser.is_dir() {
|
||||
*mode = Some(Import(*index, browser.chdir()?));
|
||||
}
|
||||
},
|
||||
_ => todo!(),
|
||||
_ => unreachable!()
|
||||
},
|
||||
Some(PhrasesMode::Export(index, ref mut browser)) => match self {
|
||||
Cancel => {
|
||||
*mode = None;
|
||||
},
|
||||
Chdir(cwd) => {
|
||||
*mode = Some(PhrasesMode::Export(*index, FileBrowser::new(Some(cwd))?));
|
||||
},
|
||||
Select(index) => {
|
||||
browser.index = index;
|
||||
},
|
||||
_ => unreachable!()
|
||||
},
|
||||
_ => unreachable!(),
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl InputToCommand<Tui, PhraseListModel> for FileBrowserCommand {
|
||||
fn input_to_command (state: &PhraseListModel, from: &TuiInput) -> Option<Self> {
|
||||
use KeyCode::{Up, Down, Right, Left, Enter, Esc, Char, Backspace};
|
||||
use FileBrowserCommand::*;
|
||||
if let Some(PhrasesMode::Import(index, browser)) = state.phrases_mode() {
|
||||
Some(match from.event() {
|
||||
key!(Up) => Select(
|
||||
browser.index.overflowing_sub(1).0.min(browser.len().saturating_sub(1))
|
||||
),
|
||||
key!(Down) => Select(
|
||||
browser.index.saturating_add(1) % browser.len()
|
||||
),
|
||||
key!(Right) => Chdir(browser.cwd.clone()),
|
||||
key!(Left) => Chdir(browser.cwd.clone()),
|
||||
key!(Enter) => Confirm,
|
||||
key!(Char(c)) => { todo!() },
|
||||
key!(Backspace) => { todo!() },
|
||||
key!(Esc) => Self::Cancel,
|
||||
_ => return None
|
||||
})
|
||||
} else if let Some(PhrasesMode::Export(index, browser)) = state.phrases_mode() {
|
||||
Some(match from.event() {
|
||||
key!(Up) => Select(browser.index.overflowing_sub(1).0.min(browser.len())),
|
||||
key!(Down) => Select(browser.index.saturating_add(1) % browser.len()),
|
||||
key!(Right) => Chdir(browser.cwd.clone()),
|
||||
key!(Left) => Chdir(browser.cwd.clone()),
|
||||
key!(Enter) => Confirm,
|
||||
key!(Char(c)) => { todo!() },
|
||||
key!(Backspace) => { todo!() },
|
||||
key!(Esc) => Self::Cancel,
|
||||
_ => return None
|
||||
})
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InputToCommand<Tui, PhraseListModel> for PhraseLengthCommand {
|
||||
fn input_to_command (state: &PhraseListModel, from: &TuiInput) -> Option<Self> {
|
||||
use KeyCode::{Up, Down, Right, Left, Enter, Esc};
|
||||
if let Some(PhrasesMode::Length(_, length, _)) = state.phrases_mode() {
|
||||
Some(match from.event() {
|
||||
key!(Up) => Self::Inc,
|
||||
key!(Down) => Self::Dec,
|
||||
key!(Right) => Self::Next,
|
||||
key!(Left) => Self::Prev,
|
||||
key!(Enter) => Self::Set(*length),
|
||||
key!(Esc) => Self::Cancel,
|
||||
_ => return None
|
||||
})
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
131
crates/tek/src/tui/ctrl_phrase_editor.rs
Normal file
131
crates/tek/src/tui/ctrl_phrase_editor.rs
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum PhraseCommand {
|
||||
// TODO: 1-9 seek markers that by default start every 8th of the phrase
|
||||
AppendNote,
|
||||
PutNote,
|
||||
SetNoteCursor(usize),
|
||||
SetNoteLength(usize),
|
||||
SetNoteScroll(usize),
|
||||
SetTimeCursor(usize),
|
||||
SetTimeScroll(usize),
|
||||
SetTimeZoom(usize),
|
||||
Show(Option<Arc<RwLock<Phrase>>>),
|
||||
SetEditMode(PhraseEditMode),
|
||||
ToggleDirection,
|
||||
}
|
||||
|
||||
impl InputToCommand<Tui, PhraseEditorModel> for PhraseCommand {
|
||||
fn input_to_command (state: &PhraseEditorModel, from: &TuiInput) -> Option<Self> {
|
||||
use PhraseCommand::*;
|
||||
use KeyCode::{Char, Esc, Up, Down, PageUp, PageDown, Left, Right};
|
||||
let note_lo = state.note_lo.load(Ordering::Relaxed);
|
||||
let note_point = state.note_point.load(Ordering::Relaxed);
|
||||
let time_start = state.time_start.load(Ordering::Relaxed);
|
||||
let time_point = state.time_point.load(Ordering::Relaxed);
|
||||
let time_zoom = state.view_mode.time_zoom();
|
||||
let length = state.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1);
|
||||
Some(match from.event() {
|
||||
key!(Char('`')) => ToggleDirection,
|
||||
key!(Esc) => SetEditMode(PhraseEditMode::Scroll),
|
||||
key!(Char('-')) => SetTimeZoom(next_note_length(time_zoom)),
|
||||
key!(Char('_')) => SetTimeZoom(next_note_length(time_zoom)),
|
||||
key!(Char('=')) => SetTimeZoom(prev_note_length(time_zoom)),
|
||||
key!(Char('+')) => SetTimeZoom(prev_note_length(time_zoom)),
|
||||
key!(Char('a')) => AppendNote,
|
||||
key!(Char('s')) => PutNote,
|
||||
key!(Char('[')) => SetNoteLength(prev_note_length(state.note_len)),
|
||||
key!(Char(']')) => SetNoteLength(next_note_length(state.note_len)),
|
||||
key!(Char('n')) => { todo!("toggle keys vs notes") },
|
||||
_ => match state.edit_mode {
|
||||
PhraseEditMode::Scroll => match from.event() {
|
||||
key!(Char('e')) => SetEditMode(PhraseEditMode::Note),
|
||||
key!(Up) => SetNoteScroll(note_lo + 1),
|
||||
key!(Down) => SetNoteScroll(note_lo.saturating_sub(1)),
|
||||
key!(PageUp) => SetNoteScroll(note_lo + 3),
|
||||
key!(PageDown) => SetNoteScroll(note_lo.saturating_sub(3)),
|
||||
key!(Left) => SetTimeScroll(time_start.saturating_sub(1)),
|
||||
key!(Right) => SetTimeScroll(time_start + 1),
|
||||
_ => return None
|
||||
},
|
||||
PhraseEditMode::Note => match from.event() {
|
||||
key!(Char('e')) => SetEditMode(PhraseEditMode::Scroll),
|
||||
key!(Up) => SetNoteCursor(note_point + 1),
|
||||
key!(Down) => SetNoteCursor(note_point.saturating_sub(1)),
|
||||
key!(PageUp) => SetNoteCursor(note_point + 3),
|
||||
key!(PageDown) => SetNoteCursor(note_point.saturating_sub(3)),
|
||||
key!(Left) => SetTimeCursor(time_point.saturating_sub(time_zoom)),
|
||||
key!(Right) => SetTimeCursor((time_point + time_zoom) % length),
|
||||
_ => return None
|
||||
},
|
||||
}
|
||||
_ => return None
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Command<PhraseEditorModel> for PhraseCommand {
|
||||
fn execute (self, state: &mut PhraseEditorModel) -> Perhaps<Self> {
|
||||
use PhraseCommand::*;
|
||||
Ok(match self {
|
||||
Show(phrase) => {
|
||||
state.show_phrase(phrase);
|
||||
None
|
||||
},
|
||||
ToggleDirection => {
|
||||
todo!()
|
||||
},
|
||||
SetEditMode(mode) => {
|
||||
state.edit_mode = mode;
|
||||
None
|
||||
}
|
||||
AppendNote => {
|
||||
state.put_note();
|
||||
state.time_cursor_advance();
|
||||
None
|
||||
},
|
||||
PutNote => {
|
||||
state.put_note();
|
||||
None
|
||||
},
|
||||
SetTimeCursor(time) => {
|
||||
state.time_point.store(time, Ordering::Relaxed);
|
||||
None
|
||||
},
|
||||
SetTimeScroll(time) => {
|
||||
state.time_start.store(time, Ordering::Relaxed);
|
||||
None
|
||||
},
|
||||
SetTimeZoom(zoom) => {
|
||||
state.view_mode.set_time_zoom(zoom);
|
||||
state.show_phrase(state.phrase.clone());
|
||||
None
|
||||
},
|
||||
SetNoteScroll(note) => {
|
||||
state.note_lo.store(note, Ordering::Relaxed);
|
||||
None
|
||||
},
|
||||
SetNoteLength(time) => {
|
||||
state.note_len = time;
|
||||
None
|
||||
},
|
||||
SetNoteCursor(note) => {
|
||||
let note = 127.min(note);
|
||||
let start = state.note_lo.load(Ordering::Relaxed);
|
||||
state.note_point.store(note, Ordering::Relaxed);
|
||||
if note < start {
|
||||
state.note_lo.store(note, Ordering::Relaxed);
|
||||
}
|
||||
None
|
||||
},
|
||||
_ => unreachable!()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum PhraseEditMode {
|
||||
Note,
|
||||
Scroll,
|
||||
}
|
||||
47
crates/tek/src/tui/ctrl_phrase_length.rs
Normal file
47
crates/tek/src/tui/ctrl_phrase_length.rs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub enum PhraseLengthCommand {
|
||||
Begin,
|
||||
Cancel,
|
||||
Set(usize),
|
||||
Next,
|
||||
Prev,
|
||||
Inc,
|
||||
Dec,
|
||||
}
|
||||
|
||||
impl Command<PhraseListModel> for PhraseLengthCommand {
|
||||
fn execute (self, state: &mut PhraseListModel) -> Perhaps<Self> {
|
||||
use PhraseLengthFocus::*;
|
||||
use PhraseLengthCommand::*;
|
||||
match state.phrases_mode_mut().clone() {
|
||||
Some(PhrasesMode::Length(phrase, ref mut length, ref mut focus)) => match self {
|
||||
Cancel => { *state.phrases_mode_mut() = None; },
|
||||
Prev => { focus.prev() },
|
||||
Next => { focus.next() },
|
||||
Inc => match focus {
|
||||
Bar => { *length += 4 * PPQ },
|
||||
Beat => { *length += PPQ },
|
||||
Tick => { *length += 1 },
|
||||
},
|
||||
Dec => match focus {
|
||||
Bar => { *length = length.saturating_sub(4 * PPQ) },
|
||||
Beat => { *length = length.saturating_sub(PPQ) },
|
||||
Tick => { *length = length.saturating_sub(1) },
|
||||
},
|
||||
Set(length) => {
|
||||
let mut phrase = state.phrases()[phrase].write().unwrap();
|
||||
let old_length = phrase.length;
|
||||
phrase.length = length;
|
||||
std::mem::drop(phrase);
|
||||
*state.phrases_mode_mut() = None;
|
||||
return Ok(Some(Self::Set(old_length)))
|
||||
},
|
||||
_ => unreachable!()
|
||||
},
|
||||
_ => unreachable!()
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
140
crates/tek/src/tui/ctrl_phrase_list.rs
Normal file
140
crates/tek/src/tui/ctrl_phrase_list.rs
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub enum PhrasesCommand {
|
||||
Select(usize),
|
||||
Phrase(PhrasePoolCommand),
|
||||
Rename(PhraseRenameCommand),
|
||||
Length(PhraseLengthCommand),
|
||||
Import(FileBrowserCommand),
|
||||
Export(FileBrowserCommand),
|
||||
}
|
||||
|
||||
impl Command<PhraseListModel> for PhrasesCommand {
|
||||
fn execute (self, state: &mut PhraseListModel) -> Perhaps<Self> {
|
||||
use PhrasesCommand::*;
|
||||
Ok(match self {
|
||||
Phrase(command) => command.execute(state)?.map(Phrase),
|
||||
Rename(command) => match command {
|
||||
PhraseRenameCommand::Begin => {
|
||||
let length = state.phrases()[state.phrase_index()].read().unwrap().length;
|
||||
*state.phrases_mode_mut() = Some(
|
||||
PhrasesMode::Length(state.phrase_index(), length, PhraseLengthFocus::Bar)
|
||||
);
|
||||
None
|
||||
},
|
||||
_ => command.execute(state)?.map(Rename)
|
||||
},
|
||||
Length(command) => match command {
|
||||
PhraseLengthCommand::Begin => {
|
||||
let name = state.phrases()[state.phrase_index()].read().unwrap().name.clone();
|
||||
*state.phrases_mode_mut() = Some(
|
||||
PhrasesMode::Rename(state.phrase_index(), name)
|
||||
);
|
||||
None
|
||||
},
|
||||
_ => command.execute(state)?.map(Length)
|
||||
},
|
||||
Import(command) => match command {
|
||||
FileBrowserCommand::Begin => {
|
||||
*state.phrases_mode_mut() = Some(
|
||||
PhrasesMode::Import(state.phrase_index(), FileBrowser::new(None)?)
|
||||
);
|
||||
None
|
||||
},
|
||||
_ => command.execute(state)?.map(Import)
|
||||
},
|
||||
Export(command) => match command {
|
||||
FileBrowserCommand::Begin => {
|
||||
*state.phrases_mode_mut() = Some(
|
||||
PhrasesMode::Export(state.phrase_index(), FileBrowser::new(None)?)
|
||||
);
|
||||
None
|
||||
},
|
||||
_ => command.execute(state)?.map(Export)
|
||||
},
|
||||
Select(phrase) => {
|
||||
state.set_phrase_index(phrase);
|
||||
None
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl HasPhrases for PhraseListModel {
|
||||
fn phrases (&self) -> &Vec<Arc<RwLock<Phrase>>> {
|
||||
&self.phrases
|
||||
}
|
||||
fn phrases_mut (&mut self) -> &mut Vec<Arc<RwLock<Phrase>>> {
|
||||
&mut self.phrases
|
||||
}
|
||||
}
|
||||
|
||||
impl InputToCommand<Tui, PhraseListModel> for PhrasesCommand {
|
||||
fn input_to_command (state: &PhraseListModel, input: &TuiInput) -> Option<Self> {
|
||||
use PhraseRenameCommand as Rename;
|
||||
use PhraseLengthCommand as Length;
|
||||
use FileBrowserCommand as Browse;
|
||||
Some(match state.phrases_mode() {
|
||||
Some(PhrasesMode::Rename(..)) => Self::Rename(Rename::input_to_command(state, input)?),
|
||||
Some(PhrasesMode::Length(..)) => Self::Length(Length::input_to_command(state, input)?),
|
||||
Some(PhrasesMode::Import(..)) => Self::Import(Browse::input_to_command(state, input)?),
|
||||
Some(PhrasesMode::Export(..)) => Self::Export(Browse::input_to_command(state, input)?),
|
||||
_ => to_phrases_command(state, input)?
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn to_phrases_command (state: &PhraseListModel, input: &TuiInput) -> Option<PhrasesCommand> {
|
||||
use KeyCode::{Up, Down, Delete, Char};
|
||||
use PhrasesCommand as Cmd;
|
||||
use PhrasePoolCommand as Pool;
|
||||
use PhraseRenameCommand as Rename;
|
||||
use PhraseLengthCommand as Length;
|
||||
use FileBrowserCommand as Browse;
|
||||
let index = state.phrase_index();
|
||||
let count = state.phrases().len();
|
||||
Some(match input.event() {
|
||||
key!(Char('n')) => Cmd::Rename(Rename::Begin),
|
||||
key!(Char('t')) => Cmd::Length(Length::Begin),
|
||||
key!(Char('m')) => Cmd::Import(Browse::Begin),
|
||||
key!(Char('x')) => Cmd::Export(Browse::Begin),
|
||||
key!(Char('c')) => Cmd::Phrase(Pool::SetColor(index, ItemColor::random())),
|
||||
key!(Up) => Cmd::Select(
|
||||
index.overflowing_sub(1).0.min(state.phrases().len() - 1)
|
||||
),
|
||||
key!(Down) => Cmd::Select(
|
||||
index.saturating_add(1) % state.phrases().len()
|
||||
),
|
||||
key!(Char(',')) => if index > 1 {
|
||||
state.set_phrase_index(state.phrase_index().saturating_sub(1));
|
||||
Cmd::Phrase(Pool::Swap(index - 1, index))
|
||||
} else {
|
||||
return None
|
||||
},
|
||||
key!(Char('.')) => if index < count.saturating_sub(1) {
|
||||
state.set_phrase_index(state.phrase_index() + 1);
|
||||
Cmd::Phrase(Pool::Swap(index + 1, index))
|
||||
} else {
|
||||
return None
|
||||
},
|
||||
key!(Delete) => if index > 0 {
|
||||
state.set_phrase_index(index.min(count.saturating_sub(1)));
|
||||
Cmd::Phrase(Pool::Delete(index))
|
||||
} else {
|
||||
return None
|
||||
},
|
||||
key!(Char('a')) => Cmd::Phrase(Pool::Add(count, Phrase::new(
|
||||
String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random())
|
||||
))),
|
||||
key!(Char('i')) => Cmd::Phrase(Pool::Add(index + 1, Phrase::new(
|
||||
String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random())
|
||||
))),
|
||||
key!(Char('d')) => {
|
||||
let mut phrase = state.phrases()[index].read().unwrap().duplicate();
|
||||
phrase.color = ItemColorTriplet::random_near(phrase.color, 0.25);
|
||||
Cmd::Phrase(Pool::Add(index + 1, phrase))
|
||||
},
|
||||
_ => return None
|
||||
})
|
||||
}
|
||||
60
crates/tek/src/tui/ctrl_phrase_rename.rs
Normal file
60
crates/tek/src/tui/ctrl_phrase_rename.rs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum PhraseRenameCommand {
|
||||
Begin,
|
||||
Cancel,
|
||||
Confirm,
|
||||
Set(String),
|
||||
}
|
||||
|
||||
impl Command<PhraseListModel> for PhraseRenameCommand {
|
||||
fn execute (self, state: &mut PhraseListModel) -> Perhaps<Self> {
|
||||
use PhraseRenameCommand::*;
|
||||
match state.phrases_mode_mut().clone() {
|
||||
Some(PhrasesMode::Rename(phrase, ref mut old_name)) => match self {
|
||||
Set(s) => {
|
||||
state.phrases()[phrase].write().unwrap().name = s.into();
|
||||
return Ok(Some(Self::Set(old_name.clone())))
|
||||
},
|
||||
Confirm => {
|
||||
let old_name = old_name.clone();
|
||||
*state.phrases_mode_mut() = None;
|
||||
return Ok(Some(Self::Set(old_name)))
|
||||
},
|
||||
Cancel => {
|
||||
state.phrases()[phrase].write().unwrap().name = old_name.clone();
|
||||
},
|
||||
_ => unreachable!()
|
||||
},
|
||||
_ => unreachable!()
|
||||
};
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl InputToCommand<Tui, PhraseListModel> for PhraseRenameCommand {
|
||||
fn input_to_command (state: &PhraseListModel, from: &TuiInput) -> Option<Self> {
|
||||
use KeyCode::{Char, Backspace, Enter, Esc};
|
||||
if let Some(PhrasesMode::Rename(_, ref old_name)) = state.phrases_mode() {
|
||||
Some(match from.event() {
|
||||
key!(Char(c)) => {
|
||||
let mut new_name = old_name.clone();
|
||||
new_name.push(*c);
|
||||
Self::Set(new_name)
|
||||
},
|
||||
key!(Backspace) => {
|
||||
let mut new_name = old_name.clone();
|
||||
new_name.pop();
|
||||
Self::Set(new_name)
|
||||
},
|
||||
key!(Enter) => Self::Confirm,
|
||||
key!(Esc) => Self::Cancel,
|
||||
_ => return None
|
||||
})
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
98
crates/tek/src/tui/ctrl_sequencer.rs
Normal file
98
crates/tek/src/tui/ctrl_sequencer.rs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
use crate::*;
|
||||
|
||||
impl Handle<Tui> for SequencerTui {
|
||||
fn handle (&mut self, i: &TuiInput) -> Perhaps<bool> {
|
||||
SequencerCommand::execute_with_state(self, i)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum SequencerCommand {
|
||||
Focus(FocusCommand),
|
||||
Clock(ClockCommand),
|
||||
Phrases(PhrasesCommand),
|
||||
Editor(PhraseCommand),
|
||||
Enqueue(Option<Arc<RwLock<Phrase>>>),
|
||||
Clear,
|
||||
Undo,
|
||||
Redo,
|
||||
}
|
||||
|
||||
impl Command<SequencerTui> for SequencerCommand {
|
||||
fn execute (self, state: &mut SequencerTui) -> Perhaps<Self> {
|
||||
use SequencerCommand::*;
|
||||
Ok(match self {
|
||||
Focus(cmd) => cmd.execute(state)?.map(Focus),
|
||||
Phrases(cmd) => cmd.execute(&mut state.phrases)?.map(Phrases),
|
||||
Editor(cmd) => cmd.execute(&mut state.editor)?.map(Editor),
|
||||
Clock(cmd) => cmd.execute(state)?.map(Clock),
|
||||
Enqueue(phrase) => {
|
||||
state.player.enqueue_next(phrase.as_ref());
|
||||
None
|
||||
},
|
||||
Undo => { todo!() },
|
||||
Redo => { todo!() },
|
||||
Clear => { todo!() },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl InputToCommand<Tui, SequencerTui> for SequencerCommand {
|
||||
fn input_to_command (state: &SequencerTui, input: &TuiInput) -> Option<Self> {
|
||||
if state.entered() {
|
||||
to_sequencer_command(state, input)
|
||||
.or_else(||to_focus_command(input).map(SequencerCommand::Focus))
|
||||
} else {
|
||||
to_focus_command(input).map(SequencerCommand::Focus)
|
||||
.or_else(||to_sequencer_command(state, input))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option<SequencerCommand> {
|
||||
use SequencerCommand::*;
|
||||
use PhraseCommand::Show;
|
||||
use ClockCommand::{Play, Pause};
|
||||
use KeyCode::{Char, Enter};
|
||||
Some(match input.event() {
|
||||
// Play/pause
|
||||
key!(Char(' ')) => Clock(
|
||||
if state.clock().is_stopped() { Play(None) } else { Pause(None) }
|
||||
),
|
||||
// Play from start/rewind to start
|
||||
key!(Shift-Char(' ')) => Clock(
|
||||
if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }
|
||||
),
|
||||
// Edit phrase
|
||||
key!(Char('e')) => match state.focused() {
|
||||
SequencerFocus::PhrasePlay => Editor(Show(
|
||||
state.player.play_phrase().as_ref().map(|x|x.1.as_ref()).flatten().map(|x|x.clone())
|
||||
)),
|
||||
SequencerFocus::PhraseNext => Editor(Show(
|
||||
state.player.next_phrase().as_ref().map(|x|x.1.as_ref()).flatten().map(|x|x.clone())
|
||||
)),
|
||||
SequencerFocus::PhraseList => Editor(Show(
|
||||
Some(state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone())
|
||||
)),
|
||||
_ => return None,
|
||||
},
|
||||
_ => match state.focused() {
|
||||
SequencerFocus::Transport(_) => match TransportCommand::input_to_command(state, input)? {
|
||||
TransportCommand::Clock(command) => Clock(command),
|
||||
_ => return None,
|
||||
},
|
||||
SequencerFocus::PhraseEditor => Editor(
|
||||
PhraseCommand::input_to_command(&state.editor, input)?
|
||||
),
|
||||
SequencerFocus::PhraseList => match input.event() {
|
||||
key!(Enter) => Enqueue(Some(
|
||||
state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone()
|
||||
)),
|
||||
_ => Phrases(
|
||||
PhrasesCommand::input_to_command(&state.phrases, input)?
|
||||
),
|
||||
}
|
||||
_ => return None
|
||||
}
|
||||
})
|
||||
}
|
||||
134
crates/tek/src/tui/ctrl_transport.rs
Normal file
134
crates/tek/src/tui/ctrl_transport.rs
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
use crate::*;
|
||||
|
||||
impl Handle<Tui> for TransportTui {
|
||||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||
TransportCommand::execute_with_state(self, from)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum TransportCommand {
|
||||
Focus(FocusCommand),
|
||||
Clock(ClockCommand),
|
||||
}
|
||||
|
||||
impl<T: TransportControl> Command<T> for TransportCommand {
|
||||
fn execute (self, state: &mut T) -> Perhaps<Self> {
|
||||
use TransportCommand::{Focus, Clock};
|
||||
use FocusCommand::{Next, Prev};
|
||||
use ClockCommand::{SetBpm, SetQuant, SetSync};
|
||||
Ok(match self {
|
||||
Focus(cmd) => cmd.execute(state)?.map(Focus),
|
||||
Clock(cmd) => cmd.execute(state)?.map(Clock),
|
||||
//Clock(SetBpm(bpm)) => Some(Clock(SetBpm(state.bpm().set(bpm)))),
|
||||
//Clock(SetQuant(quant)) => Some(Clock(SetQuant(state.quant().set(quant)))),
|
||||
//Clock(SetSync(sync)) => Some(Clock(SetSync(state.sync().set(sync)))),
|
||||
_ => return Ok(None)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub trait TransportControl: HasClock + FocusGrid + HasEnter {
|
||||
fn transport_focused (&self) -> Option<TransportFocus>;
|
||||
}
|
||||
|
||||
impl TransportControl for TransportTui {
|
||||
fn transport_focused (&self) -> Option<TransportFocus> {
|
||||
Some(self.focus.inner())
|
||||
}
|
||||
}
|
||||
|
||||
impl TransportControl for SequencerTui {
|
||||
fn transport_focused (&self) -> Option<TransportFocus> {
|
||||
match self.focus.inner() {
|
||||
SequencerFocus::Transport(focus) => Some(focus),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TransportControl for ArrangerTui {
|
||||
fn transport_focused (&self) -> Option<TransportFocus> {
|
||||
match self.focus.inner() {
|
||||
ArrangerFocus::Transport(focus) => Some(focus),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: TransportControl> InputToCommand<Tui, T> for TransportCommand {
|
||||
fn input_to_command (state: &T, input: &TuiInput) -> Option<Self> {
|
||||
to_transport_command(state, input)
|
||||
.or_else(||to_focus_command(input).map(TransportCommand::Focus))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_transport_command <T> (state: &T, input: &TuiInput) -> Option<TransportCommand>
|
||||
where
|
||||
T: TransportControl
|
||||
{
|
||||
use ClockCommand::{SetBpm, SetQuant, SetSync};
|
||||
use TransportCommand::{Focus, Clock};
|
||||
use KeyCode::{Enter, Left, Right, Char};
|
||||
Some(match input.event() {
|
||||
key!(Left) => Focus(FocusCommand::Prev),
|
||||
key!(Right) => Focus(FocusCommand::Next),
|
||||
key!(Char(' ')) => Clock(if state.clock().is_stopped() {
|
||||
ClockCommand::Play(None)
|
||||
} else {
|
||||
ClockCommand::Pause(None)
|
||||
}),
|
||||
key!(Shift-Char(' ')) => Clock(if state.clock().is_stopped() {
|
||||
ClockCommand::Play(Some(0))
|
||||
} else {
|
||||
ClockCommand::Pause(Some(0))
|
||||
}),
|
||||
_ => match state.transport_focused().unwrap() {
|
||||
TransportFocus::Bpm => match input.event() {
|
||||
key!(Char(',')) => Clock(SetBpm(state.clock().bpm().get() - 1.0)),
|
||||
key!(Char('.')) => Clock(SetBpm(state.clock().bpm().get() + 1.0)),
|
||||
key!(Char('<')) => Clock(SetBpm(state.clock().bpm().get() - 0.001)),
|
||||
key!(Char('>')) => Clock(SetBpm(state.clock().bpm().get() + 0.001)),
|
||||
_ => return None,
|
||||
},
|
||||
TransportFocus::Quant => match input.event() {
|
||||
key!(Char(',')) => Clock(SetQuant(state.clock().quant.prev())),
|
||||
key!(Char('.')) => Clock(SetQuant(state.clock().quant.next())),
|
||||
key!(Char('<')) => Clock(SetQuant(state.clock().quant.prev())),
|
||||
key!(Char('>')) => Clock(SetQuant(state.clock().quant.next())),
|
||||
_ => return None,
|
||||
},
|
||||
TransportFocus::Sync => match input.event() {
|
||||
key!(Char(',')) => Clock(SetSync(state.clock().sync.prev())),
|
||||
key!(Char('.')) => Clock(SetSync(state.clock().sync.next())),
|
||||
key!(Char('<')) => Clock(SetSync(state.clock().sync.prev())),
|
||||
key!(Char('>')) => Clock(SetSync(state.clock().sync.next())),
|
||||
_ => return None,
|
||||
},
|
||||
TransportFocus::Clock => match input.event() {
|
||||
key!(Char(',')) => todo!("transport seek bar"),
|
||||
key!(Char('.')) => todo!("transport seek bar"),
|
||||
key!(Char('<')) => todo!("transport seek beat"),
|
||||
key!(Char('>')) => todo!("transport seek beat"),
|
||||
_ => return None,
|
||||
},
|
||||
TransportFocus::PlayPause => match input.event() {
|
||||
key!(Enter) => Clock(
|
||||
if state.clock().is_stopped() {
|
||||
ClockCommand::Play(None)
|
||||
} else {
|
||||
ClockCommand::Pause(None)
|
||||
}
|
||||
),
|
||||
key!(Shift-Enter) => Clock(
|
||||
if state.clock().is_stopped() {
|
||||
ClockCommand::Play(Some(0))
|
||||
} else {
|
||||
ClockCommand::Pause(Some(0))
|
||||
}
|
||||
),
|
||||
_ => return None,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
66
crates/tek/src/tui/engine_focus.rs
Normal file
66
crates/tek/src/tui/engine_focus.rs
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
use crate::*;
|
||||
|
||||
pub trait FocusWrap<T> {
|
||||
fn wrap <'a, W: Render<Tui>> (self, focus: T, content: &'a W)
|
||||
-> impl Render<Tui> + 'a;
|
||||
}
|
||||
|
||||
pub fn to_focus_command (input: &TuiInput) -> Option<FocusCommand> {
|
||||
use KeyCode::{Tab, BackTab, Up, Down, Left, Right, Enter, Esc};
|
||||
Some(match input.event() {
|
||||
key!(Tab) => FocusCommand::Next,
|
||||
key!(Shift-Tab) => FocusCommand::Prev,
|
||||
key!(BackTab) => FocusCommand::Prev,
|
||||
key!(Shift-BackTab) => FocusCommand::Prev,
|
||||
key!(Up) => FocusCommand::Up,
|
||||
key!(Down) => FocusCommand::Down,
|
||||
key!(Left) => FocusCommand::Left,
|
||||
key!(Right) => FocusCommand::Right,
|
||||
key!(Enter) => FocusCommand::Enter,
|
||||
key!(Esc) => FocusCommand::Exit,
|
||||
_ => return None
|
||||
})
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! impl_focus {
|
||||
($Struct:ident $Focus:ident $Grid:expr $(=> [$self:ident : $update_focus:expr])?) => {
|
||||
impl HasFocus for $Struct {
|
||||
type Item = $Focus;
|
||||
/// Get the currently focused item.
|
||||
fn focused (&self) -> Self::Item {
|
||||
self.focus.inner()
|
||||
}
|
||||
/// Get the currently focused item.
|
||||
fn set_focused (&mut self, to: Self::Item) {
|
||||
self.focus.set_inner(to)
|
||||
}
|
||||
$(fn focus_updated (&mut $self) { $update_focus })?
|
||||
}
|
||||
impl HasEnter for $Struct {
|
||||
/// Get the currently focused item.
|
||||
fn entered (&self) -> bool {
|
||||
self.focus.is_entered()
|
||||
}
|
||||
/// Get the currently focused item.
|
||||
fn set_entered (&mut self, entered: bool) {
|
||||
if entered {
|
||||
self.focus.to_entered()
|
||||
} else {
|
||||
self.focus.to_focused()
|
||||
}
|
||||
}
|
||||
}
|
||||
impl FocusGrid for $Struct {
|
||||
fn focus_cursor (&self) -> (usize, usize) {
|
||||
self.cursor
|
||||
}
|
||||
fn focus_cursor_mut (&mut self) -> &mut (usize, usize) {
|
||||
&mut self.cursor
|
||||
}
|
||||
fn focus_layout (&self) -> &[&[$Focus]] {
|
||||
use $Focus::*;
|
||||
&$Grid
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
181
crates/tek/src/tui/engine_input.rs
Normal file
181
crates/tek/src/tui/engine_input.rs
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
use crate::*;
|
||||
|
||||
pub struct TuiInput {
|
||||
exited: Arc<AtomicBool>,
|
||||
event: TuiEvent,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum TuiEvent {
|
||||
/// Terminal input
|
||||
Input(::crossterm::event::Event),
|
||||
/// Update values but not the whole form.
|
||||
Update,
|
||||
/// Update the whole form.
|
||||
Redraw,
|
||||
/// Device gains focus
|
||||
Focus,
|
||||
/// Device loses focus
|
||||
Blur,
|
||||
// /// JACK notification
|
||||
// Jack(JackEvent)
|
||||
}
|
||||
|
||||
impl Input<Tui> for TuiInput {
|
||||
type Event = TuiEvent;
|
||||
fn event (&self) -> &TuiEvent { &self.event }
|
||||
fn is_done (&self) -> bool { self.exited.fetch_and(true, Ordering::Relaxed) }
|
||||
fn done (&self) { self.exited.store(true, Ordering::Relaxed); }
|
||||
}
|
||||
|
||||
impl TuiInput {
|
||||
// TODO remove
|
||||
pub fn handle_keymap <T> (&self, state: &mut T, keymap: &KeyMap<T>) -> Usually<bool> {
|
||||
match self.event() {
|
||||
TuiEvent::Input(crossterm::event::Event::Key(event)) => {
|
||||
for (code, modifiers, _, _, command) in keymap.iter() {
|
||||
if *code == event.code && modifiers.bits() == event.modifiers.bits() {
|
||||
return command(state)
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
};
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
pub type KeyHandler<T> = &'static dyn Fn(&mut T)->Usually<bool>;
|
||||
|
||||
pub type KeyBinding<T> = (KeyCode, KeyModifiers, &'static str, &'static str, KeyHandler<T>);
|
||||
|
||||
pub type KeyMap<T> = [KeyBinding<T>];
|
||||
|
||||
/// Define a key
|
||||
pub const fn key (code: KeyCode) -> KeyEvent {
|
||||
let modifiers = KeyModifiers::NONE;
|
||||
let kind = KeyEventKind::Press;
|
||||
let state = KeyEventState::NONE;
|
||||
KeyEvent { code, modifiers, kind, state }
|
||||
}
|
||||
|
||||
/// Add Ctrl modifier to key
|
||||
pub const fn ctrl (key: KeyEvent) -> KeyEvent {
|
||||
KeyEvent { modifiers: key.modifiers.union(KeyModifiers::CONTROL), ..key }
|
||||
}
|
||||
|
||||
/// Add Alt modifier to key
|
||||
pub const fn alt (key: KeyEvent) -> KeyEvent {
|
||||
KeyEvent { modifiers: key.modifiers.union(KeyModifiers::ALT), ..key }
|
||||
}
|
||||
|
||||
/// Add Shift modifier to key
|
||||
pub const fn shift (key: KeyEvent) -> KeyEvent {
|
||||
KeyEvent { modifiers: key.modifiers.union(KeyModifiers::SHIFT), ..key }
|
||||
}
|
||||
|
||||
/// Define a keymap
|
||||
#[macro_export] macro_rules! keymap {
|
||||
($T:ty { $([$k:ident $(($char:literal))?, $m:ident, $n: literal, $d: literal, $f: expr]),* $(,)? }) => {
|
||||
&[
|
||||
$((KeyCode::$k $(($char))?, KeyModifiers::$m, $n, $d, &$f as KeyHandler<$T>)),*
|
||||
] as &'static [KeyBinding<$T>]
|
||||
}
|
||||
}
|
||||
|
||||
/// Define a key in a keymap
|
||||
#[macro_export] macro_rules! map_key {
|
||||
($k:ident $(($char:literal))?, $m:ident, $n: literal, $d: literal, $f: expr) => {
|
||||
(KeyCode::$k $(($char))?, KeyModifiers::$m, $n, $d, &$f as &dyn Fn()->Usually<bool>)
|
||||
}
|
||||
}
|
||||
|
||||
/// Shorthand for key match statement
|
||||
#[macro_export] macro_rules! match_key {
|
||||
($event:expr, {
|
||||
$($key:pat=>$block:expr),* $(,)?
|
||||
}) => {
|
||||
match $event {
|
||||
$(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
||||
code: $key,
|
||||
modifiers: crossterm::event::KeyModifiers::NONE,
|
||||
kind: crossterm::event::KeyEventKind::Press,
|
||||
state: crossterm::event::KeyEventState::NONE
|
||||
}) => {
|
||||
$block
|
||||
})*
|
||||
_ => Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Define key pattern in key match statement
|
||||
#[macro_export] macro_rules! key {
|
||||
($code:pat) => {
|
||||
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
||||
code: $code,
|
||||
modifiers: crossterm::event::KeyModifiers::NONE,
|
||||
kind: crossterm::event::KeyEventKind::Press,
|
||||
state: crossterm::event::KeyEventState::NONE
|
||||
}))
|
||||
};
|
||||
(Ctrl-$code:pat) => {
|
||||
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
||||
code: $code,
|
||||
modifiers: crossterm::event::KeyModifiers::CONTROL,
|
||||
kind: crossterm::event::KeyEventKind::Press,
|
||||
state: crossterm::event::KeyEventState::NONE
|
||||
}))
|
||||
};
|
||||
(Alt-$code:pat) => {
|
||||
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
||||
code: $code,
|
||||
modifiers: crossterm::event::KeyModifiers::ALT,
|
||||
kind: crossterm::event::KeyEventKind::Press,
|
||||
state: crossterm::event::KeyEventState::NONE
|
||||
}))
|
||||
};
|
||||
(Shift-$code:pat) => {
|
||||
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
||||
code: $code,
|
||||
modifiers: crossterm::event::KeyModifiers::SHIFT,
|
||||
kind: crossterm::event::KeyEventKind::Press,
|
||||
state: crossterm::event::KeyEventState::NONE
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! key_lit {
|
||||
($code:expr) => {
|
||||
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
||||
code: $code,
|
||||
modifiers: crossterm::event::KeyModifiers::NONE,
|
||||
kind: crossterm::event::KeyEventKind::Press,
|
||||
state: crossterm::event::KeyEventState::NONE
|
||||
}))
|
||||
};
|
||||
(Ctrl-$code:expr) => {
|
||||
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
||||
code: $code,
|
||||
modifiers: crossterm::event::KeyModifiers::CONTROL,
|
||||
kind: crossterm::event::KeyEventKind::Press,
|
||||
state: crossterm::event::KeyEventState::NONE
|
||||
}))
|
||||
};
|
||||
(Alt-$code:expr) => {
|
||||
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
||||
code: $code,
|
||||
modifiers: crossterm::event::KeyModifiers::ALT,
|
||||
kind: crossterm::event::KeyEventKind::Press,
|
||||
state: crossterm::event::KeyEventState::NONE
|
||||
}))
|
||||
};
|
||||
(Shift-$code:expr) => {
|
||||
TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent {
|
||||
code: $code,
|
||||
modifiers: crossterm::event::KeyModifiers::SHIFT,
|
||||
kind: crossterm::event::KeyEventKind::Press,
|
||||
state: crossterm::event::KeyEventState::NONE
|
||||
}))
|
||||
}
|
||||
}
|
||||
1
crates/tek/src/tui/engine_layout.rs
Normal file
1
crates/tek/src/tui/engine_layout.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
use crate::*;
|
||||
175
crates/tek/src/tui/engine_output.rs
Normal file
175
crates/tek/src/tui/engine_output.rs
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
use crate::*;
|
||||
use ratatui::buffer::Cell;
|
||||
|
||||
/// Every struct that has [Content]<[Tui]> is a renderable [Render]<[Tui]>.
|
||||
//impl<C: Content<Tui>> Render<Tui> for C {
|
||||
//fn min_size (&self, to: [u16;2]) -> Perhaps<E::Size> {
|
||||
//self.content().min_size(to)
|
||||
//}
|
||||
//fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||
//match self.min_size(to.area().wh().into())? {
|
||||
//Some(wh) => to.render_in(to.area().clip(wh).into(), &self.content()),
|
||||
//None => Ok(())
|
||||
//}
|
||||
//}
|
||||
//}
|
||||
|
||||
pub struct TuiOutput {
|
||||
pub buffer: Buffer,
|
||||
pub area: [u16;4]
|
||||
}
|
||||
|
||||
impl Output<Tui> for TuiOutput {
|
||||
#[inline] fn area (&self) -> [u16;4] { self.area }
|
||||
#[inline] fn area_mut (&mut self) -> &mut [u16;4] { &mut self.area }
|
||||
#[inline] fn render_in (&mut self,
|
||||
area: [u16;4],
|
||||
widget: &dyn Render<Tui>
|
||||
) -> Usually<()> {
|
||||
let last = self.area();
|
||||
*self.area_mut() = area;
|
||||
widget.render(self)?;
|
||||
*self.area_mut() = last;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl TuiOutput {
|
||||
pub fn buffer_update (&mut self,
|
||||
area: [u16;4],
|
||||
callback: &impl Fn(&mut Cell, u16, u16)
|
||||
) {
|
||||
buffer_update(&mut self.buffer, area, callback);
|
||||
}
|
||||
pub fn fill_bold (&mut self, area: [u16;4], on: bool) {
|
||||
if on {
|
||||
self.buffer_update(area, &|cell,_,_|cell.modifier.insert(Modifier::BOLD))
|
||||
} else {
|
||||
self.buffer_update(area, &|cell,_,_|cell.modifier.remove(Modifier::BOLD))
|
||||
}
|
||||
}
|
||||
pub fn fill_bg (&mut self, area: [u16;4], color: Color) {
|
||||
self.buffer_update(area, &|cell,_,_|{cell.set_bg(color);})
|
||||
}
|
||||
pub fn fill_fg (&mut self, area: [u16;4], color: Color) {
|
||||
self.buffer_update(area, &|cell,_,_|{cell.set_fg(color);})
|
||||
}
|
||||
pub fn fill_ul (&mut self, area: [u16;4], color: Color) {
|
||||
self.buffer_update(area, &|cell,_,_|{
|
||||
cell.modifier = ratatui::prelude::Modifier::UNDERLINED;
|
||||
cell.underline_color = color;
|
||||
})
|
||||
}
|
||||
pub fn fill_char (&mut self, area: [u16;4], c: char) {
|
||||
self.buffer_update(area, &|cell,_,_|{cell.set_char(c);})
|
||||
}
|
||||
pub fn make_dim (&mut self) {
|
||||
for cell in self.buffer.content.iter_mut() {
|
||||
cell.bg = ratatui::style::Color::Rgb(30,30,30);
|
||||
cell.fg = ratatui::style::Color::Rgb(100,100,100);
|
||||
cell.modifier = ratatui::style::Modifier::DIM;
|
||||
}
|
||||
}
|
||||
pub fn blit (
|
||||
&mut self, text: &impl AsRef<str>, x: u16, y: u16, style: Option<Style>
|
||||
) {
|
||||
let text = text.as_ref();
|
||||
let buf = &mut self.buffer;
|
||||
if x < buf.area.width && y < buf.area.height {
|
||||
buf.set_string(x, y, text, style.unwrap_or(Style::default()));
|
||||
}
|
||||
}
|
||||
#[inline]
|
||||
pub fn with_rect (&mut self, area: [u16;4]) -> &mut Self {
|
||||
self.area = area;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
//impl Area<u16> for Rect {
|
||||
//fn x (&self) -> u16 { self.x }
|
||||
//fn y (&self) -> u16 { self.y }
|
||||
//fn w (&self) -> u16 { self.width }
|
||||
//fn h (&self) -> u16 { self.height }
|
||||
//}
|
||||
|
||||
pub fn half_block (lower: bool, upper: bool) -> Option<char> {
|
||||
match (lower, upper) {
|
||||
(true, true) => Some('█'),
|
||||
(true, false) => Some('▄'),
|
||||
(false, true) => Some('▀'),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct BigBuffer {
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
pub content: Vec<Cell>
|
||||
}
|
||||
|
||||
impl BigBuffer {
|
||||
pub fn new (width: usize, height: usize) -> Self {
|
||||
Self { width, height, content: vec![Cell::default(); width*height] }
|
||||
}
|
||||
pub fn get (&self, x: usize, y: usize) -> Option<&Cell> {
|
||||
let i = self.index_of(x, y);
|
||||
self.content.get(i)
|
||||
}
|
||||
pub fn get_mut (&mut self, x: usize, y: usize) -> Option<&mut Cell> {
|
||||
let i = self.index_of(x, y);
|
||||
self.content.get_mut(i)
|
||||
}
|
||||
pub fn index_of (&self, x: usize, y: usize) -> usize {
|
||||
y * self.width + x
|
||||
}
|
||||
}
|
||||
|
||||
pub fn buffer_update (buf: &mut Buffer, area: [u16;4], callback: &impl Fn(&mut Cell, u16, u16)) {
|
||||
for row in 0..area.h() {
|
||||
let y = area.y() + row;
|
||||
for col in 0..area.w() {
|
||||
let x = area.x() + col;
|
||||
if x < buf.area.width && y < buf.area.height {
|
||||
callback(buf.get_mut(x, y), col, row);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render<Tui> for &str {
|
||||
fn min_size (&self, _: [u16;2]) -> Perhaps<[u16;2]> {
|
||||
// TODO: line breaks
|
||||
Ok(Some([self.chars().count() as u16, 1]))
|
||||
}
|
||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||
let [x, y, ..] = to.area();
|
||||
//let [w, h] = self.min_size(to.area().wh())?.unwrap();
|
||||
Ok(to.blit(&self, x, y, None))
|
||||
}
|
||||
}
|
||||
|
||||
impl Render<Tui> for String {
|
||||
fn min_size (&self, _: [u16;2]) -> Perhaps<[u16;2]> {
|
||||
// TODO: line breaks
|
||||
Ok(Some([self.chars().count() as u16, 1]))
|
||||
}
|
||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||
let [x, y, ..] = to.area();
|
||||
//let [w, h] = self.min_size(to.area().wh())?.unwrap();
|
||||
Ok(to.blit(&self, x, y, None))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Render<Tui>> Render<Tui> for DebugOverlay<Tui, T> {
|
||||
fn min_size (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
||||
self.1.min_size(to)
|
||||
}
|
||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||
let [x, y, w, h] = to.area();
|
||||
self.1.render(to)?;
|
||||
Ok(to.blit(&format!("{w}x{h}+{x}+{y}"), x, y, Some(Style::default().green())))
|
||||
}
|
||||
}
|
||||
|
||||
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