wip: refactor into crates

This commit is contained in:
🪞👃🪞 2024-08-03 21:55:38 +03:00
parent 96e17e7f7c
commit 5ae99b4ada
87 changed files with 2281 additions and 2217 deletions

429
Cargo.lock generated
View file

@ -211,12 +211,6 @@ dependencies = [
"rustc-demangle",
]
[[package]]
name = "base64"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "better-panic"
version = "0.3.0"
@ -454,24 +448,6 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
[[package]]
name = "clojure-reader"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fe72db90a90a91de4a9fbd79542538caa0445ebdebcd3112589cab4c1e0e10b"
dependencies = [
"ordered-float",
]
[[package]]
name = "cmake"
version = "0.1.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130"
dependencies = [
"cc",
]
[[package]]
name = "colorchoice"
version = "1.0.1"
@ -522,12 +498,6 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "const-default"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b396d1f76d455557e1218ec8066ae14bba60b4b36ecd55577ba979f5db7ecaa"
[[package]]
name = "core-foundation"
version = "0.9.4"
@ -624,125 +594,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991"
[[package]]
name = "dasp"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7381b67da416b639690ac77c73b86a7b5e64a29e31d1f75fb3b1102301ef355a"
dependencies = [
"dasp_envelope",
"dasp_frame",
"dasp_interpolate",
"dasp_peak",
"dasp_ring_buffer",
"dasp_rms",
"dasp_sample",
"dasp_signal",
"dasp_slice",
"dasp_window",
]
[[package]]
name = "dasp_envelope"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ec617ce7016f101a87fe85ed44180839744265fae73bb4aa43e7ece1b7668b6"
dependencies = [
"dasp_frame",
"dasp_peak",
"dasp_ring_buffer",
"dasp_rms",
"dasp_sample",
]
[[package]]
name = "dasp_frame"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2a3937f5fe2135702897535c8d4a5553f8b116f76c1529088797f2eee7c5cd6"
dependencies = [
"dasp_sample",
]
[[package]]
name = "dasp_interpolate"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fc975a6563bb7ca7ec0a6c784ead49983a21c24835b0bc96eea11ee407c7486"
dependencies = [
"dasp_frame",
"dasp_ring_buffer",
"dasp_sample",
]
[[package]]
name = "dasp_peak"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cf88559d79c21f3d8523d91250c397f9a15b5fc72fbb3f87fdb0a37b79915bf"
dependencies = [
"dasp_frame",
"dasp_sample",
]
[[package]]
name = "dasp_ring_buffer"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07d79e19b89618a543c4adec9c5a347fe378a19041699b3278e616e387511ea1"
[[package]]
name = "dasp_rms"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6c5dcb30b7e5014486e2822537ea2beae50b19722ffe2ed7549ab03774575aa"
dependencies = [
"dasp_frame",
"dasp_ring_buffer",
"dasp_sample",
]
[[package]]
name = "dasp_sample"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
[[package]]
name = "dasp_signal"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa1ab7d01689c6ed4eae3d38fe1cea08cba761573fbd2d592528d55b421077e7"
dependencies = [
"dasp_envelope",
"dasp_frame",
"dasp_interpolate",
"dasp_peak",
"dasp_ring_buffer",
"dasp_rms",
"dasp_sample",
"dasp_window",
]
[[package]]
name = "dasp_slice"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e1c7335d58e7baedafa516cb361360ff38d6f4d3f9d9d5ee2a2fc8e27178fa1"
dependencies = [
"dasp_frame",
"dasp_sample",
]
[[package]]
name = "dasp_window"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99ded7b88821d2ce4e8b842c9f1c86ac911891ab89443cc1de750cae764c5076"
dependencies = [
"dasp_sample",
]
[[package]]
name = "dispatch"
version = "0.2.0"
@ -850,16 +701,6 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
[[package]]
name = "fraction"
version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f158e3ff0a1b334408dc9fb811cd99b446986f4d8b741bb08f9df1604085ae7"
dependencies = [
"lazy_static",
"num",
]
[[package]]
name = "futures-channel"
version = "0.3.30"
@ -1494,15 +1335,6 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "music-math"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7475e279ecd71671f462a096fcc33ab1840d0fb7e2e984df8b096831960050"
dependencies = [
"num-traits",
]
[[package]]
name = "ndk"
version = "0.9.0"
@ -1543,70 +1375,6 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "num"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
dependencies = [
"num-bigint",
"num-complex",
"num-integer",
"num-iter",
"num-rational",
"num-traits",
]
[[package]]
name = "num-bigint"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "num-complex"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
dependencies = [
"num-traits",
]
[[package]]
name = "num-integer"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
dependencies = [
"num-traits",
]
[[package]]
name = "num-iter"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
dependencies = [
"num-bigint",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@ -1652,7 +1420,7 @@ version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b"
dependencies = [
"proc-macro-crate 1.3.1",
"proc-macro-crate 2.0.0",
"proc-macro2",
"quote",
"syn 2.0.60",
@ -1885,15 +1653,6 @@ dependencies = [
"libredox",
]
[[package]]
name = "ordered-float"
version = "4.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19ff2cf528c6c03d9ed653d6c4ce1dc0582dc4af309790ad92f07c1cd551b0be"
dependencies = [
"num-traits",
]
[[package]]
name = "owned_ttf_parser"
version = "0.24.0"
@ -2051,15 +1810,6 @@ dependencies = [
"syn 2.0.60",
]
[[package]]
name = "primal-check"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08"
dependencies = [
"num-integer",
]
[[package]]
name = "proc-macro-crate"
version = "1.3.1"
@ -2130,15 +1880,6 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "r8brain-rs"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8d8a95a5235085537051f80f1cdf704e41b1a1c749c067d381412c62da88b44"
dependencies = [
"cmake",
]
[[package]]
name = "ratatui"
version = "0.26.3"
@ -2185,15 +1926,6 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "realfft"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953d9f7e5cdd80963547b456251296efc2626ed4e3cbf36c869d9564e0220571"
dependencies = [
"rustfft",
]
[[package]]
name = "redox_syscall"
version = "0.2.16"
@ -2259,30 +1991,6 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "rlsf"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "222fb240c3286247ecdee6fa5341e7cdad0ffdf8e7e401d9937f2d58482a20bf"
dependencies = [
"cfg-if",
"const-default",
"libc",
"svgbobdoc",
]
[[package]]
name = "rubato"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5d18b486e7d29a408ef3f825bc1327d8f87af091c987ca2f5b734625940e234"
dependencies = [
"num-complex",
"num-integer",
"num-traits",
"realfft",
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
@ -2304,21 +2012,6 @@ dependencies = [
"semver",
]
[[package]]
name = "rustfft"
version = "6.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43806561bc506d0c5d160643ad742e3161049ac01027b5e6d7524091fd401d86"
dependencies = [
"num-complex",
"num-integer",
"num-traits",
"primal-check",
"strength_reduce",
"transpose",
"version_check",
]
[[package]]
name = "rustix"
version = "0.38.34"
@ -2514,12 +2207,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "strength_reduce"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82"
[[package]]
name = "strict-num"
version = "0.1.1"
@ -2564,19 +2251,6 @@ dependencies = [
"winit",
]
[[package]]
name = "svgbobdoc"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2c04b93fc15d79b39c63218f15e3fdffaa4c227830686e3b7c5f41244eb3e50"
dependencies = [
"base64",
"proc-macro2",
"quote",
"syn 1.0.109",
"unicode-width",
]
[[package]]
name = "symphonia"
version = "0.5.4"
@ -2817,32 +2491,95 @@ checksum = "4873307b7c257eddcb50c9bedf158eb669578359fb28428bef438fec8e6ba7c2"
name = "tek"
version = "0.1.0"
dependencies = [
"atomic_float",
"tek_chain",
"tek_core",
"tek_jack",
"tek_mixer",
"tek_plugin",
"tek_sampler",
"tek_sequencer",
"tek_timer",
]
[[package]]
name = "tek_chain"
version = "0.1.0"
dependencies = [
"tek_core",
"tek_jack",
]
[[package]]
name = "tek_core"
version = "0.1.0"
dependencies = [
"backtrace",
"better-panic",
"clap",
"clojure-reader",
"crossterm",
"dasp",
"fraction",
"jack",
"livi",
"microxdg",
"midly",
"music-math",
"once_cell",
"r8brain-rs",
"ratatui",
"rlsf",
"rubato",
"suil-rs",
"symphonia",
"toml",
]
[[package]]
name = "tek_jack"
version = "0.1.0"
dependencies = [
"jack",
"tek_core",
]
[[package]]
name = "tek_mixer"
version = "0.1.0"
dependencies = [
"tek_chain",
"tek_core",
"tek_jack",
]
[[package]]
name = "tek_plugin"
version = "0.1.0"
dependencies = [
"livi",
"suil-rs",
"tek_core",
"tek_jack",
"vst",
"wavers",
"winit",
]
[[package]]
name = "tek_sampler"
version = "0.1.0"
dependencies = [
"symphonia",
"tek_core",
"tek_jack",
"wavers",
]
[[package]]
name = "tek_sequencer"
version = "0.1.0"
dependencies = [
"tek_core",
"tek_jack",
"tek_timer",
]
[[package]]
name = "tek_timer"
version = "0.1.0"
dependencies = [
"atomic_float",
"tek_core",
"tek_jack",
]
[[package]]
name = "thiserror"
version = "1.0.61"
@ -2960,16 +2697,6 @@ version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
[[package]]
name = "transpose"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e"
dependencies = [
"num-integer",
"strength_reduce",
]
[[package]]
name = "ttf-parser"
version = "0.24.0"

37
crates/suil/src/gtk.rs Normal file
View file

@ -0,0 +1,37 @@
struct SuilX11Wrapper {
socket: GtkSocket,
plug: GtkPlug,
wrapper: SuilWrapper,
instance: SuilInstance,
idle_iface: LV2UI_Idle_Interface,
idle_id: usize,
idle_ms: usize,
idle_size_req_id: usize,
initial_width: usize,
initial_height: usize,
req_width: usize,
req_height: usize,
}
struct SuilX11WrapperClass {
parent_class: GtkSocketClass,
}
impl SuilX11Wrapper {
fn x_window_is_valid (&self) {
let window: GdkWindow = gtk_widget_get_window(self.plug);
let root: X11Window = Some(0);
let parent: X11Window = Some(0);
let children: [X11Window] = None;
let child_count: usize = 0;
x_query_tree(window.xdisplay, window.xid, root, parent, children, &mut childcount);
for i in 0..child_count {
if children[i] == self.instance.ui_widget {
x_free(children);
return true;
}
}
x_free(children);
return false;
}
}

View file

@ -4,36 +4,38 @@ edition = "2021"
version = "0.1.0"
[dependencies]
jack = "0.10"
clap = { version = "4.5.4", features = [ "derive" ] }
crossterm = "0.27"
ratatui = { version = "0.26.3", features = [ "unstable-widget-ref", "underline-color" ] }
backtrace = "0.3.72"
microxdg = "0.1.2"
toml = "0.8.12"
better-panic = "0.3.0"
midly = "0.5"
tek_core = { path = "../tek_core" }
tek_jack = { path = "../tek_jack" }
tek_plugin = { path = "../tek_plugin" }
tek_sampler = { path = "../tek_sampler" }
tek_sequencer = { path = "../tek_sequencer" }
tek_timer = { path = "../tek_timer" }
tek_chain = { path = "../tek_chain" }
tek_mixer = { path = "../tek_mixer" }
#jack = "0.10"
#clap = { version = "4.5.4", features = [ "derive" ] }
#crossterm = "0.27"
#ratatui = { version = "0.26.3", features = [ "unstable-widget-ref", "underline-color" ] }
#backtrace = "0.3.72"
#microxdg = "0.1.2"
#toml = "0.8.12"
#better-panic = "0.3.0"
#midly = "0.5"
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"
atomic_float = "1.0.0"
fraction = "0.15.3"
rlsf = "0.2.1"
r8brain-rs = "0.3.5"
clojure-reader = "0.1.0"
once_cell = "1.19.0"
#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"
#clojure-reader = "0.1.0"
#once_cell = "1.19.0"
symphonia = { version = "0.5.4", features = [ "all" ] }
#symphonia = { version = "0.5.4", features = [ "all" ] }
dasp = { version = "0.11.0", features = [ "all" ] }
#dasp = { version = "0.11.0", features = [ "all" ] }
rubato = "0.15.0"
winit = { version = "0.30.4", features = [ "x11" ] }
#winit = { path = "../winit" }
suil-rs = { path = "../suil" }
#rubato = "0.15.0"

3
crates/tek/README.md Normal file
View file

@ -0,0 +1,3 @@
# `tek`
This crate unifies the `tek_*` subcrates into a single application.

130
crates/tek/src/app.rs Normal file
View file

@ -0,0 +1,130 @@
use crate::*;
/// 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/sequencer
pub arranger: Arranger,
/// Main JACK client.
pub jack: Option<JackClient>,
/// 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(microxdg::XdgApp::new("tek")?);
let first_run = crate::config::AppPaths::new(&xdg)?.should_create();
let jack = JackClient::Inactive(Client::new("tek", ClientOptions::NO_START_SERVER)?.0);
*MODAL.lock().unwrap() = first_run.then(||{
Exit::boxed(crate::devices::setup::SetupModal(Some(xdg.clone()), false))
});
Ok(Self {
entered: true,
section: AppFocus::default(),
transport: TransportToolbar::new(Some(jack.transport())),
arranger: Arranger::new(),
jack: Some(jack),
audio_outs: vec![],
chain_mode: false,
chunk_size: 0,
midi_in: None,
midi_ins: vec![],
_xdg: Some(xdg),
})
}
}
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
});
impl App {
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| {
Split::down([
&self.transport,
&self.arranger,
&If(self.arranger.selected.is_clip(), &Split::right([
&ChainView::vertical(&self),
&self.sequencer,
]))
]).render(buf, area)?;
if let Some(ref modal) = *MODAL.lock().unwrap() {
modal.render(buf, area)?;
}
Ok(area)
});

View file

@ -0,0 +1,37 @@
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,
}
}
}

View file

@ -14,6 +14,7 @@ pub struct AppPaths {
data_dir: PathBuf,
project_file: PathBuf,
}
impl AppPaths {
pub fn new (xdg: &XdgApp) -> Usually<Self> {
let config_dir = PathBuf::from(xdg.app_config()?);

View file

@ -1,185 +0,0 @@
//! Prelude.
// Stdlib dependencies:
pub(crate) use std::error::Error;
pub(crate) use std::io::{stdout};
pub(crate) use std::thread::{spawn, JoinHandle};
pub(crate) use std::time::Duration;
pub(crate) use std::collections::BTreeMap;
pub(crate) use std::sync::atomic::{Ordering, AtomicBool};
pub(crate) use std::sync::{Arc, Mutex, RwLock, LockResult, RwLockReadGuard, RwLockWriteGuard};
pub(crate) use std::path::PathBuf;
pub(crate) use std::fs::read_dir;
pub(crate) use std::ffi::OsString;
// Non-stdlib dependencies:
pub(crate) use microxdg::XdgApp;
pub(crate) use midly::{MidiMessage, live::LiveEvent, num::u7};
pub(crate) use crossterm::{ExecutableCommand};
pub(crate) use crossterm::event::{Event, KeyEvent, KeyCode, KeyModifiers};
use better_panic::{Settings, Verbosity};
use crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen,
enable_raw_mode, disable_raw_mode
};
/// Define and reexport submodules.
#[macro_export] macro_rules! submod {
($($name:ident)*) => { $(mod $name; pub use self::$name::*;)* };
}
/// Define and reexport public modules.
#[macro_export] macro_rules! pubmod {
($($name:ident)*) => { $(pub mod $name;)* };
}
submod!( handle midi render time );
/// Standard result type.
pub type Usually<T> = Result<T, Box<dyn Error>>;
/// A UI component.
pub trait Component: Render + Handle + Sync {
/// Perform type erasure for collecting heterogeneous components.
fn boxed (self) -> Box<dyn Component> where Self: Sized + 'static {
Box::new(self)
}
}
pub trait Exit: Component {
fn exited (&self) -> bool;
fn exit (&mut self);
fn boxed (self) -> Box<dyn Exit> where Self: Sized + 'static {
Box::new(self)
}
}
#[macro_export] macro_rules! exit {
($T:ty) => {
impl Exit for $T {
fn exited (&self) -> bool {
self.exited
}
fn exit (&mut self) {
self.exited = true
}
}
}
}
/// Anything that implements `Render` + `Handle` can be used as a UI component.
impl<T: Render + Handle + Sync> Component for T {}
/// A UI component that may be associated with a JACK client by the `Jack` factory.
pub trait Device: Render + Handle + Process + Send + Sync {
/// Perform type erasure for collecting heterogeneous devices.
fn boxed (self) -> Box<dyn Device> where Self: Sized + 'static {
Box::new(self)
}
}
/// All things that implement the required traits can be treated as `Device`.
impl<T: Render + Handle + Process + Send + Sync> Device for T {}
// Reexport macros:
pub use crate::{
submod, pubmod, render, handle, process, phrase, keymap, ports, exit
};
// Reexport JACK proto-lib:
pub use crate::jack::*;
/// Run the main loop.
pub fn run <T> (state: Arc<RwLock<T>>) -> Usually<Arc<RwLock<T>>>
where T: Render + Handle + Send + Sync + Sized + 'static
{
let exited = Arc::new(AtomicBool::new(false));
let _input_thread = input_thread(&exited, &state);
terminal_setup()?;
panic_hook_setup();
let main_thread = main_thread(&exited, &state)?;
main_thread.join().expect("main thread failed");
terminal_teardown()?;
Ok(state)
}
/// Set up panic hook
pub fn panic_hook_setup () {
let better_panic_handler = Settings::auto().verbosity(Verbosity::Full).create_panic_handler();
std::panic::set_hook(Box::new(move |info: &std::panic::PanicInfo|{
stdout().execute(LeaveAlternateScreen).unwrap();
disable_raw_mode().unwrap();
better_panic_handler(info);
}));
}
/// Set up terminal
pub fn terminal_setup () -> Usually<()> {
stdout().execute(EnterAlternateScreen)?;
enable_raw_mode()?;
Ok(())
}
/// Cleanup
pub fn terminal_teardown () -> Usually<()> {
stdout().execute(LeaveAlternateScreen)?;
disable_raw_mode()?;
Ok(())
}
/// Main thread render loop
pub fn main_thread (
exited: &Arc<AtomicBool>,
device: &Arc<RwLock<impl Render + Send + Sync + 'static>>
) -> Usually<JoinHandle<()>> {
let exited = exited.clone();
let device = device.clone();
let mut terminal = ratatui::Terminal::new(CrosstermBackend::new(stdout()))?;
let sleep = Duration::from_millis(20);
Ok(spawn(move || loop {
if let Ok(device) = device.try_read() {
terminal.draw(|frame|{
let area = frame.size();
let buffer = frame.buffer_mut();
device
.render(buffer, area)
.expect("Failed to render content");
})
.expect("Failed to render frame");
}
if exited.fetch_and(true, Ordering::Relaxed) {
break
}
std::thread::sleep(sleep);
}))
}
/// Spawn thread that listens for user input
pub fn input_thread (
exited: &Arc<AtomicBool>,
device: &Arc<RwLock<impl Handle + Send + Sync + 'static>>
) -> JoinHandle<()> {
let poll = Duration::from_millis(100);
let exited = exited.clone();
let device = device.clone();
spawn(move || loop {
// Exit if flag is set
if exited.fetch_and(true, Ordering::Relaxed) {
break
}
// Listen for events and send them to the main thread
if ::crossterm::event::poll(poll).is_ok() {
let event = ::crossterm::event::read().unwrap();
if let Event::Key(KeyEvent {
code: KeyCode::Char('c'), modifiers: KeyModifiers::CONTROL, ..
}) = event {
exited.store(true, Ordering::Relaxed);
} else if let Err(e) = device.write().unwrap().handle(&AppEvent::Input(event)) {
panic!("{e}")
}
}
})
}

View file

@ -1,92 +0,0 @@
use crate::core::*;
pub type MIDIMessage =
Vec<u8>;
pub type MIDIChunk =
[Vec<MIDIMessage>];
/// Add "all notes off" to the start of a buffer.
pub fn all_notes_off (output: &mut MIDIChunk) {
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);
}
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
)))
}
/// Write to JACK port from output buffer (containing notes from sequence and/or monitor)
pub fn write_midi_output (writer: &mut ::jack::MidiWriter, output: &MIDIChunk, frames: usize) {
for time in 0..frames {
for event in output[time].iter() {
writer.write(&::jack::RawMidi { time: time as u32, bytes: &event })
.expect(&format!("{event:?}"));
}
}
}
/// (pulses, name)
pub const NOTE_DURATIONS: [(u16, &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"),
];
pub fn prev_note_length (ppq: u16) -> u16 {
for i in 1..=16 {
let length = NOTE_DURATIONS[16-i].0;
if length < ppq {
return length
}
}
ppq
}
pub fn next_note_length (ppq: u16) -> u16 {
for (length, _) in &NOTE_DURATIONS {
if *length > ppq {
return *length
}
}
ppq
}
pub fn ppq_to_name (ppq: u16) -> &'static str {
for (length, name) in &NOTE_DURATIONS {
if *length == ppq {
return name
}
}
""
}

View file

@ -1,166 +0,0 @@
use crate::core::*;
pub(crate) use ratatui::prelude::CrosstermBackend;
pub(crate) use ratatui::style::{Stylize, Style, Color, Modifier};
pub(crate) use ratatui::layout::Rect;
pub(crate) use ratatui::buffer::{Buffer, Cell};
use ratatui::widgets::WidgetRef;
pub fn make_dim (buf: &mut Buffer) {
for cell in buf.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 center_box (area: Rect, w: u16, h: u16) -> Rect {
let width = w.min(area.width * 3 / 5);
let height = h.min(area.width * 3 / 5);
let x = area.x + (area.width - width) / 2;
let y = area.y + (area.height - height) / 2;
Rect { x, y, width, height }
}
pub fn buffer_update (
buf: &mut Buffer, area: Rect, callback: &impl Fn(&mut Cell, u16, u16)
) {
for row in 0..area.height {
let y = area.y + row;
for col in 0..area.width {
let x = area.x + col;
if x < buf.area.width && y < buf.area.height {
callback(buf.get_mut(x, y), col, row);
}
}
}
}
pub fn fill_fg (buf: &mut Buffer, area: Rect, color: Color) {
buffer_update(buf, area, &|cell,_,_|{cell.set_fg(color);})
}
pub fn fill_bg (buf: &mut Buffer, area: Rect, color: Color) {
buffer_update(buf, area, &|cell,_,_|{cell.set_bg(color);})
}
pub fn to_fill_bg (color: Color) -> impl Render {
move |buf: &mut Buffer, area: Rect|{
fill_bg(buf, area, color);
Ok(area)
}
}
pub fn fill_char (buf: &mut Buffer, area: Rect, c: char) {
buffer_update(buf, area, &|cell,_,_|{cell.set_char(c);})
}
pub fn half_block (lower: bool, upper: bool) -> Option<char> {
match (lower, upper) {
(true, true) => Some('█'),
(true, false) => Some('▄'),
(false, true) => Some('▀'),
_ => None
}
}
pub trait Blit {
// Render something to X, Y coordinates in a buffer, ignoring width/height.
fn blit (&self, buf: &mut Buffer, x: u16, y: u16, style: Option<Style>) -> Usually<Rect>;
}
impl<T: AsRef<str>> Blit for T {
fn blit (&self, buf: &mut Buffer, x: u16, y: u16, style: Option<Style>) -> Usually<Rect> {
if x < buf.area.width && y < buf.area.height {
buf.set_string(x, y, self.as_ref(), style.unwrap_or(Style::default()));
}
Ok(Rect { x, y, width: self.as_ref().len() as u16, height: 1 })
}
}
/// Trait for things that render to the display.
pub trait Render: Send {
// Render something to an area of the buffer.
// Returns area used by component.
// This is insufficient but for the most basic dynamic layout algorithms.
fn render (&self, _b: &mut Buffer, _a: Rect) -> Usually<Rect> {
Ok(Rect { x: 0, y: 0, width: 0, height: 0 })
}
}
/// Implement the `Render` trait.
#[macro_export] macro_rules! render {
($T:ty) => {
impl Render for $T {}
};
($T:ty |$self:ident, $buf:ident, $area:ident|$block:expr) => {
impl Render for $T {
fn render (&$self, $buf: &mut Buffer, $area: Rect) -> Usually<Rect> {
$block
}
}
};
($T:ty = $render:path) => {
impl Render for $T {
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
$render(self, buf, area)
}
}
}
}
impl Render for () {
fn render (&self, _: &mut Buffer, a: Rect) -> Usually<Rect> {
Ok(Rect { x: a.x, y: a.y, width: 0, height: 0 })
}
}
impl<T: Fn(&mut Buffer, Rect) -> Usually<Rect> + Send> Render for T {
fn render (&self, b: &mut Buffer, a: Rect) -> Usually<Rect> {
(*self)(b, a)
}
}
impl Render for Box<dyn Device> {
fn render (&self, b: &mut Buffer, a: Rect) -> Usually<Rect> {
(**self).render(b, a)
}
}
impl<T: Render> Render for Arc<Mutex<T>> {
fn render (&self, b: &mut Buffer, a: Rect) -> Usually<Rect> {
self.lock().unwrap().render(b, a)
}
}
impl<T: Render + Sync> Render for Arc<RwLock<T>> {
fn render (&self, b: &mut Buffer, a: Rect) -> Usually<Rect> {
self.read().unwrap().render(b, a)
}
}
impl WidgetRef for &dyn Render {
fn render_ref (&self, area: Rect, buf: &mut Buffer) {
Render::render(*self, buf, area).expect("Failed to render device.");
}
}
impl WidgetRef for dyn Render {
fn render_ref (&self, area: Rect, buf: &mut Buffer) {
Render::render(self, buf, area).expect("Failed to render device.");
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_line_buffer () {
let mut buffer = LineBuffer::new(Cell::default(), 12, 0);
assert_eq!(buffer.cells.len(), 0);
buffer.put("FOO", 0, 0);
assert_eq!(buffer.cells.len(), 12);
buffer.put("FOO", 6, 0);
assert_eq!(buffer.cells.len(), 12);
buffer.put("FOO", 11, 0);
assert_eq!(buffer.cells.len(), 12);
buffer.put("FOO", 12, 0);
assert_eq!(buffer.cells.len(), 12);
buffer.put("FOO", 24, 0);
assert_eq!(buffer.cells.len(), 12);
buffer.put("FOO", 0, 1);
assert_eq!(buffer.cells.len(), 24);
}
}

View file

@ -1,2 +0,0 @@
//! Music-making apparatuses.
crate::core::pubmod!{arranger chain help looper mixer plugin sampler setup sequencer transport}

View file

@ -1,12 +0,0 @@
//! TODO: audio looper (merge with [crate::devices::sampler::Sampler]?)
use crate::core::*;
pub struct Looper {
pub name: String
}
render!(Looper);
handle!(Looper);
process!(Looper);
ports!(Looper);

View file

@ -1,190 +0,0 @@
//! TODO: audio mixer (merge with [crate::devices::arranger::Arranger]?)
use crate::core::*;
// 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?
//pub const ACTIONS: [(&'static str, &'static str);2] = [
//("+/-", "Adjust"),
//("Ins/Del", "Add/remove track"),
//];
pub struct Mixer {
pub name: String,
pub tracks: Vec<MixerTrack>,
pub selected_track: usize,
pub selected_column: usize,
}
//render!(Mixer = crate::view::mixer::render);
handle!(Mixer = handle_mixer);
process!(Mixer = process);
impl Mixer {
pub fn new (name: &str) -> Usually<Self> {
let (client, _status) = Client::new(name, ClientOptions::NO_START_SERVER)?;
Ok(Self {
name: name.into(),
selected_column: 0,
selected_track: 1,
tracks: vec![
MixerTrack::new(&client, 1, "Mono 1")?,
MixerTrack::new(&client, 1, "Mono 2")?,
MixerTrack::new(&client, 2, "Stereo 1")?,
MixerTrack::new(&client, 2, "Stereo 2")?,
MixerTrack::new(&client, 2, "Stereo 3")?,
MixerTrack::new(&client, 2, "Bus 1")?,
MixerTrack::new(&client, 2, "Bus 2")?,
MixerTrack::new(&client, 2, "Mix")?,
],
})
}
}
fn process (
_: &mut Mixer,
_: &Client,
_: &ProcessScope
) -> Control {
Control::Continue
}
/// TODO: A track in the mixer. (Integrate with [crate::model::Track]?)
pub struct MixerTrack {
pub name: String,
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 MixerTrack {
pub fn new (jack: &Client, channels: u8, name: &str) -> Usually<Self> {
let mut input_ports = vec![];
let mut insert_ports = vec![];
let mut return_ports = vec![];
let mut output_ports = vec![];
for channel in 1..=channels {
input_ports.push(jack.register_port(&format!("{name} [input {channel}]"), AudioIn::default())?);
output_ports.push(jack.register_port(&format!("{name} [out {channel}]"), AudioOut::default())?);
let insert_port = jack.register_port(&format!("{name} [pre {channel}]"), AudioOut::default())?;
let return_port = jack.register_port(&format!("{name} [insert {channel}]"), AudioIn::default())?;
jack.connect_ports(&insert_port, &return_port)?;
insert_ports.push(insert_port);
return_ports.push(return_port);
}
Ok(Self {
name: name.into(),
channels,
input_ports,
pre_gain_meter: 0.0,
gain: 0.0,
post_gain_meter: 0.0,
insert_ports,
return_ports,
post_insert_meter: 0.0,
level: 0.0,
pan: 0.0,
post_fader_meter: 0.0,
route: "---".into(),
output_ports,
})
}
}
//impl<W: Write> Input<TUI<W>, bool> for Mixer {
//fn handle (&mut self, engine: &mut TUI<W>) -> Result<Option<bool>> {
//Ok(None)
//}
//
//impl<W: Write> Output<TUI<W>, [u16;2]> for Mixer {
//fn render (&self, envine: &mut TUI<W>) -> Result<Option<[u16;2]>> {
//let tracks_table = Columns::new()
//.add(titles)
//.add(input_meters)
//.add(gains)
//.add(gain_meters)
//.add(pres)
//.add(pre_meters)
//.add(levels)
//.add(pans)
//.add(pan_meters)
//.add(posts)
//.add(routes)
//Rows::new()
//.add(Columns::new()
//.add(Rows::new()
//.add("[Arrows]".bold())
//.add("Navigate"))
//.add(Rows::new()
//.add("[+/-]".bold())
//.add("Adjust"))
//.add(Rows::new()
//.add("[Ins/Del]".bold())
//.add("Add/remove track")))
//.add(tracks_table)
//.render(engine)
//}
//}
pub fn handle_mixer (state: &mut Mixer, event: &AppEvent) -> Usually<bool> {
if let AppEvent::Input(crossterm::event::Event::Key(event)) = event {
match event.code {
//KeyCode::Char('c') => {
//if event.modifiers == KeyModifiers::CONTROL {
//state.exit();
//}
//},
KeyCode::Down => {
state.selected_track = (state.selected_track + 1) % state.tracks.len();
println!("{}", state.selected_track);
return Ok(true)
},
KeyCode::Up => {
if state.selected_track == 0 {
state.selected_track = state.tracks.len() - 1;
} else {
state.selected_track = state.selected_track - 1;
}
println!("{}", state.selected_track);
return Ok(true)
},
KeyCode::Left => {
if state.selected_column == 0 {
state.selected_column = 6
} else {
state.selected_column = state.selected_column - 1;
}
return Ok(true)
},
KeyCode::Right => {
if state.selected_column == 6 {
state.selected_column = 0
} else {
state.selected_column = state.selected_column + 1;
}
return Ok(true)
},
_ => {
println!("\n{event:?}");
}
}
}
Ok(false)
}

View file

@ -11,9 +11,7 @@ extern crate clap;
extern crate jack as _jack;
extern crate crossterm;
mod core; crate::core::pubmod! { cli config control devices model view jack edn }
use crate::{core::*, model::*};
pub(crate) use tek_core::*;
/// Application entrypoint.
pub fn main () -> Usually<()> {

5
crates/tek/src/modal.rs Normal file
View file

@ -0,0 +1,5 @@
use crate::*;
/// Global modal dialog
pub static MODAL: Lazy<Arc<Mutex<Option<Box<dyn Exit>>>>> =
Lazy::new(||Arc::new(Mutex::new(None)));

View file

@ -1,406 +0,0 @@
//! Application state.
use crate::{core::*, devices::{arranger::*, sequencer::*, transport::*}};
use once_cell::sync::Lazy;
/// Global modal dialog
pub static MODAL: Lazy<Arc<Mutex<Option<Box<dyn Exit>>>>> =
Lazy::new(||Arc::new(Mutex::new(None)));
/// 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 model and view.
pub arranger: Arranger,
/// Phrase editor
pub sequencer: Sequencer,
/// Main JACK client.
pub jack: Option<JackClient>,
/// 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,
/// Paths to user directories
_xdg: Option<Arc<XdgApp>>,
/// Main audio outputs.
pub audio_outs: Vec<Arc<Port<Unowned>>>,
/// Number of frames requested by process callback
chunk_size: usize,
}
impl App {
pub fn new () -> Usually<Self> {
let xdg = Arc::new(microxdg::XdgApp::new("tek")?);
let first_run = crate::config::AppPaths::new(&xdg)?.should_create();
let jack = JackClient::Inactive(Client::new("tek", ClientOptions::NO_START_SERVER)?.0);
*MODAL.lock().unwrap() = first_run.then(||{
Exit::boxed(crate::devices::setup::SetupModal(Some(xdg.clone()), false))
});
Ok(Self {
entered: true,
section: AppFocus::default(),
transport: TransportToolbar::new(Some(jack.transport())),
arranger: Arranger::new(),
sequencer: Sequencer::new(),
jack: Some(jack),
audio_outs: vec![],
chain_mode: false,
chunk_size: 0,
midi_in: None,
midi_ins: vec![],
_xdg: Some(xdg),
})
}
}
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
});
impl App {
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)
}
}
/// 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,
}
}
}
/// A sequencer track.
#[derive(Debug)]
pub struct Track {
pub name: String,
/// Play input through output.
pub monitoring: bool,
/// Write input to sequence.
pub recording: bool,
/// Overdub input to sequence.
pub overdub: bool,
/// Map: tick -> MIDI events at tick
pub phrases: Vec<Arc<RwLock<Phrase>>>,
/// Phrase selector
pub sequence: Option<usize>,
/// Output from current sequence.
pub midi_out: Option<Port<MidiOut>>,
/// MIDI output buffer
midi_out_buf: Vec<Vec<Vec<u8>>>,
/// Device chain
pub devices: Vec<JackDevice>,
/// Device selector
pub device: usize,
/// Send all notes off
pub reset: bool, // TODO?: after Some(nframes)
/// Highlight keys on piano roll.
pub notes_in: [bool;128],
/// Highlight keys on piano roll.
pub notes_out: [bool;128],
}
impl Track {
pub fn new (name: &str) -> Usually<Self> {
Ok(Self {
name: name.to_string(),
midi_out: None,
midi_out_buf: vec![Vec::with_capacity(16);16384],
notes_in: [false;128],
notes_out: [false;128],
monitoring: false,
recording: false,
overdub: true,
sequence: None,
phrases: vec![],
devices: vec![],
device: 0,
reset: true,
})
}
fn get_device_mut (&self, i: usize) -> Option<RwLockWriteGuard<Box<dyn Device>>> {
self.devices.get(i).map(|d|d.state.write().unwrap())
}
pub fn device_mut (&self) -> Option<RwLockWriteGuard<Box<dyn Device>>> {
self.get_device_mut(self.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: &App) -> 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 fn add_device (&mut self, device: JackDevice) -> Usually<&mut JackDevice> {
self.devices.push(device);
let index = self.devices.len() - 1;
Ok(&mut self.devices[index])
}
pub fn toggle_monitor (&mut self) {
self.monitoring = !self.monitoring;
}
pub fn toggle_record (&mut self) {
self.recording = !self.recording;
}
pub fn toggle_overdub (&mut self) {
self.overdub = !self.overdub;
}
pub fn process (
&mut self,
input: Option<MidiIter>,
timebase: &Arc<Timebase>,
playing: Option<TransportState>,
started: Option<(usize, usize)>,
quant: usize,
reset: bool,
scope: &ProcessScope,
(frame0, frames): (usize, usize),
(_usec0, _usecs): (usize, usize),
period: f64,
) {
if self.midi_out.is_some() {
// Clear the section of the output buffer that we will be using
for frame in &mut self.midi_out_buf[0..frames] {
frame.clear();
}
// Emit "all notes off" at start of buffer if requested
if self.reset {
all_notes_off(&mut self.midi_out_buf);
self.reset = false;
} else if reset {
all_notes_off(&mut self.midi_out_buf);
}
}
if let (
Some(TransportState::Rolling), Some((start_frame, _)), Some(phrase)
) = (
playing, started, self.sequence.and_then(|id|self.phrases.get_mut(id))
) {
phrase.read().map(|phrase|{
if self.midi_out.is_some() {
phrase.process_out(
&mut self.midi_out_buf,
&mut self.notes_out,
timebase,
(frame0.saturating_sub(start_frame), frames, period)
);
}
}).unwrap();
let mut phrase = phrase.write().unwrap();
let length = phrase.length;
// Monitor and record input
if input.is_some() && (self.recording || self.monitoring) {
// For highlighting keys and note repeat
for (frame, event, bytes) in parse_midi_input(input.unwrap()) {
match event {
LiveEvent::Midi { message, .. } => {
if self.monitoring {
self.midi_out_buf[frame].push(bytes.to_vec())
}
if self.recording {
phrase.record_event({
let pulse = timebase.frame_to_pulse(
(frame0 + frame - start_frame) as f64
);
let quantized = (
pulse / quant as f64
).round() as usize * quant;
let looped = quantized % length;
looped
}, message);
}
match message {
MidiMessage::NoteOn { key, .. } => {
self.notes_in[key.as_int() as usize] = true;
}
MidiMessage::NoteOff { key, .. } => {
self.notes_in[key.as_int() as usize] = false;
},
_ => {}
}
},
_ => {}
}
}
}
} else if input.is_some() && self.midi_out.is_some() && self.monitoring {
for (frame, event, bytes) in parse_midi_input(input.unwrap()) {
self.process_monitor_event(frame, &event, bytes)
}
}
if let Some(out) = &mut self.midi_out {
write_midi_output(&mut out.writer(scope), &self.midi_out_buf, frames);
}
}
#[inline]
fn process_monitor_event (&mut self, frame: usize, event: &LiveEvent, bytes: &[u8]) {
match event {
LiveEvent::Midi { message, .. } => {
self.write_to_output_buffer(frame, bytes);
self.process_monitor_message(&message);
},
_ => {}
}
}
#[inline] fn write_to_output_buffer (&mut self, frame: usize, bytes: &[u8]) {
self.midi_out_buf[frame].push(bytes.to_vec());
}
#[inline]
fn process_monitor_message (&mut self, message: &MidiMessage) {
match message {
MidiMessage::NoteOn { key, .. } => {
self.notes_in[key.as_int() as usize] = true;
}
MidiMessage::NoteOff { key, .. } => {
self.notes_in[key.as_int() as usize] = false;
},
_ => {}
}
}
}
/// Define a MIDI phrase.
#[macro_export] macro_rules! phrase {
($($t:expr => $msg:expr),* $(,)?) => {{
#[allow(unused_mut)]
let mut phrase = BTreeMap::new();
$(phrase.insert($t, vec![]);)*
$(phrase.get_mut(&$t).unwrap().push($msg);)*
phrase
}}
}
macro_rules! impl_axis_common { ($A:ident $T:ty) => {
impl $A<$T> {
pub fn start_inc (&mut self) -> $T {
self.start = self.start + 1;
self.start
}
pub fn start_dec (&mut self) -> $T {
self.start = self.start.saturating_sub(1);
self.start
}
pub fn point_inc (&mut self) -> Option<$T> {
self.point = self.point.map(|p|p + 1);
self.point
}
pub fn point_dec (&mut self) -> Option<$T> {
self.point = self.point.map(|p|p.saturating_sub(1));
self.point
}
}
} }
pub struct FixedAxis<T> { pub start: T, pub point: Option<T> }
impl_axis_common!(FixedAxis u16);
impl_axis_common!(FixedAxis usize);
pub struct ScaledAxis<T> { pub start: T, pub scale: T, pub point: Option<T> }
impl_axis_common!(ScaledAxis u16);
impl_axis_common!(ScaledAxis usize);
impl<T: Copy> ScaledAxis<T> {
pub fn scale_mut (&mut self, cb: &impl Fn(T)->T) {
self.scale = cb(self.scale)
}
}

View file

@ -1,44 +0,0 @@
//! Rendering of application to display.
use crate::{render, App, core::*, devices::chain::ChainView, model::MODAL};
submod! { border split theme }
render!(App |self, buf, area| {
Split::down([
&self.transport,
&self.arranger,
&If(self.arranger.selected.is_clip(), &Split::right([
&ChainView::vertical(&self),
&self.sequencer,
]))
]).render(buf, area)?;
if let Some(ref modal) = *MODAL.lock().unwrap() {
modal.render(buf, area)?;
}
Ok(area)
});
#[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
}
}

View file

@ -1,161 +0,0 @@
use crate::core::*;
pub trait BorderStyle {
const NW: &'static str = "";
const N: &'static str = "";
const NE: &'static str = "";
const E: &'static str = "";
const SE: &'static str = "";
const S: &'static str = "";
const SW: &'static str = "";
const W: &'static str = "";
#[inline]
fn draw (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
self.draw_horizontal(buf, area, None)?;
self.draw_vertical(buf, area, None)?;
self.draw_corners(buf, area, None)?;
Ok(area)
}
#[inline]
fn draw_horizontal (&self, buf: &mut Buffer, area: Rect, style: Option<Style>) -> Usually<Rect> {
let style = style.or_else(||self.style_horizontal());
for x in area.x..(area.x+area.width).saturating_sub(1) {
self.draw_north(buf, x, area.y, style)?;
self.draw_south(buf, x, (area.y + area.height).saturating_sub(1), style)?;
}
Ok(area)
}
#[inline]
fn draw_north (&self, buf: &mut Buffer, x: u16, y: u16, style: Option<Style>) -> Usually<Rect> {
Self::N.blit(buf, x, y, style)
}
#[inline]
fn draw_south (&self, buf: &mut Buffer, x: u16, y: u16, style: Option<Style>) -> Usually<Rect> {
Self::S.blit(buf, x, y, style)
}
#[inline]
fn draw_vertical (&self, buf: &mut Buffer, area: Rect, style: Option<Style>) -> Usually<Rect> {
let style = style.or_else(||self.style_vertical());
for y in area.y..(area.y+area.height).saturating_sub(1) {
Self::W.blit(buf, area.x, y, style)?;
Self::E.blit(buf, area.x + area.width - 1, y, style)?;
}
Ok(area)
}
#[inline]
fn draw_corners (&self, buf: &mut Buffer, area: Rect, style: Option<Style>) -> Usually<Rect> {
let style = style.or_else(||self.style_corners());
Self::NW.blit(buf, area.x, area.y, style)?;
Self::NE.blit(buf, area.x + area.width - 1, area.y, style)?;
Self::SW.blit(buf, area.x, area.y + area.height - 1, style)?;
Self::SE.blit(buf, area.x + area.width - 1, area.y + area.height - 1, style)?;
Ok(area)
}
#[inline]
fn style (&self) -> Option<Style> {
None
}
#[inline]
fn style_horizontal (&self) -> Option<Style> {
self.style()
}
#[inline]
fn style_vertical (&self) -> Option<Style> {
self.style()
}
#[inline]
fn style_corners (&self) -> Option<Style> {
self.style()
}
}
macro_rules! border {
($($T:ty {
$nw:literal $n:literal $ne:literal $w:literal $e:literal $sw:literal $s:literal $se:literal
$($x:tt)*
}),+) => {
$(impl BorderStyle for $T {
const NW: &'static str = $nw;
const N: &'static str = $n;
const NE: &'static str = $ne;
const W: &'static str = $w;
const E: &'static str = $e;
const SW: &'static str = $sw;
const S: &'static str = $s;
const SE: &'static str = $se;
$($x)*
})+
}
}
pub struct Lozenge(pub Style);
pub struct LozengeV(pub Style);
pub struct LozengeDotted(pub Style);
pub struct Quarter(pub Style);
pub struct QuarterV(pub Style);
pub struct Chamfer(pub Style);
pub struct Corners(pub Style);
border! {
Lozenge {
"" "" ""
"" ""
"" "" ""
fn style (&self) -> Option<Style> {
Some(self.0)
}
},
LozengeV {
"" "" ""
"" ""
"" "" ""
fn style (&self) -> Option<Style> {
Some(self.0)
}
},
LozengeDotted {
"" "" ""
"" ""
"" "" ""
fn style (&self) -> Option<Style> {
Some(self.0)
}
},
Quarter {
"" "" "🮇"
"" "🮇"
"" "" "🮇"
fn style (&self) -> Option<Style> {
Some(self.0)
}
},
QuarterV {
"" "" "🮇"
"" "🮇"
"" "" "🮇"
fn style (&self) -> Option<Style> {
Some(self.0)
}
},
Chamfer {
"🭂" "" "🭍"
"" "🮇"
"🭓" "" "🭞"
fn style (&self) -> Option<Style> {
Some(self.0)
}
},
Corners {
"🬆" "" "🬊" // 🬴 🬸
"" ""
"🬱" "" "🬵"
fn style (&self) -> Option<Style> {
Some(self.0)
}
}
}

View file

@ -1,129 +0,0 @@
use crate::{core::*, view::*};
pub struct Layered<'a, const N: usize>(pub [&'a (dyn Render + Sync); N]);
impl<'a, const N: usize> Render for Layered<'a, N> {
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
for layer in self.0.iter() {
layer.render(buf, area)?;
}
Ok(area)
}
}
pub struct If<'a>(pub bool, pub &'a (dyn Render + Sync));
impl<'a> Render for If<'a> {
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
match self.0 {
true => self.1 as &dyn Render,
false => &() as &dyn Render
}.render(buf, area)
}
}
pub struct IfElse<'a>(pub bool, pub &'a (dyn Render + Sync), pub &'a (dyn Render + Sync));
impl<'a> Render for IfElse<'a> {
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
match self.0 {
true => self.1 as &dyn Render,
false => &() as &dyn Render
}.render(buf, area)
}
}
#[derive(Copy, Clone)]
pub enum Direction { Down, Right }
impl Direction {
pub fn split <'a, const N: usize> (&self, items: [&'a (dyn Render + Sync);N]) -> Split<'a, N> {
Split(*self, items)
}
pub fn split_focus <'a> (&self, index: usize, items: Renderables<'a>, style: Style) -> SplitFocus<'a> {
SplitFocus(*self, index, items, style)
}
pub fn is_down (&self) -> bool {
match self { Self::Down => true, _ => false }
}
pub fn is_right (&self) -> bool {
match self { Self::Right => true, _ => false }
}
}
pub struct Split<'a, const N: usize>(
pub Direction, pub [&'a (dyn Render + Sync);N]
);
impl<'a, const N: usize> Split<'a, N> {
pub fn down (items: [&'a (dyn Render + Sync);N]) -> Self {
Self(Direction::Down, items)
}
pub fn right (items: [&'a (dyn Render + Sync);N]) -> Self {
Self(Direction::Right, items)
}
pub fn render_areas (&self, buf: &mut Buffer, area: Rect) -> Usually<(Rect, Vec<Rect>)> {
let Rect { mut x, mut y, mut width, mut height } = area;
let mut areas = vec![];
for item in self.1 {
if width == 0 || height == 0 {
break
}
let result = item.render(buf, Rect { x, y, width, height })?;
match self.0 {
Direction::Down => {
y = y + result.height;
height = height.saturating_sub(result.height);
},
Direction::Right => {
x = x + result.width;
width = width.saturating_sub(result.width);
},
};
areas.push(area);
}
Ok((area, areas))
}
}
impl<'a, const N: usize> Render for Split<'a, N> {
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
Ok(self.render_areas(buf, area)?.0)
}
}
type Renderables<'a> = &'a [JackDevice];
pub struct SplitFocus<'a>(pub Direction, pub usize, pub Renderables<'a>, pub Style);
impl<'a> SplitFocus<'a> {
pub fn render_areas (&self, buf: &mut Buffer, area: Rect) -> Usually<(Rect, Vec<Rect>)> {
let Rect { mut x, mut y, mut width, mut height } = area;
let mut areas = vec![];
for item in self.2.iter() {
if width == 0 || height == 0 {
break
}
let result = item.render(buf, Rect { x, y, width, height })?;
areas.push(result);
match self.0 {
Direction::Down => {
y = y + result.height;
height = height.saturating_sub(result.height);
},
Direction::Right => {
x = x + result.width;
width = width.saturating_sub(result.width);
},
}
Lozenge(self.3).draw(buf, result)?;
}
Ok((area, areas))
}
}
impl<'a> Render for SplitFocus<'a> {
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
Ok(self.render_areas(buf, area)?.0)
}
}

View file

@ -1,71 +0,0 @@
use crate::core::*;
pub trait Theme {
const BG0: Color;
const BG1: Color;
const BG2: Color;
const BG3: Color;
const BG4: Color;
const RED: Color;
const YELLOW: Color;
const GREEN: Color;
const PLAYING: Color;
const SEPARATOR: Color;
fn bg_hier (focused: bool, entered: bool) -> Color {
if focused && entered {
Self::BG3
} else if focused {
Self::BG2
} else {
Self::BG1
}
}
fn bg_hi (focused: bool, entered: bool) -> Color {
if focused && entered {
Self::BG2
} else if focused {
Self::BG1
} else {
Self::BG0
}
}
fn bg_lo (focused: bool, entered: bool) -> Color {
if focused && entered {
Self::BG1
} else if focused {
Self::BG0
} else {
Color::Reset
}
}
fn style_hi (focused: bool, highlight: bool) -> Style {
if highlight && focused {
Style::default().yellow().not_dim()
} else if highlight {
Style::default().yellow().dim()
} else {
Style::default()
}
}
}
pub struct Nord;
impl Theme for Nord {
const BG0: Color = Color::Rgb(41, 46, 57);
const BG1: Color = Color::Rgb(46, 52, 64);
const BG2: Color = Color::Rgb(59, 66, 82);
const BG3: Color = Color::Rgb(67, 76, 94);
const BG4: Color = Color::Rgb(76, 86, 106);
const RED: Color = Color::Rgb(191, 97, 106);
const YELLOW: Color = Color::Rgb(235, 203, 139);
const GREEN: Color = Color::Rgb(163, 190, 140);
const PLAYING: Color = Color::Rgb(60, 100, 50);
const SEPARATOR: Color = Color::Rgb(0, 0, 0);
}

View file

@ -0,0 +1,8 @@
[package]
name = "tek_chain"
edition = "2021"
version = "0.1.0"
[dependencies]
tek_core = { path = "../tek_core" }
tek_jack = { path = "../tek_jack" }

View file

@ -0,0 +1 @@
# `tek_chain`

View file

View file

@ -0,0 +1,49 @@
use crate::*;
/// A sequencer track.
#[derive(Debug)]
pub struct Chain {
pub name: String,
/// Device chain
pub devices: Vec<JackDevice>,
/// Device selector
pub device: usize,
}
impl Chain {
pub fn new (name: &str) -> Usually<Self> {
Ok(Self {
name: name.to_string(),
devices: vec![],
device: 0,
})
}
fn get_device_mut (&self, i: usize) -> Option<RwLockWriteGuard<Box<dyn Device>>> {
self.devices.get(i).map(|d|d.state.write().unwrap())
}
pub fn device_mut (&self) -> Option<RwLockWriteGuard<Box<dyn Device>>> {
self.get_device_mut(self.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: &App) -> 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 fn add_device (&mut self, device: JackDevice) -> Usually<&mut JackDevice> {
self.devices.push(device);
let index = self.devices.len() - 1;
Ok(&mut self.devices[index])
}
}

View file

@ -1,7 +1,8 @@
use crate::{core::*, model::*, view::*};
use crate::*;
use tek_core::Direction;
pub struct ChainView<'a> {
pub track: Option<&'a Track>,
pub track: Option<&'a Chain>,
pub direction: Direction,
pub focused: bool,
pub entered: bool,
@ -53,3 +54,5 @@ impl<'a> Render for ChainView<'a> {
}
}
}

View file

@ -0,0 +1,8 @@
pub(crate) use tek_core::*;
pub(crate) use tek_core::ratatui::prelude::*;
pub(crate) use tek_jack::*;
pub(crate) use std::sync::RwLockWriteGuard;
submod! {
chain
chain_view
}

View file

@ -0,0 +1,14 @@
[package]
name = "tek_core"
edition = "2021"
version = "0.1.0"
[dependencies]
clap = { version = "4.5.4", features = [ "derive" ] }
crossterm = "0.27"
ratatui = { version = "0.26.3", features = [ "unstable-widget-ref", "underline-color" ] }
backtrace = "0.3.72"
microxdg = "0.1.2"
toml = "0.8.12"
better-panic = "0.3.0"
midly = "0.5"

38
crates/tek_core/README.md Normal file
View file

@ -0,0 +1,38 @@
# `tek_core`
Core "utilities" module of Tek TUI DAW.
## Exiting
The `Exit` trait defines the API for exitable entities,
such as the main app or the modal dialogs.
## Rendering output
The `Render` trait...
The `render!` macro for implementing the `Render` trait...
The `render_thread` function for starting the render thread...
The various layout and rendering helpers...
## Handling input
The `Handle` trait...
The `handle!` macro...
The `input_thread` function...
## Running an app
The `Component` trait...
The `run` function...
## Module helpers
The `submod!` macro...
The `pubmod!` macro...

View file

@ -0,0 +1,20 @@
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)
}
}
#[macro_export] macro_rules! exit {
($T:ty) => {
impl Exit for $T {
fn exited (&self) -> bool {
self.exited
}
fn exit (&mut self) {
self.exited = true
}
}
}
}

View file

@ -1,4 +1,31 @@
use crate::core::*;
use crate::*;
/// Spawn thread that listens for user input
pub fn input_thread (
exited: &Arc<AtomicBool>,
device: &Arc<RwLock<impl Handle + Send + Sync + 'static>>
) -> JoinHandle<()> {
let poll = Duration::from_millis(100);
let exited = exited.clone();
let device = device.clone();
spawn(move || loop {
// Exit if flag is set
if exited.fetch_and(true, Ordering::Relaxed) {
break
}
// Listen for events and send them to the main thread
if ::crossterm::event::poll(poll).is_ok() {
let event = ::crossterm::event::read().unwrap();
if let Event::Key(KeyEvent {
code: KeyCode::Char('c'), modifiers: KeyModifiers::CONTROL, ..
}) = event {
exited.store(true, Ordering::Relaxed);
} else if let Err(e) = device.write().unwrap().handle(&AppEvent::Input(event)) {
panic!("{e}")
}
}
})
}
/// Trait for things that handle input events.
pub trait Handle {
@ -43,8 +70,8 @@ pub enum AppEvent {
Focus,
/// Device loses focus
Blur,
/// JACK notification
Jack(JackEvent)
// /// JACK notification
// Jack(JackEvent)
}
pub type KeyHandler<T> = &'static dyn Fn(&mut T)->Usually<bool>;
@ -59,7 +86,7 @@ pub fn handle_keymap <T> (
state: &mut T, event: &AppEvent, keymap: &KeyMap<T>,
) -> Usually<bool> {
match event {
AppEvent::Input(Event::Key(event)) => {
AppEvent::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)

View file

@ -0,0 +1,90 @@
pub use ratatui;
pub use crossterm;
pub use midly;
pub(crate) use std::error::Error;
pub(crate) use std::io::{stdout};
pub(crate) use std::thread::{spawn, JoinHandle};
pub(crate) use std::time::Duration;
pub(crate) use std::sync::atomic::{Ordering, AtomicBool};
pub(crate) use std::sync::{Arc, Mutex, RwLock};
//, LockResult, RwLockReadGuard, RwLockWriteGuard};
//pub(crate) use std::path::PathBuf;
//pub(crate) use std::fs::read_dir;
//pub(crate) use std::ffi::OsString;
// Non-stdlib dependencies:
//pub(crate) use microxdg::XdgApp;
//pub(crate) use midly::{MidiMessage, live::LiveEvent, num::u7};
pub(crate) use crossterm::{ExecutableCommand};
pub(crate) use crossterm::event::{Event, KeyEvent, KeyCode, KeyModifiers};
use better_panic::{Settings, Verbosity};
use crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen,
enable_raw_mode, disable_raw_mode
};
/// Define and reexport submodules.
#[macro_export] macro_rules! submod {
($($name:ident)*) => { $(mod $name; pub use self::$name::*;)* };
}
/// Define and reexport public modules.
#[macro_export] macro_rules! pubmod {
($($name:ident)*) => { $(pub mod $name;)* };
}
submod! {
exit render handle
}
/// Standard result type.
pub type Usually<T> = Result<T, Box<dyn Error>>;
/// A UI component.
pub trait Component: Render + Handle + Sync {
/// Perform type erasure for collecting heterogeneous components.
fn boxed (self) -> Box<dyn Component> where Self: Sized + 'static {
Box::new(self)
}
}
/// Anything that implements `Render` + `Handle` can be used as a UI component.
impl<T: Render + Handle + Sync> Component for T {}
/// Run the main loop.
pub fn run <T> (state: Arc<RwLock<T>>) -> Usually<Arc<RwLock<T>>>
where T: Render + Handle + Send + Sync + Sized + 'static
{
let exited = Arc::new(AtomicBool::new(false));
let _input_thread = input_thread(&exited, &state);
terminal_setup()?;
panic_hook_setup();
let main_thread = render_thread(&exited, &state)?;
main_thread.join().expect("main thread failed");
terminal_teardown()?;
Ok(state)
}
/// Set up panic hook
pub fn panic_hook_setup () {
let better_panic_handler = Settings::auto().verbosity(Verbosity::Full).create_panic_handler();
std::panic::set_hook(Box::new(move |info: &std::panic::PanicInfo|{
stdout().execute(LeaveAlternateScreen).unwrap();
disable_raw_mode().unwrap();
better_panic_handler(info);
}));
}
/// Set up terminal
pub fn terminal_setup () -> Usually<()> {
stdout().execute(EnterAlternateScreen)?;
enable_raw_mode()?;
Ok(())
}
/// Cleanup
pub fn terminal_teardown () -> Usually<()> {
stdout().execute(LeaveAlternateScreen)?;
disable_raw_mode()?;
Ok(())
}

View file

@ -0,0 +1,592 @@
//! Rendering of application to display.
use crate::*;
pub(crate) use ratatui::prelude::CrosstermBackend;
pub(crate) use ratatui::style::{Stylize, Style, Color};
pub(crate) use ratatui::layout::Rect;
pub(crate) use ratatui::buffer::{Buffer, Cell};
use ratatui::widgets::WidgetRef;
/// Main thread render loop
pub fn render_thread (
exited: &Arc<AtomicBool>,
device: &Arc<RwLock<impl Render + Send + Sync + 'static>>
) -> Usually<JoinHandle<()>> {
let exited = exited.clone();
let device = device.clone();
let mut terminal = ratatui::Terminal::new(CrosstermBackend::new(stdout()))?;
let sleep = Duration::from_millis(20);
Ok(spawn(move || loop {
if let Ok(device) = device.try_read() {
terminal.draw(|frame|{
let area = frame.size();
let buffer = frame.buffer_mut();
device
.render(buffer, area)
.expect("Failed to render content");
})
.expect("Failed to render frame");
}
if exited.fetch_and(true, Ordering::Relaxed) {
break
}
std::thread::sleep(sleep);
}))
}
pub fn make_dim (buf: &mut Buffer) {
for cell in buf.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 center_box (area: Rect, w: u16, h: u16) -> Rect {
let width = w.min(area.width * 3 / 5);
let height = h.min(area.width * 3 / 5);
let x = area.x + (area.width - width) / 2;
let y = area.y + (area.height - height) / 2;
Rect { x, y, width, height }
}
pub fn buffer_update (
buf: &mut Buffer, area: Rect, callback: &impl Fn(&mut Cell, u16, u16)
) {
for row in 0..area.height {
let y = area.y + row;
for col in 0..area.width {
let x = area.x + col;
if x < buf.area.width && y < buf.area.height {
callback(buf.get_mut(x, y), col, row);
}
}
}
}
pub fn fill_fg (buf: &mut Buffer, area: Rect, color: Color) {
buffer_update(buf, area, &|cell,_,_|{cell.set_fg(color);})
}
pub fn fill_bg (buf: &mut Buffer, area: Rect, color: Color) {
buffer_update(buf, area, &|cell,_,_|{cell.set_bg(color);})
}
pub fn to_fill_bg (color: Color) -> impl Render {
move |buf: &mut Buffer, area: Rect|{
fill_bg(buf, area, color);
Ok(area)
}
}
pub fn fill_char (buf: &mut Buffer, area: Rect, c: char) {
buffer_update(buf, area, &|cell,_,_|{cell.set_char(c);})
}
pub fn half_block (lower: bool, upper: bool) -> Option<char> {
match (lower, upper) {
(true, true) => Some('█'),
(true, false) => Some('▄'),
(false, true) => Some('▀'),
_ => None
}
}
pub trait Blit {
// Render something to X, Y coordinates in a buffer, ignoring width/height.
fn blit (&self, buf: &mut Buffer, x: u16, y: u16, style: Option<Style>) -> Usually<Rect>;
}
impl<T: AsRef<str>> Blit for T {
fn blit (&self, buf: &mut Buffer, x: u16, y: u16, style: Option<Style>) -> Usually<Rect> {
if x < buf.area.width && y < buf.area.height {
buf.set_string(x, y, self.as_ref(), style.unwrap_or(Style::default()));
}
Ok(Rect { x, y, width: self.as_ref().len() as u16, height: 1 })
}
}
/// Trait for things that render to the display.
pub trait Render: Send {
// Render something to an area of the buffer.
// Returns area used by component.
// This is insufficient but for the most basic dynamic layout algorithms.
fn render (&self, _b: &mut Buffer, _a: Rect) -> Usually<Rect> {
Ok(Rect { x: 0, y: 0, width: 0, height: 0 })
}
}
/// Implement the `Render` trait.
#[macro_export] macro_rules! render {
($T:ty) => {
impl Render for $T {}
};
($T:ty |$self:ident, $buf:ident, $area:ident|$block:expr) => {
impl Render for $T {
fn render (&$self, $buf: &mut Buffer, $area: Rect) -> Usually<Rect> {
$block
}
}
};
($T:ty = $render:path) => {
impl Render for $T {
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
$render(self, buf, area)
}
}
}
}
impl Render for () {
fn render (&self, _: &mut Buffer, a: Rect) -> Usually<Rect> {
Ok(Rect { x: a.x, y: a.y, width: 0, height: 0 })
}
}
impl<T: Fn(&mut Buffer, Rect) -> Usually<Rect> + Send> Render for T {
fn render (&self, b: &mut Buffer, a: Rect) -> Usually<Rect> {
(*self)(b, a)
}
}
impl<T: Render> Render for Arc<Mutex<T>> {
fn render (&self, b: &mut Buffer, a: Rect) -> Usually<Rect> {
self.lock().unwrap().render(b, a)
}
}
impl<T: Render + Sync> Render for Arc<RwLock<T>> {
fn render (&self, b: &mut Buffer, a: Rect) -> Usually<Rect> {
self.read().unwrap().render(b, a)
}
}
impl WidgetRef for &dyn Render {
fn render_ref (&self, area: Rect, buf: &mut Buffer) {
Render::render(*self, buf, area).expect("Failed to render device.");
}
}
impl WidgetRef for dyn Render {
fn render_ref (&self, area: Rect, buf: &mut Buffer) {
Render::render(self, buf, area).expect("Failed to render device.");
}
}
#[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 struct Layered<'a, const N: usize>(pub [&'a (dyn Render + Sync); N]);
impl<'a, const N: usize> Render for Layered<'a, N> {
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
for layer in self.0.iter() {
layer.render(buf, area)?;
}
Ok(area)
}
}
pub struct If<'a>(pub bool, pub &'a (dyn Render + Sync));
impl<'a> Render for If<'a> {
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
match self.0 {
true => self.1 as &dyn Render,
false => &() as &dyn Render
}.render(buf, area)
}
}
pub struct IfElse<'a>(pub bool, pub &'a (dyn Render + Sync), pub &'a (dyn Render + Sync));
impl<'a> Render for IfElse<'a> {
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
match self.0 {
true => self.1 as &dyn Render,
false => &() as &dyn Render
}.render(buf, area)
}
}
#[derive(Copy, Clone)]
pub enum Direction { Down, Right }
impl Direction {
pub fn split <'a, const N: usize> (&self, items: [&'a (dyn Render + Sync);N]) -> Split<'a, N> {
Split(*self, items)
}
pub fn split_focus <'a> (&self, index: usize, items: Renderables<'a>, style: Style) -> SplitFocus<'a> {
SplitFocus(*self, index, items, style)
}
pub fn is_down (&self) -> bool {
match self { Self::Down => true, _ => false }
}
pub fn is_right (&self) -> bool {
match self { Self::Right => true, _ => false }
}
}
pub struct Split<'a, const N: usize>(
pub Direction, pub [&'a (dyn Render + Sync);N]
);
impl<'a, const N: usize> Split<'a, N> {
pub fn down (items: [&'a (dyn Render + Sync);N]) -> Self {
Self(Direction::Down, items)
}
pub fn right (items: [&'a (dyn Render + Sync);N]) -> Self {
Self(Direction::Right, items)
}
pub fn render_areas (&self, buf: &mut Buffer, area: Rect) -> Usually<(Rect, Vec<Rect>)> {
let Rect { mut x, mut y, mut width, mut height } = area;
let mut areas = vec![];
for item in self.1 {
if width == 0 || height == 0 {
break
}
let result = item.render(buf, Rect { x, y, width, height })?;
match self.0 {
Direction::Down => {
y = y + result.height;
height = height.saturating_sub(result.height);
},
Direction::Right => {
x = x + result.width;
width = width.saturating_sub(result.width);
},
};
areas.push(area);
}
Ok((area, areas))
}
}
impl<'a, const N: usize> Render for Split<'a, N> {
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
Ok(self.render_areas(buf, area)?.0)
}
}
type Renderables<'a> = &'a [&'a dyn Component];
pub struct SplitFocus<'a>(pub Direction, pub usize, pub Renderables<'a>, pub Style);
impl<'a> SplitFocus<'a> {
pub fn render_areas (&self, buf: &mut Buffer, area: Rect) -> Usually<(Rect, Vec<Rect>)> {
let Rect { mut x, mut y, mut width, mut height } = area;
let mut areas = vec![];
for item in self.2.iter() {
if width == 0 || height == 0 {
break
}
let result = item.render(buf, Rect { x, y, width, height })?;
areas.push(result);
match self.0 {
Direction::Down => {
y = y + result.height;
height = height.saturating_sub(result.height);
},
Direction::Right => {
x = x + result.width;
width = width.saturating_sub(result.width);
},
}
Lozenge(self.3).draw(buf, result)?;
}
Ok((area, areas))
}
}
impl<'a> Render for SplitFocus<'a> {
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
Ok(self.render_areas(buf, area)?.0)
}
}
pub trait Theme {
const BG0: Color;
const BG1: Color;
const BG2: Color;
const BG3: Color;
const BG4: Color;
const RED: Color;
const YELLOW: Color;
const GREEN: Color;
const PLAYING: Color;
const SEPARATOR: Color;
fn bg_hier (focused: bool, entered: bool) -> Color {
if focused && entered {
Self::BG3
} else if focused {
Self::BG2
} else {
Self::BG1
}
}
fn bg_hi (focused: bool, entered: bool) -> Color {
if focused && entered {
Self::BG2
} else if focused {
Self::BG1
} else {
Self::BG0
}
}
fn bg_lo (focused: bool, entered: bool) -> Color {
if focused && entered {
Self::BG1
} else if focused {
Self::BG0
} else {
Color::Reset
}
}
fn style_hi (focused: bool, highlight: bool) -> Style {
if highlight && focused {
Style::default().yellow().not_dim()
} else if highlight {
Style::default().yellow().dim()
} else {
Style::default()
}
}
}
pub struct Nord;
impl Theme for Nord {
const BG0: Color = Color::Rgb(41, 46, 57);
const BG1: Color = Color::Rgb(46, 52, 64);
const BG2: Color = Color::Rgb(59, 66, 82);
const BG3: Color = Color::Rgb(67, 76, 94);
const BG4: Color = Color::Rgb(76, 86, 106);
const RED: Color = Color::Rgb(191, 97, 106);
const YELLOW: Color = Color::Rgb(235, 203, 139);
const GREEN: Color = Color::Rgb(163, 190, 140);
const PLAYING: Color = Color::Rgb(60, 100, 50);
const SEPARATOR: Color = Color::Rgb(0, 0, 0);
}
pub trait BorderStyle {
const NW: &'static str = "";
const N: &'static str = "";
const NE: &'static str = "";
const E: &'static str = "";
const SE: &'static str = "";
const S: &'static str = "";
const SW: &'static str = "";
const W: &'static str = "";
#[inline]
fn draw (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
self.draw_horizontal(buf, area, None)?;
self.draw_vertical(buf, area, None)?;
self.draw_corners(buf, area, None)?;
Ok(area)
}
#[inline]
fn draw_horizontal (&self, buf: &mut Buffer, area: Rect, style: Option<Style>) -> Usually<Rect> {
let style = style.or_else(||self.style_horizontal());
for x in area.x..(area.x+area.width).saturating_sub(1) {
self.draw_north(buf, x, area.y, style)?;
self.draw_south(buf, x, (area.y + area.height).saturating_sub(1), style)?;
}
Ok(area)
}
#[inline]
fn draw_north (&self, buf: &mut Buffer, x: u16, y: u16, style: Option<Style>) -> Usually<Rect> {
Self::N.blit(buf, x, y, style)
}
#[inline]
fn draw_south (&self, buf: &mut Buffer, x: u16, y: u16, style: Option<Style>) -> Usually<Rect> {
Self::S.blit(buf, x, y, style)
}
#[inline]
fn draw_vertical (&self, buf: &mut Buffer, area: Rect, style: Option<Style>) -> Usually<Rect> {
let style = style.or_else(||self.style_vertical());
for y in area.y..(area.y+area.height).saturating_sub(1) {
Self::W.blit(buf, area.x, y, style)?;
Self::E.blit(buf, area.x + area.width - 1, y, style)?;
}
Ok(area)
}
#[inline]
fn draw_corners (&self, buf: &mut Buffer, area: Rect, style: Option<Style>) -> Usually<Rect> {
let style = style.or_else(||self.style_corners());
Self::NW.blit(buf, area.x, area.y, style)?;
Self::NE.blit(buf, area.x + area.width - 1, area.y, style)?;
Self::SW.blit(buf, area.x, area.y + area.height - 1, style)?;
Self::SE.blit(buf, area.x + area.width - 1, area.y + area.height - 1, style)?;
Ok(area)
}
#[inline]
fn style (&self) -> Option<Style> {
None
}
#[inline]
fn style_horizontal (&self) -> Option<Style> {
self.style()
}
#[inline]
fn style_vertical (&self) -> Option<Style> {
self.style()
}
#[inline]
fn style_corners (&self) -> Option<Style> {
self.style()
}
}
macro_rules! border {
($($T:ty {
$nw:literal $n:literal $ne:literal $w:literal $e:literal $sw:literal $s:literal $se:literal
$($x:tt)*
}),+) => {
$(impl BorderStyle for $T {
const NW: &'static str = $nw;
const N: &'static str = $n;
const NE: &'static str = $ne;
const W: &'static str = $w;
const E: &'static str = $e;
const SW: &'static str = $sw;
const S: &'static str = $s;
const SE: &'static str = $se;
$($x)*
})+
}
}
pub struct Lozenge(pub Style);
pub struct LozengeV(pub Style);
pub struct LozengeDotted(pub Style);
pub struct Quarter(pub Style);
pub struct QuarterV(pub Style);
pub struct Chamfer(pub Style);
pub struct Corners(pub Style);
border! {
Lozenge {
"" "" ""
"" ""
"" "" ""
fn style (&self) -> Option<Style> {
Some(self.0)
}
},
LozengeV {
"" "" ""
"" ""
"" "" ""
fn style (&self) -> Option<Style> {
Some(self.0)
}
},
LozengeDotted {
"" "" ""
"" ""
"" "" ""
fn style (&self) -> Option<Style> {
Some(self.0)
}
},
Quarter {
"" "" "🮇"
"" "🮇"
"" "" "🮇"
fn style (&self) -> Option<Style> {
Some(self.0)
}
},
QuarterV {
"" "" "🮇"
"" "🮇"
"" "" "🮇"
fn style (&self) -> Option<Style> {
Some(self.0)
}
},
Chamfer {
"🭂" "" "🭍"
"" "🮇"
"🭓" "" "🭞"
fn style (&self) -> Option<Style> {
Some(self.0)
}
},
Corners {
"🬆" "" "🬊" // 🬴 🬸
"" ""
"🬱" "" "🬵"
fn style (&self) -> Option<Style> {
Some(self.0)
}
}
}
macro_rules! impl_axis_common { ($A:ident $T:ty) => {
impl $A<$T> {
pub fn start_inc (&mut self) -> $T {
self.start = self.start + 1;
self.start
}
pub fn start_dec (&mut self) -> $T {
self.start = self.start.saturating_sub(1);
self.start
}
pub fn point_inc (&mut self) -> Option<$T> {
self.point = self.point.map(|p|p + 1);
self.point
}
pub fn point_dec (&mut self) -> Option<$T> {
self.point = self.point.map(|p|p.saturating_sub(1));
self.point
}
}
} }
pub struct FixedAxis<T> { pub start: T, pub point: Option<T> }
impl_axis_common!(FixedAxis u16);
impl_axis_common!(FixedAxis usize);
pub struct ScaledAxis<T> { pub start: T, pub scale: T, pub point: Option<T> }
impl_axis_common!(ScaledAxis u16);
impl_axis_common!(ScaledAxis usize);
impl<T: Copy> ScaledAxis<T> {
pub fn scale_mut (&mut self, cb: &impl Fn(T)->T) {
self.scale = cb(self.scale)
}
}

View file

@ -0,0 +1,8 @@
[package]
name = "tek_jack"
edition = "2021"
version = "0.1.0"
[dependencies]
tek_core = { path = "../tek_core" }
jack = "0.10"

View file

@ -0,0 +1,3 @@
# `tek_jack`
This crate interfaces with the JACK Audio Connection Kit API.

View file

@ -1,6 +1,7 @@
//! Wrap JACK-enabled [Device]s.
use super::*;
use tek_core::ratatui::prelude::{Buffer, Rect};
/// A [Device] bound to a JACK client and a set of ports.
pub struct JackDevice {

View file

@ -5,10 +5,10 @@ use super::*;
/// Notification handler used by the [Jack] factory
/// when constructing [JackDevice]s.
pub type DynamicNotifications =
Notifications<Box<dyn Fn(AppEvent) + Send + Sync>>;
Notifications<Box<dyn Fn(JackEvent) + Send + Sync>>;
#[derive(Debug)]
/// Event enum for JACK events in [AppEvent].
/// Event enum for JACK events.
pub enum JackEvent {
ThreadInit,
Shutdown(ClientStatus, String),
@ -22,51 +22,51 @@ pub enum JackEvent {
XRun,
}
/// Generic notification handler that emits [AppEvent]::Jack([JackEvent])s.
pub struct Notifications<T: Fn(AppEvent) + Send>(pub T);
/// Generic notification handler that emits [JackEvent]
pub struct Notifications<T: Fn(JackEvent) + Send>(pub T);
impl<T: Fn(AppEvent) + Send> NotificationHandler for Notifications<T> {
impl<T: Fn(JackEvent) + Send> NotificationHandler for Notifications<T> {
fn thread_init (&self, _: &Client) {
self.0(AppEvent::Jack(JackEvent::ThreadInit));
self.0(JackEvent::ThreadInit);
}
fn shutdown (&mut self, status: ClientStatus, reason: &str) {
self.0(AppEvent::Jack(JackEvent::Shutdown(status, reason.into())));
self.0(JackEvent::Shutdown(status, reason.into()));
}
fn freewheel (&mut self, _: &Client, enabled: bool) {
self.0(AppEvent::Jack(JackEvent::Freewheel(enabled)));
self.0(JackEvent::Freewheel(enabled));
}
fn sample_rate (&mut self, _: &Client, frames: Frames) -> Control {
self.0(AppEvent::Jack(JackEvent::SampleRate(frames)));
self.0(JackEvent::SampleRate(frames));
Control::Quit
}
fn client_registration (&mut self, _: &Client, name: &str, reg: bool) {
self.0(AppEvent::Jack(JackEvent::ClientRegistration(name.into(), reg)));
self.0(JackEvent::ClientRegistration(name.into(), reg));
}
fn port_registration (&mut self, _: &Client, id: PortId, reg: bool) {
self.0(AppEvent::Jack(JackEvent::PortRegistration(id, reg)));
self.0(JackEvent::PortRegistration(id, reg));
}
fn port_rename (&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control {
self.0(AppEvent::Jack(JackEvent::PortRename(id, old.into(), new.into())));
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(AppEvent::Jack(JackEvent::PortsConnected(a, b, are)));
self.0(JackEvent::PortsConnected(a, b, are));
}
fn graph_reorder (&mut self, _: &Client) -> Control {
self.0(AppEvent::Jack(JackEvent::GraphReorder));
self.0(JackEvent::GraphReorder);
Control::Continue
}
fn xrun (&mut self, _: &Client) -> Control {
self.0(AppEvent::Jack(JackEvent::XRun));
self.0(JackEvent::XRun);
Control::Continue
}
}

View file

@ -50,7 +50,7 @@ impl Jack {
// FIXME: this deadlocks
//state.lock().unwrap().handle(&event).unwrap();
}
}) as Box<dyn Fn(AppEvent) + Send + Sync>),
}) as Box<dyn Fn(JackEvent) + Send + Sync>),
ClosureProcessHandler::new(Box::new({
let state = state.clone();
move|c: &Client, s: &ProcessScope|{
@ -69,23 +69,6 @@ impl Jack {
state,
})
}
pub fn ports_from_lv2 (self, plugin: &::livi::Plugin) -> Self {
let counts = plugin.port_counts();
let mut jack = self;
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}"));
}
jack
}
pub fn audio_in (mut self, name: &str) -> Self {
self.audio_ins.push(name.to_string());
self

View file

@ -1,10 +1,15 @@
//! Audio engine.
use crate::core::*;
pub use jack;
use std::sync::{Arc, RwLock, LockResult, RwLockReadGuard, RwLockWriteGuard};
use std::collections::BTreeMap;
submod!( device event factory ports );
pub(crate) use ::_jack::{
pub(crate) use tek_core::*;
pub(crate) use tek_core::ratatui::prelude::{Buffer, Rect};
pub(crate) use ::jack::{
AsyncClient,
AudioIn,
AudioOut,
@ -13,23 +18,40 @@ pub(crate) use ::_jack::{
ClientStatus,
ClosureProcessHandler,
Control,
CycleTimes,
//CycleTimes,
Frames,
MidiIn,
MidiIter,
//MidiIter,
MidiOut,
NotificationHandler,
Port,
PortFlags,
//PortFlags,
PortId,
PortSpec,
ProcessScope,
RawMidi,
//RawMidi,
Transport,
TransportState,
//TransportState,
Unowned
};
/// A UI component that may be associated with a JACK client by the `Jack` factory.
pub trait Device: Render + Handle + Process + Send + Sync {
/// Perform type erasure for collecting heterogeneous devices.
fn boxed (self) -> Box<dyn Device> where Self: Sized + 'static {
Box::new(self)
}
}
/// All things that implement the required traits can be treated as `Device`.
impl<T: Render + Handle + Process + Send + Sync> Device for T {}
impl Render for Box<dyn Device> {
fn render (&self, b: &mut Buffer, a: Rect) -> Usually<Rect> {
(**self).render(b, a)
}
}
/// Wraps [Client] or [DynamicAsyncClient] in place.
pub enum JackClient {
Inactive(Client),
@ -63,7 +85,7 @@ impl JackClient {
Self::Active(_) => self,
Self::Inactive(client) => Self::Active(client.activate_async(
Notifications(Box::new(move|_|{/*TODO*/})
as Box<dyn Fn(AppEvent) + Send + Sync>),
as Box<dyn Fn(JackEvent) + Send + Sync>),
ClosureProcessHandler::new(Box::new({
let state = state.clone();
move|c: &Client, s: &ProcessScope|process(&state, c, s)
@ -123,7 +145,7 @@ pub fn jack_run <T: Sync> (name: &str, app: &Arc<RwLock<T>>) -> Usually<DynamicA
// FIXME: this deadlocks
//app.lock().unwrap().handle(&event).unwrap();
}
}) as Box<dyn Fn(AppEvent) + Send + Sync>),
}) as Box<dyn Fn(JackEvent) + Send + Sync>),
ClosureProcessHandler::new(Box::new({
let app = app.clone();
move|c: &Client, s: &ProcessScope|{
@ -133,3 +155,4 @@ pub fn jack_run <T: Sync> (name: &str, app: &Arc<RwLock<T>>) -> Usually<DynamicA
}) as BoxedProcessHandler)
)?)
}

View file

@ -0,0 +1,9 @@
[package]
name = "tek_mixer"
edition = "2021"
version = "0.1.0"
[dependencies]
tek_core = { path = "../tek_core" }
tek_jack = { path = "../tek_jack" }
tek_chain = { path = "../tek_chain" }

View file

@ -0,0 +1,7 @@
# `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?

View file

View file

@ -0,0 +1,8 @@
pub(crate) use tek_core::*;
pub(crate) use tek_core::ratatui::prelude::*;
pub(crate) use tek_core::crossterm::event::{KeyCode, KeyModifiers};
pub(crate) use tek_jack::{*, jack::*};
submod! {
mixer
mixer_track
}

View file

@ -0,0 +1,94 @@
use crate::*;
//pub const ACTIONS: [(&'static str, &'static str);2] = [
//("+/-", "Adjust"),
//("Ins/Del", "Add/remove track"),
//];
pub struct Mixer {
pub name: String,
pub tracks: Vec<MixerTrack>,
pub selected_track: usize,
pub selected_column: usize,
}
//render!(Mixer = crate::view::mixer::render);
handle!(Mixer = handle_mixer);
process!(Mixer = process);
impl Mixer {
pub fn new (name: &str) -> Usually<Self> {
let (client, _status) = Client::new(name, ClientOptions::NO_START_SERVER)?;
Ok(Self {
name: name.into(),
selected_column: 0,
selected_track: 1,
tracks: vec![
MixerTrack::new(&client, 1, "Mono 1")?,
MixerTrack::new(&client, 1, "Mono 2")?,
MixerTrack::new(&client, 2, "Stereo 1")?,
MixerTrack::new(&client, 2, "Stereo 2")?,
MixerTrack::new(&client, 2, "Stereo 3")?,
MixerTrack::new(&client, 2, "Bus 1")?,
MixerTrack::new(&client, 2, "Bus 2")?,
MixerTrack::new(&client, 2, "Mix")?,
],
})
}
}
fn process (
_: &mut Mixer,
_: &Client,
_: &ProcessScope
) -> Control {
Control::Continue
}
pub fn handle_mixer (state: &mut Mixer, event: &AppEvent) -> Usually<bool> {
if let AppEvent::Input(crossterm::event::Event::Key(event)) = event {
match event.code {
//KeyCode::Char('c') => {
//if event.modifiers == KeyModifiers::CONTROL {
//state.exit();
//}
//},
KeyCode::Down => {
state.selected_track = (state.selected_track + 1) % state.tracks.len();
println!("{}", state.selected_track);
return Ok(true)
},
KeyCode::Up => {
if state.selected_track == 0 {
state.selected_track = state.tracks.len() - 1;
} else {
state.selected_track = state.selected_track - 1;
}
println!("{}", state.selected_track);
return Ok(true)
},
KeyCode::Left => {
if state.selected_column == 0 {
state.selected_column = 6
} else {
state.selected_column = state.selected_column - 1;
}
return Ok(true)
},
KeyCode::Right => {
if state.selected_column == 6 {
state.selected_column = 0
} else {
state.selected_column = state.selected_column + 1;
}
return Ok(true)
},
_ => {
println!("\n{event:?}");
}
}
}
Ok(false)
}

View file

@ -0,0 +1,91 @@
use crate::*;
/// TODO: A track in the mixer. (Integrate with [crate::model::Track]?)
pub struct MixerTrack {
pub name: String,
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 MixerTrack {
pub fn new (jack: &Client, channels: u8, name: &str) -> Usually<Self> {
let mut input_ports = vec![];
let mut insert_ports = vec![];
let mut return_ports = vec![];
let mut output_ports = vec![];
for channel in 1..=channels {
input_ports.push(jack.register_port(&format!("{name} [input {channel}]"), AudioIn::default())?);
output_ports.push(jack.register_port(&format!("{name} [out {channel}]"), AudioOut::default())?);
let insert_port = jack.register_port(&format!("{name} [pre {channel}]"), AudioOut::default())?;
let return_port = jack.register_port(&format!("{name} [insert {channel}]"), AudioIn::default())?;
jack.connect_ports(&insert_port, &return_port)?;
insert_ports.push(insert_port);
return_ports.push(return_port);
}
Ok(Self {
name: name.into(),
channels,
input_ports,
pre_gain_meter: 0.0,
gain: 0.0,
post_gain_meter: 0.0,
insert_ports,
return_ports,
post_insert_meter: 0.0,
level: 0.0,
pan: 0.0,
post_fader_meter: 0.0,
route: "---".into(),
output_ports,
})
}
}
//impl<W: Write> Input<TUI<W>, bool> for Mixer {
//fn handle (&mut self, engine: &mut TUI<W>) -> Result<Option<bool>> {
//Ok(None)
//}
//
//impl<W: Write> Output<TUI<W>, [u16;2]> for Mixer {
//fn render (&self, envine: &mut TUI<W>) -> Result<Option<[u16;2]>> {
//let tracks_table = Columns::new()
//.add(titles)
//.add(input_meters)
//.add(gains)
//.add(gain_meters)
//.add(pres)
//.add(pre_meters)
//.add(levels)
//.add(pans)
//.add(pan_meters)
//.add(posts)
//.add(routes)
//Rows::new()
//.add(Columns::new()
//.add(Rows::new()
//.add("[Arrows]".bold())
//.add("Navigate"))
//.add(Rows::new()
//.add("[+/-]".bold())
//.add("Adjust"))
//.add(Rows::new()
//.add("[Ins/Del]".bold())
//.add("Add/remove track")))
//.add(tracks_table)
//.render(engine)
//}
//}

View file

@ -0,0 +1,13 @@
[package]
name = "tek_plugin"
edition = "2021"
version = "0.1.0"
[dependencies]
tek_core = { path = "../tek_core" }
tek_jack = { path = "../tek_jack" }
livi = "0.7.4"
winit = { version = "0.30.4", features = [ "x11" ] }
suil-rs = { path = "../suil" }
vst = "0.4.0"
#vst3 = "0.1.0"

View file

@ -0,0 +1,4 @@
# `tek_plugin`
This crate allows plugins to be loaded.

View file

View file

@ -0,0 +1,18 @@
//! Plugin (currently LV2 only; TODO other formats)
pub(crate) use tek_core::*;
pub(crate) use tek_core::ratatui::prelude::*;
pub(crate) use tek_core::crossterm::event::{KeyCode, KeyModifiers};
pub(crate) use tek_jack::*;
pub(crate) use tek_jack::jack::*;
use std::sync::Arc;
use std::sync::Mutex;
submod! {
plugin
lv2
lv2_gui
vst2
vst3
}

View file

@ -1,4 +1,5 @@
use super::*;
use ::livi::{
World,
Instance,
@ -8,6 +9,7 @@ use ::livi::{
Port,
event::LV2AtomSequence,
};
use ::winit::{
application::ApplicationHandler,
event::WindowEvent,
@ -15,13 +17,15 @@ use ::winit::{
window::{Window, WindowId},
platform::x11::EventLoopBuilderExtX11
};
use ::suil_rs::{self};
use std::thread::{spawn, JoinHandle};
impl Plugin {
pub fn lv2 (name: &str, path: &str) -> Usually<JackDevice> {
let plugin = LV2Plugin::new(path)?;
Jack::new(name)?
.ports_from_lv2(&plugin.plugin)
jack_from_lv2(name, &plugin.plugin)?
.run(|ports|Box::new(Self {
name: name.into(),
path: Some(String::from(path)),
@ -121,3 +125,21 @@ impl ApplicationHandler for LV2PluginUI {
fn lv2_ui_instantiate (kind: &str) {
//let host = Suil
}
pub 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)
}

View file

View file

@ -1,8 +1,4 @@
//! Plugin (currently LV2 only; TODO other formats)
use crate::core::*;
submod! { lv2 }
use crate::*;
pub fn handle_plugin (state: &mut Plugin, event: &AppEvent) -> Usually<bool> {
handle_keymap(state, event, KEYMAP_PLUGIN)
@ -206,18 +202,6 @@ fn draw_header (state: &Plugin, buf: &mut Buffer, x: u16, y: u16, w: u16) -> Usu
Ok(Rect { x, y, width: w, height: 1 })
}
impl ::vst::host::Host for Plugin {}
fn set_vst_plugin (host: &Arc<Mutex<Plugin>>, _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()?
})
}
//pub struct LV2PluginUI {
//write: (),
//controller: (),

View file

@ -0,0 +1,13 @@
use crate::*;
impl ::vst::host::Host for Plugin {}
fn set_vst_plugin (host: &Arc<Mutex<Plugin>>, _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()?
})
}

View file

@ -0,0 +1 @@
use crate::*;

View file

@ -0,0 +1,10 @@
[package]
name = "tek_sampler"
edition = "2021"
version = "0.1.0"
[dependencies]
tek_core = { path = "../tek_core" }
tek_jack = { path = "../tek_jack" }
symphonia = { version = "0.5.4", features = [ "all" ] }
wavers = "1.4.3"

View file

@ -0,0 +1,4 @@
# `tek_sampler`
This crate implements a sampler device which plays audio files
in response to MIDI notes.

View file

View file

@ -0,0 +1,20 @@
//! Sampler (currently 16bit WAVs at system rate; TODO convert/resample)
pub(crate) use tek_core::*;
pub(crate) use tek_core::ratatui::prelude::*;
pub(crate) use tek_core::crossterm::event::{KeyCode, KeyModifiers};
pub(crate) use tek_core::midly::{num::u7, live::LiveEvent, MidiMessage};
pub(crate) use tek_jack::{*, jack::*};
pub(crate) use std::collections::BTreeMap;
pub(crate) use std::sync::{Arc, Mutex, RwLock};
pub(crate) use std::path::PathBuf;
pub(crate) use std::ffi::OsString;
pub(crate) use std::fs::read_dir;
submod! {
sampler
sample
sample_add
voice
}

View file

@ -1,4 +1,3 @@
use crate::core::*;
use super::*;
/// A sound sample.
@ -52,3 +51,4 @@ pub fn read_sample_data (src: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
}
Ok((end, data))
}

View file

@ -1,4 +1,3 @@
use crate::{core::*, view::*};
use super::*;
use std::fs::File;
@ -277,3 +276,4 @@ impl Sample {
Ok(sample)
}
}

View file

@ -1,8 +1,4 @@
//! Sampler (currently 16bit WAVs at system rate; TODO convert/resample)
use crate::{core::*, view::*, model::MODAL};
submod! { add_sample sample voice }
use crate::*;
/// The sampler plugin plays sounds.
pub struct Sampler {
@ -14,6 +10,7 @@ pub struct Sampler {
pub voices: Arc<RwLock<Vec<Voice>>>,
pub ports: JackPorts,
pub buffer: Vec<Vec<f32>>,
pub modal: Arc<Mutex<Option<Box<dyn Exit + Send>>>>,
pub output_gain: f32
}
@ -83,13 +80,13 @@ pub const KEYMAP_SAMPLER: &'static [KeyBinding<Sampler>] = keymap!(Sampler {
}],
[Char('a'), NONE, "/sampler/add", "add a new sample", |state: &mut Sampler| {
let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![])));
*MODAL.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &state.voices)?));
*state.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &state.voices)?));
state.unmapped.push(sample);
Ok(true)
}],
[Char('r'), NONE, "/sampler/replace", "replace selected sample", |state: &mut Sampler| {
if let Some(sample) = state.sample() {
*MODAL.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &state.voices)?));
*state.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &state.voices)?));
}
Ok(true)
}],
@ -121,6 +118,7 @@ impl Sampler {
ports,
buffer: vec![vec![0.0;16384];2],
output_gain: 0.5,
modal: Default::default()
}))
}

View file

@ -1,4 +1,3 @@
use crate::core::*;
use super::*;
/// A currently playing instance of a sample.

View file

@ -0,0 +1,9 @@
[package]
name = "tek_sequencer"
edition = "2021"
version = "0.1.0"
[dependencies]
tek_core = { path = "../tek_core" }
tek_jack = { path = "../tek_jack" }
tek_timer = { path = "../tek_timer" }

View file

@ -0,0 +1,3 @@
# `tek_sequencer`
This crate implements a MIDI sequencer and arranger with clip launching.

View file

@ -1,61 +1,62 @@
//! Clip launcher and arrangement editor.
use crate::{core::*, model::*};
use crate::*;
use self::arr_focus::ArrangerFocus;
pub use self::arr_scene::Scene;
submod! { arr_draw_h arr_draw_v arr_focus arr_phrase arr_scene arr_track }
/// Key bindings for arranger section.
pub const KEYMAP_ARRANGER: &'static [KeyBinding<App>] = keymap!(App {
[Char('`'), NONE, "arranger_mode_switch", "switch the display mode", |app: &mut App| {
app.arranger.mode.to_next();
pub const KEYMAP_ARRANGER: &'static [KeyBinding<Arranger>] = keymap!(Arranger {
[Char('`'), NONE, "arranger_mode_switch", "switch the display mode", |app: &mut Arranger| {
app.mode.to_next();
Ok(true)
}],
[Up, NONE, "arranger_cursor_up", "move cursor up", |app: &mut App| {
match app.arranger.mode {
ArrangerViewMode::Horizontal => app.arranger.track_prev(),
_ => app.arranger.scene_prev(),
[Up, NONE, "arranger_cursor_up", "move cursor up", |app: &mut Arranger| {
match app.mode {
ArrangerViewMode::Horizontal => app.track_prev(),
_ => app.scene_prev(),
};
app.sequencer.show(app.arranger.phrase())?;
app.show_phrase()?;
Ok(true)
}],
[Down, NONE, "arranger_cursor_down", "move cursor down", |app: &mut App| {
match app.arranger.mode {
ArrangerViewMode::Horizontal => app.arranger.track_next(),
_ => app.arranger.scene_next(),
[Down, NONE, "arranger_cursor_down", "move cursor down", |app: &mut Arranger| {
match app.mode {
ArrangerViewMode::Horizontal => app.track_next(),
_ => app.scene_next(),
};
app.sequencer.show(app.arranger.phrase())?;
app.show_phrase()?;
Ok(true)
}],
[Left, NONE, "arranger_cursor_left", "move cursor left", |app: &mut App| {
match app.arranger.mode {
ArrangerViewMode::Horizontal => app.arranger.scene_prev(),
_ => app.arranger.track_prev(),
[Left, NONE, "arranger_cursor_left", "move cursor left", |app: &mut Arranger| {
match app.mode {
ArrangerViewMode::Horizontal => app.scene_prev(),
_ => app.track_prev(),
};
app.sequencer.show(app.arranger.phrase())?;
app.show_phrase()?;
Ok(true)
}],
[Right, NONE, "arranger_cursor_right", "move cursor right", |app: &mut App| {
match app.arranger.mode {
ArrangerViewMode::Horizontal => app.arranger.scene_next(),
_ => app.arranger.track_next(),
[Right, NONE, "arranger_cursor_right", "move cursor right", |app: &mut Arranger| {
match app.mode {
ArrangerViewMode::Horizontal => app.scene_next(),
_ => app.track_next(),
};
app.sequencer.show(app.arranger.phrase())?;
app.show_phrase()?;
Ok(true)
}],
[Char('.'), NONE, "arranger_increment", "set next clip at cursor", |app: &mut App| {
app.arranger.phrase_next();
app.sequencer.phrase = app.arranger.phrase().map(Clone::clone);
[Char('.'), NONE, "arranger_increment", "set next clip at cursor", |app: &mut Arranger| {
app.phrase_next();
app.sequencer.phrase = app.phrase().map(Clone::clone);
Ok(true)
}],
[Char(','), NONE, "arranger_decrement", "set previous clip at cursor", |app: &mut App| {
app.arranger.phrase_prev();
app.sequencer.phrase = app.arranger.phrase().map(Clone::clone);
[Char(','), NONE, "arranger_decrement", "set previous clip at cursor", |app: &mut Arranger| {
app.phrase_prev();
app.sequencer.phrase = app.phrase().map(Clone::clone);
Ok(true)
}],
[Enter, NONE, "arranger_activate", "activate item at cursor", |app: &mut App| {
app.arranger.activate();
[Enter, NONE, "arranger_activate", "activate item at cursor", |app: &mut Arranger| {
app.activate();
Ok(true)
}],
});
@ -74,6 +75,8 @@ pub struct Arranger {
pub focused: bool,
pub entered: bool,
pub fixed_height: bool,
pub sequencer: Sequencer,
}
/// Display mode of arranger
@ -104,6 +107,7 @@ impl Arranger {
entered: true,
focused: true,
fixed_height: false,
sequencer: Sequencer::new(),
}
}
@ -122,6 +126,11 @@ impl Arranger {
_ => {}
}
}
fn show_phrase (&mut self) -> Usually<()> {
let phrase = self.phrase();
self.sequencer.show(phrase)
}
}
render!(Arranger |self, buf, area| match self.mode {
@ -132,3 +141,4 @@ render!(Arranger |self, buf, area| match self.mode {
ArrangerViewMode::VerticalCompact =>
self::arr_draw_v::draw_compact(self, buf, area),
});

View file

@ -1,3 +1,5 @@
use crate::*;
#[derive(PartialEq)]
/// Represents the current user selection in the arranger
pub enum ArrangerFocus {
@ -93,3 +95,4 @@ impl ArrangerFocus {
}
}
}

View file

@ -1,4 +1,5 @@
use crate::{core::*, devices::sequencer::Phrase};
use crate::*;
use super::Arranger;
/// Phrase management methods

View file

@ -1,4 +1,5 @@
use crate::{core::*, model::Track};
use crate::*;
use super::Arranger;
/// A collection of phrases to play on each track.

View file

@ -1,4 +1,5 @@
use crate::{core::*, model::Track};
use crate::*;
use super::Arranger;
/// Track management methods

View file

@ -1,4 +1,4 @@
use crate::{core::*, view::*};
use crate::*;
use super::{Arranger, arr_track::*};
pub fn draw (state: &Arranger, buf: &mut Buffer, mut area: Rect) -> Usually<Rect> {
@ -197,3 +197,4 @@ fn track_scenes_column <'a> (state: &'a Arranger) -> impl Render + 'a {
Ok(Rect { x, y, height, width: x2 })
}
}

View file

@ -1,4 +1,4 @@
use crate::{core::*, view::*};
use crate::*;
use super::{
Arranger,
arr_focus::ArrangerFocus,

View file

@ -0,0 +1,3 @@
fn main () {
panic!()
}

View file

@ -0,0 +1,53 @@
//! Phrase editor.
pub(crate) use tek_core::*;
pub(crate) use tek_core::ratatui::prelude::*;
pub(crate) use tek_core::crossterm::event::{KeyCode, KeyModifiers};
pub(crate) use tek_core::midly::{num::u7, live::LiveEvent, MidiMessage};
pub(crate) use tek_jack::{*, jack::*};
pub(crate) use tek_timer::*;
pub(crate) use std::sync::{Arc, RwLock};
submod! { midi phrase arranger sequencer sequencer_track }
/// Key bindings for phrase editor.
pub const KEYMAP_SEQUENCER: &'static [KeyBinding<Sequencer>] = keymap!(Sequencer {
[Up, NONE, "seq_cursor_up", "move cursor up", |sequencer: &mut Sequencer| {
match sequencer.entered {
true => { sequencer.note_axis.point_dec(); },
false => { sequencer.note_axis.start_dec(); },
}
Ok(true)
}],
[Down, NONE, "seq_cursor_down", "move cursor down", |sequencer: &mut Sequencer| {
match sequencer.entered {
true => { sequencer.note_axis.point_inc(); },
false => { sequencer.note_axis.start_inc(); },
}
Ok(true)
}],
[Left, NONE, "seq_cursor_left", "move cursor up", |sequencer: &mut Sequencer| {
match sequencer.entered {
true => { sequencer.time_axis.point_dec(); },
false => { sequencer.time_axis.start_dec(); },
}
Ok(true)
}],
[Right, NONE, "seq_cursor_right", "move cursor up", |sequencer: &mut Sequencer| {
match sequencer.entered {
true => { sequencer.time_axis.point_inc(); },
false => { sequencer.time_axis.start_inc(); },
}
Ok(true)
}],
[Char('`'), NONE, "seq_mode_switch", "switch the display mode", |sequencer: &mut Sequencer| {
sequencer.mode = !sequencer.mode;
Ok(true)
}],
/*
[Char('a'), NONE, "note_add", "Add note", note_add],
[Char('z'), NONE, "note_del", "Delete note", note_del],
[CapsLock, NONE, "advance", "Toggle auto advance", nop],
[Char('w'), NONE, "rest", "Advance by note duration", nop],
*/
});

View file

@ -0,0 +1,36 @@
use crate::*;
use tek_jack::jack::*;
/// MIDI message serialized to bytes
pub type MIDIMessage = Vec<u8>;
/// Collection of serialized MIDI messages
pub type MIDIChunk = [Vec<MIDIMessage>];
/// Add "all notes off" to the start of a buffer.
pub fn all_notes_off (output: &mut MIDIChunk) {
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
)))
}
/// Write to JACK port from output buffer (containing notes from sequence and/or monitor)
pub fn write_midi_output (writer: &mut MidiWriter, output: &MIDIChunk, frames: usize) {
for time in 0..frames {
for event in output[time].iter() {
writer.write(&RawMidi { time: time as u32, bytes: &event })
.expect(&format!("{event:?}"));
}
}
}

View file

@ -0,0 +1,93 @@
use crate::*;
/// Define a MIDI phrase.
#[macro_export] macro_rules! phrase {
($($t:expr => $msg:expr),* $(,)?) => {{
#[allow(unused_mut)]
let mut phrase = BTreeMap::new();
$(phrase.insert($t, vec![]);)*
$(phrase.get_mut(&$t).unwrap().push($msg);)*
phrase
}}
}
pub type PhraseData = Vec<Vec<MidiMessage>>;
#[derive(Debug)]
/// A MIDI sequence.
pub struct Phrase {
pub name: String,
pub length: usize,
pub notes: PhraseData,
pub looped: Option<(usize, usize)>,
/// All notes are displayed with minimum length
pub percussive: bool
}
impl Default for Phrase {
fn default () -> Self {
Self::new("", 0, None)
}
}
impl Phrase {
pub fn new (name: &str, length: usize, notes: Option<PhraseData>) -> Self {
Self {
name: name.to_string(),
length,
notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]),
looped: Some((0, length)),
percussive: true,
}
}
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() {
match event {
MidiMessage::NoteOn {key,..} => {
if *key == k {
return true
}
}
_ => {}
}
}
}
return false
}
/// Write a chunk of MIDI events to an output port.
pub fn process_out (
&self,
output: &mut MIDIChunk,
notes_on: &mut [bool;128],
timebase: &Arc<Timebase>,
(frame0, frames, _): (usize, usize, f64),
) {
let mut buf = Vec::with_capacity(8);
for (time, tick) in Ticks(timebase.pulse_per_frame()).between_frames(
frame0, frame0 + frames
) {
let tick = tick % self.length;
for message in self.notes[tick].iter() {
buf.clear();
let channel = 0.into();
let message = *message;
LiveEvent::Midi { channel, message }.write(&mut buf).unwrap();
output[time as usize].push(buf.clone());
match message {
MidiMessage::NoteOn { key, .. } => notes_on[key.as_int() as usize] = true,
MidiMessage::NoteOff { key, .. } => notes_on[key.as_int() as usize] = false,
_ => {}
}
}
}
}
}

View file

@ -1,129 +1,4 @@
//! Phrase editor.
use crate::{core::*, model::*, view::*};
/// Key bindings for phrase editor.
pub const KEYMAP_SEQUENCER: &'static [KeyBinding<App>] = keymap!(App {
[Up, NONE, "seq_cursor_up", "move cursor up", |app: &mut App| {
match app.sequencer.entered {
true => { app.sequencer.note_axis.point_dec(); },
false => { app.sequencer.note_axis.start_dec(); },
}
Ok(true)
}],
[Down, NONE, "seq_cursor_down", "move cursor down", |app: &mut App| {
match app.sequencer.entered {
true => { app.sequencer.note_axis.point_inc(); },
false => { app.sequencer.note_axis.start_inc(); },
}
Ok(true)
}],
[Left, NONE, "seq_cursor_left", "move cursor up", |app: &mut App| {
match app.sequencer.entered {
true => { app.sequencer.time_axis.point_dec(); },
false => { app.sequencer.time_axis.start_dec(); },
}
Ok(true)
}],
[Right, NONE, "seq_cursor_right", "move cursor up", |app: &mut App| {
match app.sequencer.entered {
true => { app.sequencer.time_axis.point_inc(); },
false => { app.sequencer.time_axis.start_inc(); },
}
Ok(true)
}],
[Char('`'), NONE, "seq_mode_switch", "switch the display mode", |app: &mut App| {
app.sequencer.mode = !app.sequencer.mode;
Ok(true)
}],
/*
[Char('a'), NONE, "note_add", "Add note", note_add],
[Char('z'), NONE, "note_del", "Delete note", note_del],
[CapsLock, NONE, "advance", "Toggle auto advance", nop],
[Char('w'), NONE, "rest", "Advance by note duration", nop],
*/
});
pub type PhraseData = Vec<Vec<MidiMessage>>;
#[derive(Debug)]
/// A MIDI sequence.
pub struct Phrase {
pub name: String,
pub length: usize,
pub notes: PhraseData,
pub looped: Option<(usize, usize)>,
/// Immediate note-offs in view
pub percussive: bool
}
impl Default for Phrase {
fn default () -> Self {
Self::new("", 0, None)
}
}
impl Phrase {
pub fn new (name: &str, length: usize, notes: Option<PhraseData>) -> Self {
Self {
name: name.to_string(),
length,
notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]),
looped: Some((0, length)),
percussive: true,
}
}
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() {
match event {
MidiMessage::NoteOn {key,..} => {
if *key == k {
return true
}
}
_ => {}
}
}
}
return false
}
/// Write a chunk of MIDI events to an output port.
pub fn process_out (
&self,
output: &mut MIDIChunk,
notes_on: &mut [bool;128],
timebase: &Arc<Timebase>,
(frame0, frames, _): (usize, usize, f64),
) {
let mut buf = Vec::with_capacity(8);
for (time, tick) in Ticks(timebase.pulse_per_frame()).between_frames(
frame0, frame0 + frames
) {
let tick = tick % self.length;
for message in self.notes[tick].iter() {
buf.clear();
let channel = 0.into();
let message = *message;
LiveEvent::Midi { channel, message }.write(&mut buf).unwrap();
output[time as usize].push(buf.clone());
match message {
MidiMessage::NoteOn { key, .. } => notes_on[key.as_int() as usize] = true,
MidiMessage::NoteOff { key, .. } => notes_on[key.as_int() as usize] = false,
_ => {}
}
}
}
}
}
use crate::*;
/// Phrase editor.
pub struct Sequencer {
@ -141,8 +16,8 @@ pub struct Sequencer {
pub now: usize,
pub ppq: usize,
pub note_axis: FixedAxis<u16>,
pub time_axis: ScaledAxis<u16>,
pub note_axis: FixedAxis<usize>,
pub time_axis: ScaledAxis<usize>,
}
@ -225,7 +100,7 @@ impl Sequencer {
}
}
const H_KEYS_OFFSET: u16 = 5;
const H_KEYS_OFFSET: usize = 5;
fn horizontal_draw (&self, buf: &mut Buffer, area: Rect) -> Usually<()> {
self.horizontal_keys(buf, area)?;
@ -243,14 +118,14 @@ impl Sequencer {
return Ok(area)
}
let area = Rect {
x: area.x + Self::H_KEYS_OFFSET,
x: area.x + Self::H_KEYS_OFFSET as u16,
y: area.y + 1,
width: area.width - Self::H_KEYS_OFFSET,
width: area.width - Self::H_KEYS_OFFSET as u16,
height: area.height - 2
};
buffer_update(buf, area, &move |cell, x, y|{
let src_x = ((x + self.time_axis.start) * self.time_axis.scale) as usize;
let src_y = (y + self.note_axis.start) as usize;
let src_x = ((x as usize + self.time_axis.start) * self.time_axis.scale) as usize;
let src_y = (y as usize + self.note_axis.start) as usize;
if src_x < self.buffer.width && src_y < self.buffer.height - 1 {
let src = self.buffer.get(src_x, self.buffer.height - src_y);
src.map(|src|{
@ -273,7 +148,7 @@ impl Sequencer {
height: area.height - 2
};
buffer_update(buf, area, &|cell, x, y|{
let y = y + self.note_axis.start;
let y = y + self.note_axis.start as u16;
if x < self.keys.area.width && y < self.keys.area.height {
*cell = self.keys.get(x, y).clone()
}
@ -290,7 +165,7 @@ impl Sequencer {
fn horizontal_cursor (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
if let (Some(time), Some(note)) = (self.time_axis.point, self.note_axis.point) {
let x = area.x + Self::H_KEYS_OFFSET + time as u16;
let x = area.x + Self::H_KEYS_OFFSET as u16 + time as u16;
let y = area.y + 1 + note as u16 / 2;
let c = if note % 2 == 0 { "" } else { "" };
c.blit(buf, x, y, self.style_focus())
@ -305,11 +180,13 @@ impl Sequencer {
let phrase = phrase.read().unwrap();
let (time0, time_z, now) = (self.time_axis.start, self.time_axis.scale, self.now % phrase.length);
let Rect { x, width, .. } = area;
for x in x+Self::H_KEYS_OFFSET..x+width {
let step = (time0 + (x-Self::H_KEYS_OFFSET)) * time_z;
let next_step = (time0 + (x-Self::H_KEYS_OFFSET) + 1) * time_z;
let x2 = x as usize + Self::H_KEYS_OFFSET;
let x3 = x as usize + width as usize;
for x in x2..x3 {
let step = (time0 + x2) * time_z;
let next_step = (time0 + x2 + 1) * time_z;
let style = Self::style_timer_step(now, step as usize, next_step as usize);
"-".blit(buf, x, area.y, Some(style))?;
"-".blit(buf, x as u16, area.y, Some(style))?;
}
return Ok(Rect { x: area.x, y: area.y, width: area.width, height: 1 })
}
@ -413,14 +290,16 @@ fn fill_seq_fg (buf: &mut BigBuffer, phrase: &Phrase) -> Usually<()> {
break
}
if let Some(notes) = phrase.notes.get(x as usize) {
for note in notes {
if phrase.percussive {
if phrase.percussive {
for note in notes {
match note {
MidiMessage::NoteOn { key, .. } =>
notes_on[key.as_int() as usize] = true,
_ => {}
}
} else {
}
} else {
for note in notes {
match note {
MidiMessage::NoteOn { key, .. } =>
notes_on[key.as_int() as usize] = true,

View file

@ -0,0 +1,227 @@
use crate::*;
use tek_core::Direction;
/// A sequencer track.
#[derive(Debug)]
pub struct SequencerTrack {
pub name: String,
/// Play input through output.
pub monitoring: bool,
/// Write input to sequence.
pub recording: bool,
/// Overdub input to sequence.
pub overdub: bool,
/// Map: tick -> MIDI events at tick
pub phrases: Vec<Arc<RwLock<Phrase>>>,
/// Phrase selector
pub sequence: Option<usize>,
/// Output from current sequence.
pub midi_out: Option<Port<MidiOut>>,
/// MIDI output buffer
midi_out_buf: Vec<Vec<Vec<u8>>>,
/// Send all notes off
pub reset: bool, // TODO?: after Some(nframes)
/// Highlight keys on piano roll.
pub notes_in: [bool;128],
/// Highlight keys on piano roll.
pub notes_out: [bool;128],
}
impl SequencerTrack {
pub fn new (name: &str) -> Usually<Self> {
Ok(Self {
name: name.to_string(),
monitoring: false,
recording: false,
overdub: true,
phrases: vec![],
sequence: None,
midi_out: None,
midi_out_buf: vec![Vec::with_capacity(16);16384],
reset: true,
notes_in: [false;128],
notes_out: [false;128],
})
}
pub fn toggle_monitor (&mut self) {
self.monitoring = !self.monitoring;
}
pub fn toggle_record (&mut self) {
self.recording = !self.recording;
}
pub fn toggle_overdub (&mut self) {
self.overdub = !self.overdub;
}
pub fn process (
&mut self,
input: Option<MidiIter>,
timebase: &Arc<Timebase>,
playing: Option<TransportState>,
started: Option<(usize, usize)>,
quant: usize,
reset: bool,
scope: &ProcessScope,
(frame0, frames): (usize, usize),
(_usec0, _usecs): (usize, usize),
period: f64,
) {
if self.midi_out.is_some() {
// Clear the section of the output buffer that we will be using
for frame in &mut self.midi_out_buf[0..frames] {
frame.clear();
}
// Emit "all notes off" at start of buffer if requested
if self.reset {
all_notes_off(&mut self.midi_out_buf);
self.reset = false;
} else if reset {
all_notes_off(&mut self.midi_out_buf);
}
}
if let (
Some(TransportState::Rolling), Some((start_frame, _)), Some(phrase)
) = (
playing, started, self.sequence.and_then(|id|self.phrases.get_mut(id))
) {
phrase.read().map(|phrase|{
if self.midi_out.is_some() {
phrase.process_out(
&mut self.midi_out_buf,
&mut self.notes_out,
timebase,
(frame0.saturating_sub(start_frame), frames, period)
);
}
}).unwrap();
let mut phrase = phrase.write().unwrap();
let length = phrase.length;
// Monitor and record input
if input.is_some() && (self.recording || self.monitoring) {
// For highlighting keys and note repeat
for (frame, event, bytes) in parse_midi_input(input.unwrap()) {
match event {
LiveEvent::Midi { message, .. } => {
if self.monitoring {
self.midi_out_buf[frame].push(bytes.to_vec())
}
if self.recording {
phrase.record_event({
let pulse = timebase.frame_to_pulse(
(frame0 + frame - start_frame) as f64
);
let quantized = (
pulse / quant as f64
).round() as usize * quant;
let looped = quantized % length;
looped
}, message);
}
match message {
MidiMessage::NoteOn { key, .. } => {
self.notes_in[key.as_int() as usize] = true;
}
MidiMessage::NoteOff { key, .. } => {
self.notes_in[key.as_int() as usize] = false;
},
_ => {}
}
},
_ => {}
}
}
}
} else if input.is_some() && self.midi_out.is_some() && self.monitoring {
for (frame, event, bytes) in parse_midi_input(input.unwrap()) {
self.process_monitor_event(frame, &event, bytes)
}
}
if let Some(out) = &mut self.midi_out {
write_midi_output(&mut out.writer(scope), &self.midi_out_buf, frames);
}
}
#[inline]
fn process_monitor_event (&mut self, frame: usize, event: &LiveEvent, bytes: &[u8]) {
match event {
LiveEvent::Midi { message, .. } => {
self.write_to_output_buffer(frame, bytes);
self.process_monitor_message(&message);
},
_ => {}
}
}
#[inline] fn write_to_output_buffer (&mut self, frame: usize, bytes: &[u8]) {
self.midi_out_buf[frame].push(bytes.to_vec());
}
#[inline]
fn process_monitor_message (&mut self, message: &MidiMessage) {
match message {
MidiMessage::NoteOn { key, .. } => {
self.notes_in[key.as_int() as usize] = true;
}
MidiMessage::NoteOff { key, .. } => {
self.notes_in[key.as_int() as usize] = false;
},
_ => {}
}
}
}
pub struct ChainView<'a> {
pub track: Option<&'a Track>,
pub direction: Direction,
pub focused: bool,
pub entered: bool,
}
impl<'a> ChainView<'a> {
pub fn horizontal (app: &'a App) -> Self {
Self::new(app, Direction::Right)
}
pub fn vertical (app: &'a App) -> Self {
Self::new(app, Direction::Down)
}
pub fn new (app: &'a App, direction: Direction) -> Self {
Self {
direction,
entered: app.entered,
focused: app.section == AppFocus::Chain,
track: app.arranger.track()
}
}
}
impl<'a> Render for ChainView<'a> {
fn render (&self, buf: &mut Buffer, mut area: Rect) -> Usually<Rect> {
if let Some(track) = self.track {
match self.direction {
Direction::Down => area.width = area.width.min(40),
Direction::Right => area.width = area.width.min(10),
}
fill_bg(buf, area, Nord::bg_lo(self.focused, self.entered));
let (area, areas) = self.direction
.split_focus(0, track.devices.as_slice(), if self.focused {
Style::default().green().dim()
} else {
Style::default().dim()
})
.render_areas(buf, area)?;
if self.focused && self.entered {
Corners(Style::default().green().not_dim()).draw(buf, areas[0])?;
}
Ok(area)
} else {
let Rect { x, y, width, height } = area;
let label = "No track selected";
let x = x + (width - label.len() as u16) / 2;
let y = y + height / 2;
label.blit(buf, x, y, Some(Style::default().dim().bold()))?;
Ok(area)
}
}
}

View file

@ -0,0 +1,9 @@
[package]
name = "tek_timer"
edition = "2021"
version = "0.1.0"
[dependencies]
tek_core = { path = "../tek_core" }
tek_jack = { path = "../tek_jack" }
atomic_float = "1.0.0"

View file

@ -0,0 +1,3 @@
# `tek_timer`
This crate implements time sync and JACK transport control.

View file

@ -0,0 +1,5 @@
/// Application entrypoint.
pub fn main () -> Usually<()> {
run(Arc::new(RwLock::new(TransportToolbar::standalone())))?;
Ok(())
}

View file

@ -0,0 +1,72 @@
pub(crate) use tek_core::*;
pub(crate) use tek_core::ratatui::prelude::*;
pub(crate) use tek_core::crossterm::event::{KeyCode, KeyModifiers};
pub(crate) use tek_jack::jack::*;
pub(crate) use std::sync::{Arc, atomic::Ordering};
pub(crate) use atomic_float::AtomicF64;
submod! {
timebase
ticks
transport
transport_focus
}
/// (pulses, name)
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 (ppq: usize) -> usize {
for i in 1..=16 {
let length = NOTE_DURATIONS[16-i].0;
if length < ppq {
return length
}
}
ppq
}
/// Returns the next longer length
pub fn next_note_length (ppq: usize) -> usize {
for (length, _) in &NOTE_DURATIONS {
if *length > ppq {
return *length
}
}
ppq
}
pub fn ppq_to_name (ppq: usize) -> &'static str {
for (length, name) in &NOTE_DURATIONS {
if *length == ppq {
return name
}
}
""
}

View file

@ -0,0 +1,50 @@
use crate::*;
/// Defines frames per tick.
pub struct Ticks(pub f64);
impl Ticks {
/// Iterate over ticks between start and end.
pub fn between_frames (&self, start: usize, end: usize) -> TicksIterator {
TicksIterator(self.0, start, start, end)
}
}
/// Iterator that emits subsequent ticks within a range.
pub struct TicksIterator(f64, usize, usize, usize);
impl Iterator for TicksIterator {
type Item = (usize, usize);
fn next (&mut self) -> Option<Self::Item> {
loop {
if self.1 > self.3 {
return None
}
let fpt = self.0;
let frame = self.1 as f64;
let start = self.2;
let end = self.3;
self.1 = self.1 + 1;
//println!("{fpt} {frame} {start} {end}");
let jitter = frame.rem_euclid(fpt); // ramps
let next_jitter = (frame + 1.0).rem_euclid(fpt);
if jitter > next_jitter { // at crossing:
let time = (frame as usize) % (end as usize-start as usize);
let tick = (frame / fpt) as usize;
return Some((time, tick))
}
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_frames_to_ticks () {
let ticks = Ticks(12.3).between_frames(0, 100).collect::<Vec<_>>();
println!("{ticks:?}");
}
}

View file

@ -1,5 +1,5 @@
use crate::core::*;
use atomic_float::AtomicF64;
use crate::*;
#[derive(Debug)]
/// Keeps track of global time units.
pub struct Timebase {
@ -101,52 +101,3 @@ impl Timebase {
}
}
/// Defines frames per tick.
pub struct Ticks(pub f64);
impl Ticks {
/// Iterate over ticks between start and end.
pub fn between_frames (&self, start: usize, end: usize) -> TicksIterator {
TicksIterator(self.0, start, start, end)
}
}
/// Iterator that emits subsequent ticks within a range.
pub struct TicksIterator(f64, usize, usize, usize);
impl Iterator for TicksIterator {
type Item = (usize, usize);
fn next (&mut self) -> Option<Self::Item> {
loop {
if self.1 > self.3 {
return None
}
let fpt = self.0;
let frame = self.1 as f64;
let start = self.2;
let end = self.3;
self.1 = self.1 + 1;
//println!("{fpt} {frame} {start} {end}");
let jitter = frame.rem_euclid(fpt); // ramps
let next_jitter = (frame + 1.0).rem_euclid(fpt);
if jitter > next_jitter { // at crossing:
let time = (frame as usize) % (end as usize-start as usize);
let tick = (frame / fpt) as usize;
return Some((time, tick))
}
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_frames_to_ticks () {
let ticks = Ticks(12.3).between_frames(0, 100).collect::<Vec<_>>();
println!("{ticks:?}");
}
}

View file

@ -1,67 +1,4 @@
//! Transport controller.
use crate::{core::*, view::*, model::App};
/// Key bindings for transport toolbar.
pub const KEYMAP_TRANSPORT: &'static [KeyBinding<App>] = keymap!(App {
[Left, NONE, "transport_prev", "select previous control", |app: &mut App| Ok({
app.transport.selected.prev();
true
})],
[Right, NONE, "transport_next", "select next control", |app: &mut App| Ok({
app.transport.selected.next();
true
})],
[Char('.'), NONE, "transport_increment", "increment value at cursor", |app: &mut App| {
match app.transport.selected {
TransportFocus::BPM => {
app.transport.timebase.bpm.fetch_add(1.0, Ordering::Relaxed);
},
TransportFocus::Quant => {
app.transport.quant = next_note_length(app.transport.quant)
},
TransportFocus::Sync => {
app.transport.sync = next_note_length(app.transport.sync)
},
};
Ok(true)
}],
[Char(','), NONE, "transport_decrement", "decrement value at cursor", |app: &mut App| {
match app.transport.selected {
TransportFocus::BPM => {
app.transport.timebase.bpm.fetch_sub(1.0, Ordering::Relaxed);
},
TransportFocus::Quant => {
app.transport.quant = prev_note_length(app.transport.quant);
},
TransportFocus::Sync => {
app.transport.sync = prev_note_length(app.transport.sync);
},
};
Ok(true)
}],
});
#[derive(PartialEq)]
/// Which section of the transport is focused
pub enum TransportFocus { BPM, Quant, Sync }
impl TransportFocus {
pub fn prev (&mut self) {
*self = match self {
Self::BPM => Self::Sync,
Self::Quant => Self::BPM,
Self::Sync => Self::Quant,
}
}
pub fn next (&mut self) {
*self = match self {
Self::BPM => Self::Quant,
Self::Quant => Self::Sync,
Self::Sync => Self::BPM,
}
}
}
use crate::*;
/// Stores and displays time-related state.
pub struct TransportToolbar {
@ -75,9 +12,9 @@ pub struct TransportToolbar {
/// JACK transport handle.
transport: Option<Transport>,
/// Quantization factor
pub quant: u16,
pub quant: usize,
/// Global sync quant
pub sync: u16,
pub sync: usize,
/// Current transport state
pub playing: Option<TransportState>,
/// Current position according to transport
@ -87,6 +24,9 @@ pub struct TransportToolbar {
}
impl TransportToolbar {
pub fn standalone () -> Self {
Self::new(None)
}
pub fn new (transport: Option<Transport>) -> Self {
let timebase = Arc::new(Timebase::default());
Self {
@ -98,7 +38,7 @@ impl TransportToolbar {
playing: Some(TransportState::Stopped),
started: None,
quant: 24,
sync: timebase.ppq() as u16 * 4,
sync: timebase.ppq() as usize * 4,
transport,
timebase,
}
@ -215,7 +155,7 @@ render!(TransportToolbar |self, buf, area| {
// Quantization
&|buf: &mut Buffer, Rect { x, y, .. }: Rect|{
"QUANT".blit(buf, x, y, Some(not_dim))?;
let width = ppq_to_name(*quant as u16).blit(buf, x, y + 1, Some(not_dim_bold))?.width;
let width = ppq_to_name(*quant as usize).blit(buf, x, y + 1, Some(not_dim_bold))?.width;
let area = Rect { x, y, width: (width + 2).max(10), height: 2 };
if self.focused && self.entered && self.selected == TransportFocus::Quant {
corners.draw(buf, Rect { x: area.x - 1, ..area })?;
@ -226,7 +166,7 @@ render!(TransportToolbar |self, buf, area| {
// Clip launch sync
&|buf: &mut Buffer, Rect { x, y, .. }: Rect|{
"SYNC".blit(buf, x, y, Some(not_dim))?;
let width = ppq_to_name(*sync as u16).blit(buf, x, y + 1, Some(not_dim_bold))?.width;
let width = ppq_to_name(*sync as usize).blit(buf, x, y + 1, Some(not_dim_bold))?.width;
let area = Rect { x, y, width: (width + 2).max(10), height: 2 };
if self.focused && self.entered && self.selected == TransportFocus::Sync {
corners.draw(buf, Rect { x: area.x - 1, ..area })?;
@ -246,3 +186,49 @@ render!(TransportToolbar |self, buf, area| {
]).render(buf, area)
});
/// Key bindings for transport toolbar.
pub const KEYMAP_TRANSPORT: &'static [KeyBinding<TransportToolbar>] = keymap!(TransportToolbar {
[Left, NONE, "transport_prev", "select previous control", |transport: &mut TransportToolbar| Ok({
transport.selected.prev();
true
})],
[Right, NONE, "transport_next", "select next control", |transport: &mut TransportToolbar| Ok({
transport.selected.next();
true
})],
[Char('.'), NONE, "transport_increment", "increment value at cursor", |transport: &mut TransportToolbar| {
match transport.selected {
TransportFocus::BPM => {
transport.timebase.bpm.fetch_add(1.0, Ordering::Relaxed);
},
TransportFocus::Quant => {
transport.quant = next_note_length(transport.quant)
},
TransportFocus::Sync => {
transport.sync = next_note_length(transport.sync)
},
};
Ok(true)
}],
[Char(','), NONE, "transport_decrement", "decrement value at cursor", |transport: &mut TransportToolbar| {
match transport.selected {
TransportFocus::BPM => {
transport.timebase.bpm.fetch_sub(1.0, Ordering::Relaxed);
},
TransportFocus::Quant => {
transport.quant = prev_note_length(transport.quant);
},
TransportFocus::Sync => {
transport.sync = prev_note_length(transport.sync);
},
};
Ok(true)
}],
});
handle!{
TransportToolbar |self, e| {
handle_keymap(self, e, KEYMAP_TRANSPORT)
}
}

View file

@ -0,0 +1,23 @@
use crate::*;
#[derive(PartialEq)]
/// Which section of the transport is focused
pub enum TransportFocus { BPM, Quant, Sync }
impl TransportFocus {
pub fn prev (&mut self) {
*self = match self {
Self::BPM => Self::Sync,
Self::Quant => Self::BPM,
Self::Sync => Self::Quant,
}
}
pub fn next (&mut self) {
*self = match self {
Self::BPM => Self::Quant,
Self::Quant => Self::Sync,
Self::Sync => Self::BPM,
}
}
}