mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 11:46:41 +01:00
src -> app; move core libs to tengri
This commit is contained in:
parent
8465d64807
commit
bcc3f5809e
113 changed files with 132 additions and 5729 deletions
27
app/Cargo.toml
Normal file
27
app/Cargo.toml
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
[package]
|
||||
name = "tek"
|
||||
edition = "2021"
|
||||
version = "0.2.0"
|
||||
|
||||
[dependencies]
|
||||
tek_tui = { git = "https://codeberg.org/unspeaker/tengri", ref = "5352a9d" }
|
||||
|
||||
tek_jack = { path = "../jack" }
|
||||
tek_time = { path = "../time" }
|
||||
tek_midi = { path = "../midi" }
|
||||
tek_sampler = { path = "../sampler" }
|
||||
tek_plugin = { path = "../plugin" }
|
||||
|
||||
backtrace = "0.3.72"
|
||||
palette = { version = "0.7.6", features = [ "random" ] }
|
||||
rand = "0.8.5"
|
||||
toml = "0.8.12"
|
||||
clap = { optional = true, version = "4.5.4", features = [ "derive" ] }
|
||||
|
||||
[dev-dependencies]
|
||||
proptest = "^1"
|
||||
proptest-derive = "^0.5.1"
|
||||
|
||||
[features]
|
||||
default = ["cli"]
|
||||
cli = ["clap"]
|
||||
0
app/examples/arranger.edn
Normal file
0
app/examples/arranger.edn
Normal file
0
app/examples/clip.edn
Normal file
0
app/examples/clip.edn
Normal file
12
app/examples/mixer.edn
Normal file
12
app/examples/mixer.edn
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
(mixer
|
||||
(track
|
||||
(name "Drums")
|
||||
(sampler
|
||||
(dir "/home/user/Lab/Music/pak")
|
||||
(sample (midi 34) (name "808 D") (file "808.wav"))))
|
||||
(track
|
||||
(name "Lead")
|
||||
(lv2
|
||||
(name "Odin2")
|
||||
(path "file:///home/user/.lv2/Odin2.lv2"))
|
||||
(gain 0.0)))
|
||||
0
app/examples/sampler.edn
Normal file
0
app/examples/sampler.edn
Normal file
18
app/examples/sequencer.edn
Normal file
18
app/examples/sequencer.edn
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
(arranger
|
||||
(track
|
||||
(name "Drums")
|
||||
(phrase
|
||||
(name "4 kicks")
|
||||
(beats 4)
|
||||
(steps 16)
|
||||
(:00 (36 128))
|
||||
(:04 (36 100))
|
||||
(:08 (36 100))
|
||||
(:12 (36 100))))
|
||||
(track
|
||||
(name "Bass")
|
||||
(phrase
|
||||
(beats 4)
|
||||
(steps 16)
|
||||
(:04 (36 100))
|
||||
(:12 (36 100)))))
|
||||
82
app/src/arranger.edn
Normal file
82
app/src/arranger.edn
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
This is the unified Tek Arranger.
|
||||
|
||||
Its appearance is defined by the following view definition:
|
||||
|
||||
{def :view (bsp/s (fixed/y 2 :toolbar)
|
||||
(fill/x (align/c (bsp/w (fixed/x :pool-w :pool)
|
||||
(bsp/n (fixed/y 3 :outputs)
|
||||
(bsp/n (fixed/y 3 :inputs)
|
||||
(bsp/n (fixed/y 3 :tracks) :scenes)))))))}
|
||||
|
||||
The arranger's behavior is controlled by the
|
||||
following keymaps:
|
||||
|
||||
{def :keys
|
||||
(@u undo 1)
|
||||
(@shift-u redo 1)
|
||||
(@space clock toggle)
|
||||
(@shift-space clock toggle 0)
|
||||
(@ctrl-a scene add)
|
||||
(@ctrl-t track add)
|
||||
(@tab edit :clip)
|
||||
(@c color)}
|
||||
|
||||
{def :keys-mix
|
||||
(@down select 0 1)
|
||||
(@s select 0 1)
|
||||
|
||||
(@right select 1 0)
|
||||
(@d select 1 0)}
|
||||
|
||||
{def :keys-track
|
||||
(@left select :track-prev :scene)
|
||||
(@a select :track-prev :scene)
|
||||
(@right select :track-next :scene)
|
||||
(@d select :track-next :scene)
|
||||
(@down select :track :scene-next)
|
||||
(@s select :track :scene-next)
|
||||
|
||||
(@q track launch)
|
||||
(@c track color :track)
|
||||
(@comma track swap-prev)
|
||||
(@period track swap-next)
|
||||
(@lt track size-dec)
|
||||
(@gt track size-inc)
|
||||
(@delete track delete)}
|
||||
|
||||
{def :keys-scene
|
||||
(@up select :track :scene-prev)
|
||||
(@w select :track :scene-prev)
|
||||
(@down select :track :scene-next)
|
||||
(@s select :track :scene-next)
|
||||
(@right select :track-next :scene)
|
||||
(@d select :track-next :scene)
|
||||
|
||||
(@q scene launch)
|
||||
(@c scene color :scene)
|
||||
(@comma scene swap-prev)
|
||||
(@period scene swap-next)
|
||||
(@lt scene size-dec)
|
||||
(@gt scene size-inc)
|
||||
(@delete scene delete)}
|
||||
|
||||
{def :keys-clip
|
||||
(@up select :track :scene-prev)
|
||||
(@w select :track :scene-prev)
|
||||
(@down select :track :scene-next)
|
||||
(@s select :track :scene-next)
|
||||
(@left select :track-prev :scene)
|
||||
(@a select :track-prev :scene)
|
||||
(@right select :track-next :scene)
|
||||
(@d select :track-next :scene)
|
||||
|
||||
(@q enqueue :clip)
|
||||
(@c clip color :track :scene)
|
||||
(@g clip get)
|
||||
(@p clip put)
|
||||
(@delete clip del)
|
||||
(@comma clip prev)
|
||||
(@period clip next)
|
||||
(@lt clip swap-prev)
|
||||
(@gt clip swap-next)
|
||||
(@l clip loop-toggle)}
|
||||
95
app/src/audio.rs
Normal file
95
app/src/audio.rs
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
use crate::*;
|
||||
impl HasJack for Tek { fn jack (&self) -> &Jack { &self.jack } }
|
||||
audio!(
|
||||
|self: Tek, client, scope|{
|
||||
// Start profiling cycle
|
||||
let t0 = self.perf.get_t0();
|
||||
// Update transport clock
|
||||
self.clock().update_from_scope(scope).unwrap();
|
||||
// Collect MIDI input (TODO preallocate)
|
||||
let midi_in = self.midi_ins.iter()
|
||||
.map(|port|port.port().iter(scope)
|
||||
.map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes)))
|
||||
.collect::<Vec<_>>())
|
||||
.collect::<Vec<_>>();
|
||||
// Update standalone MIDI sequencer
|
||||
//if let Some(player) = self.player.as_mut() {
|
||||
//if Control::Quit == PlayerAudio(
|
||||
//player,
|
||||
//&mut self.note_buf,
|
||||
//&mut self.midi_buf,
|
||||
//).process(client, scope) {
|
||||
//return Control::Quit
|
||||
//}
|
||||
//}
|
||||
// Update standalone sampler
|
||||
//if let Some(sampler) = self.sampler.as_mut() {
|
||||
//if Control::Quit == SamplerAudio(sampler).process(client, scope) {
|
||||
//return Control::Quit
|
||||
//}
|
||||
//for port in midi_in.iter() {
|
||||
//for message in port.iter() {
|
||||
//match message {
|
||||
//Ok(M
|
||||
//}
|
||||
//}
|
||||
//}
|
||||
//}
|
||||
// TODO move these to editor and sampler?:
|
||||
//for port in midi_in.iter() {
|
||||
//for event in port.iter() {
|
||||
//match event {
|
||||
//(time, Ok(LiveEvent::Midi {message, ..})) => match message {
|
||||
//MidiMessage::NoteOn {ref key, ..} if let Some(editor) = self.editor.as_ref() => {
|
||||
//editor.set_note_pos(key.as_int() as usize);
|
||||
//},
|
||||
//MidiMessage::Controller {controller, value} if let (Some(editor), Some(sampler)) = (
|
||||
//self.editor.as_ref(),
|
||||
//self.sampler.as_ref(),
|
||||
//) => {
|
||||
//// TODO: give sampler its own cursor
|
||||
//if let Some(sample) = &sampler.mapped[editor.note_pos()] {
|
||||
//sample.write().unwrap().handle_cc(*controller, *value)
|
||||
//}
|
||||
//}
|
||||
//_ =>{}
|
||||
//},
|
||||
//_ =>{}
|
||||
//}
|
||||
//}
|
||||
//}
|
||||
// Update track sequencers
|
||||
for track in self.tracks.iter_mut() {
|
||||
if PlayerAudio(
|
||||
track.player_mut(), &mut self.note_buf, &mut self.midi_buf
|
||||
).process(client, scope) == Control::Quit {
|
||||
return Control::Quit
|
||||
}
|
||||
}
|
||||
// End profiling cycle
|
||||
self.perf.update(t0, scope);
|
||||
Control::Continue
|
||||
};
|
||||
|self, event|{
|
||||
use JackEvent::*;
|
||||
match event {
|
||||
SampleRate(sr) => { self.clock.timebase.sr.set(sr as f64); },
|
||||
PortRegistration(id, true) => {
|
||||
//let port = self.jack().port_by_id(id);
|
||||
//println!("\rport add: {id} {port:?}");
|
||||
//println!("\rport add: {id}");
|
||||
},
|
||||
PortRegistration(id, false) => {
|
||||
/*println!("\rport del: {id}")*/
|
||||
},
|
||||
PortsConnected(a, b, true) => { /*println!("\rport conn: {a} {b}")*/ },
|
||||
PortsConnected(a, b, false) => { /*println!("\rport disc: {a} {b}")*/ },
|
||||
ClientRegistration(id, true) => {},
|
||||
ClientRegistration(id, false) => {},
|
||||
ThreadInit => {},
|
||||
XRun => {},
|
||||
GraphReorder => {},
|
||||
_ => { panic!("{event:?}"); }
|
||||
}
|
||||
}
|
||||
);
|
||||
212
app/src/cli.rs
Normal file
212
app/src/cli.rs
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
use crate::*;
|
||||
use clap::{self, Parser, Subcommand};
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(version, about, long_about = None)]
|
||||
pub struct TekCli {
|
||||
/// Which app to initialize
|
||||
#[command(subcommand)] mode: TekMode,
|
||||
/// Name of JACK client
|
||||
#[arg(short='n', long)] name: Option<String>,
|
||||
/// Whether to attempt to become transport master
|
||||
#[arg(short='S', long, default_value_t = false)] sync_lead: bool,
|
||||
/// Whether to sync to external transport master
|
||||
#[arg(short='s', long, default_value_t = true)] sync_follow: bool,
|
||||
/// Initial tempo in beats per minute
|
||||
#[arg(short='b', long, default_value = None)] bpm: Option<f64>,
|
||||
/// Whether to include a transport toolbar (default: true)
|
||||
#[arg(short='t', long, default_value_t = true)] show_clock: bool,
|
||||
/// MIDI outs to connect to (multiple instances accepted)
|
||||
#[arg(short='I', long)] midi_from: Vec<String>,
|
||||
/// MIDI outs to connect to (multiple instances accepted)
|
||||
#[arg(short='i', long)] midi_from_re: Vec<String>,
|
||||
/// MIDI ins to connect to (multiple instances accepted)
|
||||
#[arg(short='O', long)] midi_to: Vec<String>,
|
||||
/// MIDI ins to connect to (multiple instances accepted)
|
||||
#[arg(short='o', long)] midi_to_re: Vec<String>,
|
||||
/// Audio outs to connect to left input
|
||||
#[arg(short='l', long)] left_from: Vec<String>,
|
||||
/// Audio outs to connect to right input
|
||||
#[arg(short='r', long)] right_from: Vec<String>,
|
||||
/// Audio ins to connect from left output
|
||||
#[arg(short='L', long)] left_to: Vec<String>,
|
||||
/// Audio ins to connect from right output
|
||||
#[arg(short='R', long)] right_to: Vec<String>,
|
||||
}
|
||||
#[derive(Debug, Clone, Subcommand)] pub enum TekMode {
|
||||
/// A standalone transport clock.
|
||||
Clock,
|
||||
/// A MIDI sequencer.
|
||||
Sequencer,
|
||||
/// A MIDI-controlled audio sampler.
|
||||
Sampler,
|
||||
/// Sequencer and sampler together.12
|
||||
Groovebox,
|
||||
/// Multi-track MIDI sequencer.
|
||||
Arranger {
|
||||
/// Number of scenes
|
||||
#[arg(short = 'y', long, default_value_t = 12)] scenes: usize,
|
||||
/// Number of tracks
|
||||
#[arg(short = 'x', long, default_value_t = 16)] tracks: usize,
|
||||
/// Width of tracks
|
||||
#[arg(short = 'w', long, default_value_t = 12)] track_width: usize,
|
||||
},
|
||||
/// TODO: A MIDI-controlled audio mixer
|
||||
Mixer,
|
||||
/// TODO: A customizable channel strip
|
||||
Track,
|
||||
/// TODO: An audio plugin host
|
||||
Plugin,
|
||||
}
|
||||
impl TekCli {
|
||||
pub fn run (&self) -> Usually<()> {
|
||||
let name = self.name.as_ref().map_or("tek", |x|x.as_str());
|
||||
//let color = ItemPalette::random();
|
||||
let jack = Jack::new(name)?;
|
||||
let engine = Tui::new()?;
|
||||
let empty = &[] as &[&str];
|
||||
let midi_froms = PortConnect::collect(&self.midi_from, empty, &self.midi_from_re);
|
||||
let midi_tos = PortConnect::collect(&self.midi_to, empty, &self.midi_to_re);
|
||||
let left_froms = PortConnect::collect(&self.left_from, empty, empty);
|
||||
let left_tos = PortConnect::collect(&self.left_to, empty, empty);
|
||||
let right_froms = PortConnect::collect(&self.right_from, empty, empty);
|
||||
let right_tos = PortConnect::collect(&self.right_to, empty, empty);
|
||||
let audio_froms = &[left_froms.as_slice(), right_froms.as_slice()];
|
||||
let audio_tos = &[left_tos.as_slice(), right_tos.as_slice() ];
|
||||
let jack = jack.run(|jack|match self.mode {
|
||||
TekMode::Clock => Tek::new_clock(
|
||||
jack, self.bpm, self.sync_lead, self.sync_follow,
|
||||
&midi_froms, &midi_tos),
|
||||
TekMode::Sequencer => Tek::new_sequencer(
|
||||
jack, self.bpm, self.sync_lead, self.sync_follow,
|
||||
&midi_froms, &midi_tos),
|
||||
TekMode::Groovebox => Tek::new_groovebox(
|
||||
jack, self.bpm, self.sync_lead, self.sync_follow,
|
||||
&midi_froms, &midi_tos,
|
||||
&audio_froms, &audio_tos),
|
||||
TekMode::Arranger { scenes, tracks, track_width, .. } => Tek::new_arranger(
|
||||
jack, self.bpm, self.sync_lead, self.sync_follow,
|
||||
&midi_froms, &midi_tos,
|
||||
&audio_froms, &audio_tos,
|
||||
scenes, tracks, track_width),
|
||||
_ => todo!()
|
||||
})?;
|
||||
engine.run(&jack)
|
||||
}
|
||||
}
|
||||
impl Tek {
|
||||
pub fn new_clock (
|
||||
jack: &Jack,
|
||||
bpm: Option<f64>, sync_lead: bool, sync_follow: bool,
|
||||
midi_froms: &[PortConnect], midi_tos: &[PortConnect],
|
||||
) -> Usually<Self> {
|
||||
let tek = Self {
|
||||
view: SourceIter(include_str!("./view_transport.edn")),
|
||||
jack: jack.clone(),
|
||||
color: ItemPalette::random(),
|
||||
clock: Clock::new(jack, bpm)?,
|
||||
midi_ins: {
|
||||
let mut midi_ins = vec![];
|
||||
for (index, connect) in midi_froms.iter().enumerate() {
|
||||
let port = JackMidiIn::new(jack, &format!("M/{index}"), &[connect.clone()])?;
|
||||
midi_ins.push(port);
|
||||
}
|
||||
midi_ins
|
||||
},
|
||||
midi_outs: {
|
||||
let mut midi_outs = vec![];
|
||||
for (index, connect) in midi_tos.iter().enumerate() {
|
||||
let port = JackMidiOut::new(jack, &format!("{index}/M"), &[connect.clone()])?;
|
||||
midi_outs.push(port);
|
||||
}
|
||||
midi_outs
|
||||
},
|
||||
keys: SourceIter(KEYS_APP),
|
||||
keys_clip: SourceIter(KEYS_CLIP),
|
||||
keys_track: SourceIter(KEYS_TRACK),
|
||||
keys_scene: SourceIter(KEYS_SCENE),
|
||||
keys_mix: SourceIter(KEYS_MIX),
|
||||
tracks: vec![],
|
||||
scenes: vec![],
|
||||
..Default::default()
|
||||
};
|
||||
jack.sync_lead(sync_lead, |mut state|{
|
||||
let clock = tek.clock();
|
||||
clock.playhead.update_from_sample(state.position.frame() as f64);
|
||||
state.position.bbt = Some(clock.bbt());
|
||||
state.position
|
||||
});
|
||||
jack.sync_follow(sync_follow);
|
||||
Ok(tek)
|
||||
}
|
||||
pub fn new_sequencer (
|
||||
jack: &Jack,
|
||||
bpm: Option<f64>, sync_lead: bool, sync_follow: bool,
|
||||
midi_froms: &[PortConnect], midi_tos: &[PortConnect],
|
||||
) -> Usually<Self> {
|
||||
let clip = MidiClip::new("Clip", true, 384usize, None, Some(ItemColor::random().into()));
|
||||
let clip = Arc::new(RwLock::new(clip));
|
||||
let this = Self::new_clock(jack, bpm, sync_lead, sync_follow, midi_froms, midi_tos)?;
|
||||
Ok(Self {
|
||||
view: SourceIter(include_str!("./view_sequencer.edn")),
|
||||
pool: Some((&clip).into()),
|
||||
editor: Some((&clip).into()),
|
||||
editing: false.into(),
|
||||
midi_buf: vec![vec![];65536],
|
||||
tracks: vec![Track::default()],
|
||||
//player: Some(MidiPlayer::new("sequencer", &jack, Some(&this.clock), Some(&clip), &midi_froms, &midi_tos)?),
|
||||
..this
|
||||
})
|
||||
}
|
||||
pub fn new_groovebox (
|
||||
jack: &Jack,
|
||||
bpm: Option<f64>, sync_lead: bool, sync_follow: bool,
|
||||
midi_froms: &[PortConnect], midi_tos: &[PortConnect],
|
||||
audio_froms: &[&[PortConnect];2], audio_tos: &[&[PortConnect];2],
|
||||
) -> Usually<Self> {
|
||||
let tek = Self {
|
||||
view: SourceIter(include_str!("./view_groovebox.edn")),
|
||||
tracks: vec![Track {
|
||||
devices: vec![Sampler::new(jack, &"sampler", midi_froms, audio_froms, audio_tos)?.boxed()],
|
||||
..Track::default()
|
||||
}],
|
||||
..Self::new_sequencer(jack, bpm, sync_lead, sync_follow, midi_froms, midi_tos)?
|
||||
};
|
||||
//if let Some(sampler) = tek.sampler.as_ref().unwrap().midi_in.as_ref() {
|
||||
//tek.player.as_ref().unwrap().midi_outs[0].connect_to(sampler.port())?;
|
||||
//}
|
||||
Ok(tek)
|
||||
}
|
||||
pub fn new_arranger (
|
||||
jack: &Jack,
|
||||
bpm: Option<f64>, sync_lead: bool, sync_follow: bool,
|
||||
midi_froms: &[PortConnect], midi_tos: &[PortConnect],
|
||||
audio_froms: &[&[PortConnect];2], audio_tos: &[&[PortConnect];2],
|
||||
scenes: usize, tracks: usize, track_width: usize,
|
||||
) -> Usually<Self> {
|
||||
let mut tek = Self {
|
||||
view: SourceIter(include_str!("./view_arranger.edn")),
|
||||
pool: Some(Default::default()),
|
||||
editor: Some(Default::default()),
|
||||
editing: false.into(),
|
||||
midi_buf: vec![vec![];65536],
|
||||
tracks: vec![],
|
||||
scenes: vec![],
|
||||
..Self::new_clock(jack, bpm, sync_lead, sync_follow, midi_froms, midi_tos)?
|
||||
};
|
||||
tek.arranger = Default::default();
|
||||
tek.selected = Selection::Clip(1, 1);
|
||||
tek.scenes_add(scenes);
|
||||
tek.tracks_add(tracks, Some(track_width), &[], &[]);
|
||||
Ok(tek)
|
||||
}
|
||||
}
|
||||
#[cfg(test)] #[test] fn test_tek_cli () {
|
||||
use clap::CommandFactory;
|
||||
TekCli::command().debug_assert();
|
||||
let jack = Jack::default();
|
||||
//TODO:
|
||||
//let _ = Tek::new_clock(&jack, None, false, false, &[], &[]);
|
||||
//let _ = Tek::new_sequencer(&jack, None, false, false, &[], &[]);
|
||||
//let _ = Tek::new_groovebox(&jack, None, false, false, &[], &[], &[&[], &[]], &[&[], &[]]);
|
||||
//let _ = Tek::new_arranger(&jack, None, false, false, &[], &[], &[&[], &[]], &[&[], &[]], 0, 0, 0);
|
||||
}
|
||||
6
app/src/device.rs
Normal file
6
app/src/device.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
use crate::*;
|
||||
pub trait Device: Send + Sync + std::fmt::Debug {
|
||||
fn boxed <'a> (self) -> Box<dyn Device + 'a> where Self: Sized + 'a { Box::new(self) }
|
||||
}
|
||||
impl Device for Sampler {}
|
||||
impl Device for Plugin {}
|
||||
21
app/src/keys.edn
Normal file
21
app/src/keys.edn
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
(@u undo 1)
|
||||
(@shift-u redo 1)
|
||||
(@space clock toggle)
|
||||
(@shift-space clock toggle 0)
|
||||
(@t select :track 0)
|
||||
(@tab edit :clip)
|
||||
(@c color)
|
||||
(@q launch)
|
||||
(@shift-I input add)
|
||||
(@shift-O output add)
|
||||
(@shift-S scene add)
|
||||
(@shift-T track add)
|
||||
|
||||
(@up select :scene-prev)
|
||||
(@w select :scene-prev)
|
||||
(@down select :scene-next)
|
||||
(@s select :scene-next)
|
||||
(@left select :track-prev)
|
||||
(@a select :track-prev)
|
||||
(@right select :track-next)
|
||||
(@d select :track-next)
|
||||
205
app/src/keys.rs
Normal file
205
app/src/keys.rs
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
use crate::*;
|
||||
pub const KEYS_APP: &str = include_str!("keys.edn");
|
||||
pub const KEYS_CLIP: &str = include_str!("keys_clip.edn");
|
||||
pub const KEYS_TRACK: &str = include_str!("keys_track.edn");
|
||||
pub const KEYS_SCENE: &str = include_str!("keys_scene.edn");
|
||||
pub const KEYS_MIX: &str = include_str!("keys_mix.edn");
|
||||
handle!(TuiIn: |self: Tek, input|Ok({
|
||||
// If editing, editor keys take priority
|
||||
if self.is_editing() {
|
||||
if self.editor.handle(input)? == Some(true) {
|
||||
return Ok(Some(true))
|
||||
}
|
||||
}
|
||||
// Handle from root keymap
|
||||
if let Some(command) = self.keys.command::<_, TekCommand, _>(self, input) {
|
||||
if let Some(undo) = command.execute(self)? { self.history.push(undo); }
|
||||
return Ok(Some(true))
|
||||
}
|
||||
// Handle from selection-dependent keymaps
|
||||
if let Some(command) = match self.selected() {
|
||||
Selection::Clip(_, _) => self.keys_clip,
|
||||
Selection::Track(_) => self.keys_track,
|
||||
Selection::Scene(_) => self.keys_scene,
|
||||
Selection::Mix => self.keys_mix,
|
||||
}.command::<_, TekCommand, _>(self, input) {
|
||||
if let Some(undo) = command.execute(self)? { self.history.push(undo); }
|
||||
return Ok(Some(true))
|
||||
}
|
||||
None
|
||||
}));
|
||||
#[derive(Clone, Debug)] pub enum TekCommand {
|
||||
Clip(ClipCommand),
|
||||
Clock(ClockCommand),
|
||||
Color(ItemPalette),
|
||||
Edit(Option<bool>),
|
||||
Editor(MidiEditCommand),
|
||||
Enqueue(Option<Arc<RwLock<MidiClip>>>),
|
||||
History(isize),
|
||||
Input(InputCommand),
|
||||
Launch,
|
||||
Output(OutputCommand),
|
||||
Pool(PoolCommand),
|
||||
Sampler(SamplerCommand),
|
||||
Scene(SceneCommand),
|
||||
Select(Selection),
|
||||
StopAll,
|
||||
Track(TrackCommand),
|
||||
Zoom(Option<usize>),
|
||||
}
|
||||
atom_command!(TekCommand: |app: Tek| {
|
||||
("stop" [] Some(Self::StopAll))
|
||||
("undo" [d: usize] Some(Self::History(-(d.unwrap_or(0)as isize))))
|
||||
("redo" [d: usize] Some(Self::History(d.unwrap_or(0) as isize)))
|
||||
("zoom" [z: usize] Some(Self::Zoom(z)))
|
||||
("edit" [] Some(Self::Edit(None)))
|
||||
("edit" [c: bool] Some(Self::Edit(c)))
|
||||
("color" [c: Color] Some(Self::Color(ItemPalette::random())))
|
||||
("color" [c: Color] Some(Self::Color(c.map(ItemPalette::from).expect("no color"))))
|
||||
("enqueue" [c: Arc<RwLock<MidiClip>>] Some(Self::Enqueue(c)))
|
||||
("launch" [] Some(Self::Launch))
|
||||
("clip" [,..a] ClipCommand::try_from_expr(app, a).map(Self::Clip))
|
||||
("clock" [,..a] ClockCommand::try_from_expr(app.clock(), a).map(Self::Clock))
|
||||
("editor" [,..a] MidiEditCommand::try_from_expr(app.editor.as_ref().expect("no editor"), a).map(Self::Editor))
|
||||
("pool" [,..a] PoolCommand::try_from_expr(app.pool.as_ref().expect("no pool"), a).map(Self::Pool))
|
||||
//("sampler" [,..a] Self::Sampler( //SamplerCommand::try_from_expr(app.sampler().as_ref().expect("no sampler"), a).expect("invalid command")))
|
||||
("scene" [,..a] SceneCommand::try_from_expr(app, a).map(Self::Scene))
|
||||
("track" [,..a] TrackCommand::try_from_expr(app, a).map(Self::Track))
|
||||
("input" [,..a] InputCommand::try_from_expr(app, a).map(Self::Input))
|
||||
("output" [,..a] OutputCommand::try_from_expr(app, a).map(Self::Output))
|
||||
("select" [t: Selection] Some(t.map(Self::Select).expect("no selection")))
|
||||
("select" [t: usize, s: usize] Some(match (t.expect("no track"), s.expect("no scene")) {
|
||||
(0, 0) => Self::Select(Selection::Mix),
|
||||
(t, 0) => Self::Select(Selection::Track(t)),
|
||||
(0, s) => Self::Select(Selection::Scene(s)),
|
||||
(t, s) => Self::Select(Selection::Clip(t, s)),
|
||||
}))
|
||||
});
|
||||
command!(|self: TekCommand, app: Tek|match self {
|
||||
Self::Zoom(_) => { println!("\n\rtodo: global zoom"); None },
|
||||
Self::History(delta) => { println!("\n\rtodo: undo/redo"); None },
|
||||
Self::Select(s) => {
|
||||
app.selected = s;
|
||||
// autoedit: load focused clip in editor.
|
||||
if let Some(ref mut editor) = app.editor {
|
||||
editor.set_clip(match app.selected {
|
||||
Selection::Clip(t, s) if let Some(Some(Some(clip))) = app
|
||||
.scenes.get(s).map(|s|s.clips.get(t)) => Some(clip),
|
||||
_ => None
|
||||
});
|
||||
}
|
||||
None
|
||||
},
|
||||
Self::Edit(value) => {
|
||||
if let Some(value) = value {
|
||||
if app.is_editing() != value {
|
||||
app.editing.store(value, Relaxed);
|
||||
}
|
||||
} else {
|
||||
app.editing.store(!app.is_editing(), Relaxed);
|
||||
};
|
||||
// autocreate: create new clip from pool when entering empty cell
|
||||
if let Some(ref pool) = app.pool {
|
||||
if app.is_editing() {
|
||||
if let Selection::Clip(t, s) = app.selected {
|
||||
if let Some(scene) = app.scenes.get_mut(s) {
|
||||
if let Some(slot) = scene.clips.get_mut(t) {
|
||||
if slot.is_none() {
|
||||
let (index, mut clip) = pool.add_new_clip();
|
||||
// autocolor: new clip colors from scene and track color
|
||||
clip.write().unwrap().color = ItemColor::random_near(
|
||||
app.tracks[t].color.base.mix(
|
||||
scene.color.base,
|
||||
0.5
|
||||
),
|
||||
0.2
|
||||
).into();
|
||||
if let Some(ref mut editor) = app.editor {
|
||||
editor.set_clip(Some(&clip));
|
||||
}
|
||||
*slot = Some(clip);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
},
|
||||
Self::Clock(cmd) => cmd.delegate(app, Self::Clock)?,
|
||||
Self::Scene(cmd) => cmd.delegate(app, Self::Scene)?,
|
||||
Self::Track(cmd) => cmd.delegate(app, Self::Track)?,
|
||||
Self::Input(cmd) => cmd.delegate(app, Self::Input)?,
|
||||
Self::Output(cmd) => cmd.delegate(app, Self::Output)?,
|
||||
Self::Clip(cmd) => cmd.delegate(app, Self::Clip)?,
|
||||
Self::Editor(cmd) => app.editor.as_mut()
|
||||
.map(|editor|cmd.delegate(editor, Self::Editor)).transpose()?.flatten(),
|
||||
//Self::Sampler(cmd) => app.sampler.as_mut()
|
||||
//.map(|sampler|cmd.delegate(sampler, Self::Sampler)).transpose()?.flatten(),
|
||||
//Self::Enqueue(clip) => app.player.as_mut()
|
||||
//.map(|player|{player.enqueue_next(clip.as_ref());None}).flatten(),
|
||||
Self::Launch => {
|
||||
use Selection::*;
|
||||
match app.selected {
|
||||
Track(t) => app.tracks[t].player.enqueue_next(None),
|
||||
Clip(t, s) => app.tracks[t].player.enqueue_next(app.scenes[s].clips[t].as_ref()),
|
||||
Scene(s) => {
|
||||
for t in 0..app.tracks.len() {
|
||||
app.tracks[t].player.enqueue_next(app.scenes[s].clips[t].as_ref())
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
};
|
||||
None
|
||||
},
|
||||
Self::Color(palette) => {
|
||||
use Selection::*;
|
||||
Some(Self::Color(match app.selected {
|
||||
Mix => {
|
||||
let old = app.color;
|
||||
app.color = palette;
|
||||
old
|
||||
},
|
||||
Track(t) => {
|
||||
let old = app.tracks[t].color;
|
||||
app.tracks[t].color = palette;
|
||||
old
|
||||
}
|
||||
Scene(s) => {
|
||||
let old = app.scenes[s].color;
|
||||
app.scenes[s].color = palette;
|
||||
old
|
||||
}
|
||||
Clip(t, s) => {
|
||||
if let Some(ref clip) = app.scenes[s].clips[t] {
|
||||
let mut clip = clip.write().unwrap();
|
||||
let old = clip.color;
|
||||
clip.color = palette;
|
||||
old
|
||||
} else {
|
||||
return Ok(None)
|
||||
}
|
||||
}
|
||||
}))
|
||||
},
|
||||
Self::StopAll => {
|
||||
for track in 0..app.tracks.len(){app.tracks[track].player.enqueue_next(None);}
|
||||
None
|
||||
},
|
||||
Self::Pool(cmd) => if let Some(pool) = app.pool.as_mut() {
|
||||
let undo = cmd.clone().delegate(pool, Self::Pool)?;
|
||||
if let Some(editor) = app.editor.as_mut() {
|
||||
match cmd {
|
||||
// autoselect: automatically load selected clip in editor
|
||||
// autocolor: update color in all places simultaneously
|
||||
PoolCommand::Select(_) | PoolCommand::Clip(PoolClipCommand::SetColor(_, _)) =>
|
||||
editor.set_clip(pool.clip().as_ref()),
|
||||
_ => {}
|
||||
}
|
||||
};
|
||||
undo
|
||||
} else {
|
||||
None
|
||||
},
|
||||
_ => todo!("{self:?}")
|
||||
});
|
||||
8
app/src/keys_clip.edn
Normal file
8
app/src/keys_clip.edn
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
(@g clip get)
|
||||
(@p clip put)
|
||||
(@delete clip del)
|
||||
(@comma clip prev)
|
||||
(@period clip next)
|
||||
(@lt clip swap-prev)
|
||||
(@gt clip swap-next)
|
||||
(@l clip loop-toggle)
|
||||
39
app/src/keys_clip.rs
Normal file
39
app/src/keys_clip.rs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
use crate::*;
|
||||
#[derive(Clone, Debug)] pub enum ClipCommand {
|
||||
Get(usize, usize),
|
||||
Put(usize, usize, Option<Arc<RwLock<MidiClip>>>),
|
||||
Enqueue(usize, usize),
|
||||
Edit(Option<Arc<RwLock<MidiClip>>>),
|
||||
SetLoop(usize, usize, bool),
|
||||
SetColor(usize, usize, ItemPalette),
|
||||
}
|
||||
atom_command!(ClipCommand: |app: Tek| {
|
||||
("get" [a: usize, b: usize] Some(Self::Get(a.unwrap(), b.unwrap())))
|
||||
("put" [a: usize, b: usize, c: Option<Arc<RwLock<MidiClip>>>] Some(Self::Put(a.unwrap(), b.unwrap(), c.unwrap())))
|
||||
("enqueue" [a: usize, b: usize] Some(Self::Enqueue(a.unwrap(), b.unwrap())))
|
||||
("edit" [a: Option<Arc<RwLock<MidiClip>>>] Some(Self::Edit(a.unwrap())))
|
||||
("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap())))
|
||||
("color" [a: usize, b: usize] Some(Self::SetColor(a.unwrap(), b.unwrap(), ItemPalette::random())))
|
||||
});
|
||||
command!(|self: ClipCommand, app: Tek|match self {
|
||||
Self::Get(track, scene) => { todo!() },
|
||||
Self::Put(track, scene, clip) => {
|
||||
let old = app.scenes[scene].clips[track].clone();
|
||||
app.scenes[scene].clips[track] = clip;
|
||||
Some(Self::Put(track, scene, old))
|
||||
},
|
||||
Self::Enqueue(track, scene) => {
|
||||
app.tracks[track].player.enqueue_next(app.scenes[scene].clips[track].as_ref());
|
||||
None
|
||||
},
|
||||
Self::SetColor(track, scene, color) => {
|
||||
app.scenes[scene].clips[track].as_ref().map(|clip|{
|
||||
let mut clip = clip.write().unwrap();
|
||||
let old = clip.color.clone();
|
||||
clip.color = color.clone();
|
||||
panic!("{color:?} {old:?}");
|
||||
Self::SetColor(track, scene, old)
|
||||
})
|
||||
},
|
||||
_ => None
|
||||
});
|
||||
12
app/src/keys_ins.rs
Normal file
12
app/src/keys_ins.rs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
use crate::*;
|
||||
#[derive(Clone, Debug)] pub enum InputCommand { Add }
|
||||
atom_command!(InputCommand: |app: Tek| {
|
||||
("add" [] Some(Self::Add))
|
||||
});
|
||||
command!(|self: InputCommand, app: Tek|match self {
|
||||
Self::Add => {
|
||||
app.midi_ins.push(JackMidiIn::new(&app.jack, &format!("M/{}", app.midi_ins.len()), &[])?);
|
||||
app.redraw_arranger();
|
||||
None
|
||||
},
|
||||
});
|
||||
0
app/src/keys_mix.edn
Normal file
0
app/src/keys_mix.edn
Normal file
12
app/src/keys_outs.rs
Normal file
12
app/src/keys_outs.rs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
use crate::*;
|
||||
#[derive(Clone, Debug)] pub enum OutputCommand { Add }
|
||||
atom_command!(OutputCommand: |app: Tek| {
|
||||
("add" [] Some(Self::Add))
|
||||
});
|
||||
command!(|self: OutputCommand, app: Tek|match self {
|
||||
Self::Add => {
|
||||
app.midi_outs.push(JackMidiOut::new(&app.jack, &format!("{}/M", app.midi_outs.len()), &[])?);
|
||||
app.redraw_arranger();
|
||||
None
|
||||
},
|
||||
});
|
||||
7
app/src/keys_scene.edn
Normal file
7
app/src/keys_scene.edn
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
(@q scene launch :scene)
|
||||
(@c scene color :scene)
|
||||
(@comma scene prev)
|
||||
(@period scene next)
|
||||
(@lt scene swap-prev)
|
||||
(@gt scene swap-next)
|
||||
(@delete scene delete)
|
||||
43
app/src/keys_scene.rs
Normal file
43
app/src/keys_scene.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
use crate::*;
|
||||
#[derive(Clone, Debug)] pub enum SceneCommand {
|
||||
Add,
|
||||
Del(usize),
|
||||
Swap(usize, usize),
|
||||
SetSize(usize),
|
||||
SetZoom(usize),
|
||||
SetColor(usize, ItemPalette),
|
||||
Enqueue(usize),
|
||||
}
|
||||
atom_command!(SceneCommand: |app: Tek| {
|
||||
("add" [] Some(Self::Add))
|
||||
("del" [a: usize] Some(Self::Del(0)))
|
||||
("zoom" [a: usize] Some(Self::SetZoom(a.unwrap())))
|
||||
("color" [a: usize] Some(Self::SetColor(a.unwrap(), ItemPalette::G[128])))
|
||||
("enqueue" [a: usize] Some(Self::Enqueue(a.unwrap())))
|
||||
("swap" [a: usize, b: usize] Some(Self::Swap(a.unwrap(), b.unwrap())))
|
||||
});
|
||||
command!(|self: SceneCommand, app: Tek|match self {
|
||||
Self::Add => {
|
||||
use Selection::*;
|
||||
let index = app.scene_add(None, None)?.0;
|
||||
app.selected = match app.selected {
|
||||
Scene(s) => Scene(index),
|
||||
Clip(t, s) => Clip(t, index),
|
||||
_ => app.selected
|
||||
};
|
||||
Some(Self::Del(index))
|
||||
},
|
||||
Self::Del(index) => { app.scene_del(index); None },
|
||||
Self::SetColor(index, color) => {
|
||||
let old = app.scenes[index].color;
|
||||
app.scenes[index].color = color;
|
||||
Some(Self::SetColor(index, old))
|
||||
},
|
||||
Self::Enqueue(scene) => {
|
||||
for track in 0..app.tracks.len() {
|
||||
app.tracks[track].player.enqueue_next(app.scenes[scene].clips[track].as_ref());
|
||||
}
|
||||
None
|
||||
},
|
||||
_ => None
|
||||
});
|
||||
12
app/src/keys_track.edn
Normal file
12
app/src/keys_track.edn
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
(@q track launch :track)
|
||||
(@c track color :track)
|
||||
(@comma track prev)
|
||||
(@period track next)
|
||||
(@lt track swap-prev)
|
||||
(@gt track swap-next)
|
||||
(@delete track delete)
|
||||
|
||||
(@r track rec)
|
||||
(@m track mon)
|
||||
(@p track play)
|
||||
(@P track solo)
|
||||
65
app/src/keys_track.rs
Normal file
65
app/src/keys_track.rs
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
use crate::*;
|
||||
#[derive(Clone, Debug)] pub enum TrackCommand {
|
||||
Add,
|
||||
Del(usize),
|
||||
Stop(usize),
|
||||
Swap(usize, usize),
|
||||
SetSize(usize),
|
||||
SetZoom(usize),
|
||||
SetColor(usize, ItemPalette),
|
||||
TogglePlay,
|
||||
ToggleSolo,
|
||||
ToggleRecord,
|
||||
ToggleMonitor,
|
||||
}
|
||||
atom_command!(TrackCommand: |app: Tek| {
|
||||
("add" [] Some(Self::Add))
|
||||
("size" [a: usize] Some(Self::SetSize(a.unwrap())))
|
||||
("zoom" [a: usize] Some(Self::SetZoom(a.unwrap())))
|
||||
("color" [a: usize] Some(Self::SetColor(a.unwrap(), ItemPalette::random())))
|
||||
("del" [a: usize] Some(Self::Del(a.unwrap())))
|
||||
("stop" [a: usize] Some(Self::Stop(a.unwrap())))
|
||||
("swap" [a: usize, b: usize] Some(Self::Swap(a.unwrap(), b.unwrap())))
|
||||
("play" [] Some(Self::TogglePlay))
|
||||
("solo" [] Some(Self::ToggleSolo))
|
||||
("rec" [] Some(Self::ToggleRecord))
|
||||
("mon" [] Some(Self::ToggleMonitor))
|
||||
});
|
||||
command!(|self: TrackCommand, app: Tek|match self {
|
||||
Self::Add => {
|
||||
use Selection::*;
|
||||
let index = app.track_add(None, None, &[], &[])?.0;
|
||||
app.selected = match app.selected {
|
||||
Track(t) => Track(index),
|
||||
Clip(t, s) => Clip(index, s),
|
||||
_ => app.selected
|
||||
};
|
||||
Some(Self::Del(index))
|
||||
},
|
||||
Self::Del(index) => { app.track_del(index); None },
|
||||
Self::Stop(track) => { app.tracks[track].player.enqueue_next(None); None },
|
||||
Self::SetColor(index, color) => {
|
||||
let old = app.tracks[index].color;
|
||||
app.tracks[index].color = color;
|
||||
Some(Self::SetColor(index, old))
|
||||
},
|
||||
Self::TogglePlay => {
|
||||
Some(Self::TogglePlay)
|
||||
},
|
||||
Self::ToggleSolo => {
|
||||
Some(Self::ToggleSolo)
|
||||
},
|
||||
Self::ToggleRecord => {
|
||||
if let Some(t) = app.selected.track() {
|
||||
app.tracks[t-1].player.recording = !app.tracks[t-1].player.recording;
|
||||
}
|
||||
Some(Self::ToggleRecord)
|
||||
},
|
||||
Self::ToggleMonitor => {
|
||||
if let Some(t) = app.selected.track() {
|
||||
app.tracks[t-1].player.monitoring = !app.tracks[t-1].player.monitoring;
|
||||
}
|
||||
Some(Self::ToggleMonitor)
|
||||
},
|
||||
_ => None
|
||||
});
|
||||
53
app/src/lib.rs
Normal file
53
app/src/lib.rs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
#![allow(unused)]
|
||||
#![allow(clippy::unit_arg)]
|
||||
#![feature(adt_const_params)]
|
||||
#![feature(associated_type_defaults)]
|
||||
#![feature(if_let_guard)]
|
||||
#![feature(impl_trait_in_assoc_type)]
|
||||
#![feature(type_alias_impl_trait)]
|
||||
#![feature(trait_alias)]
|
||||
mod cli; pub use self::cli::*;
|
||||
mod audio; pub use self::audio::*;
|
||||
mod device; pub use self::device::*;
|
||||
mod keys; pub use self::keys::*;
|
||||
mod keys_clip; pub use self::keys_clip::*;
|
||||
mod keys_ins; pub use self::keys_ins::*;
|
||||
mod keys_outs; pub use self::keys_outs::*;
|
||||
mod keys_scene; pub use self::keys_scene::*;
|
||||
mod keys_track; pub use self::keys_track::*;
|
||||
mod model; pub use self::model::*;
|
||||
mod model_track; pub use self::model_track::*;
|
||||
mod model_scene; pub use self::model_scene::*;
|
||||
mod model_select; pub use self::model_select::*;
|
||||
mod view; pub use self::view::*;
|
||||
mod view_arranger; pub use self::view_arranger::*;
|
||||
mod view_clock; pub use self::view_clock::*;
|
||||
mod view_color; pub use self::view_color::*;
|
||||
mod view_iter; pub use self::view_iter::*;
|
||||
mod view_memo; pub use self::view_memo::*;
|
||||
mod view_meter; pub use self::view_meter::*;
|
||||
mod view_sizes; pub use self::view_sizes::*;
|
||||
/// Standard result type.
|
||||
pub type Usually<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
/// Standard optional result type.
|
||||
pub type Perhaps<T> = std::result::Result<Option<T>, Box<dyn std::error::Error>>;
|
||||
pub use ::tek_time::{self, *};
|
||||
pub use ::tek_jack::{self, *, jack::*};
|
||||
pub use ::tek_midi::{self, *, midly::{MidiMessage, num::*, live::*}};
|
||||
pub use ::tek_sampler::{self, *};
|
||||
pub use ::tek_plugin::{self, *};
|
||||
pub use ::tek_tui::{
|
||||
*, tek_edn::*, tek_input::*, tek_output::*,
|
||||
ratatui, ratatui::{prelude::{Color::{self, *}, Style, Stylize, Buffer, Modifier}, buffer::Cell},
|
||||
crossterm, crossterm::event::{
|
||||
Event, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers, KeyCode::{self, *},
|
||||
},
|
||||
};
|
||||
pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicBool, Ordering::Relaxed}};
|
||||
// ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
//██Let me play the world's tiniest piano for you. ██
|
||||
//█▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀█
|
||||
//█▙▙█▙▙▙█▙▙█▙▙▙█▙▙█▙▙▙█▙▙█▙▙▙█▙▙█▙▙▙█▙▙█▙▙▙█▙▙█▙▙▙██
|
||||
//█▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄█
|
||||
//███████████████████████████████████████████████████
|
||||
//█ ▀ ▀ ▀ █
|
||||
153
app/src/model.rs
Normal file
153
app/src/model.rs
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
use crate::*;
|
||||
#[derive(Default, Debug)] pub struct Tek {
|
||||
/// Must not be dropped for the duration of the process
|
||||
pub jack: Jack,
|
||||
/// Source of time
|
||||
pub clock: Clock,
|
||||
/// Theme
|
||||
pub color: ItemPalette,
|
||||
/// Contains all clips in the project
|
||||
pub pool: Option<MidiPool>,
|
||||
/// Contains the currently edited MIDI clip
|
||||
pub editor: Option<MidiEditor>,
|
||||
/// Contains a render of the project arrangement, redrawn on update.
|
||||
pub arranger: Arc<RwLock<Buffer>>,
|
||||
pub midi_ins: Vec<JackMidiIn>,
|
||||
pub midi_outs: Vec<JackMidiOut>,
|
||||
pub audio_ins: Vec<JackAudioIn>,
|
||||
pub audio_outs: Vec<JackAudioOut>,
|
||||
pub note_buf: Vec<u8>,
|
||||
pub midi_buf: Vec<Vec<Vec<u8>>>,
|
||||
|
||||
pub tracks: Vec<Track>,
|
||||
pub track_scroll: usize,
|
||||
|
||||
pub scenes: Vec<Scene>,
|
||||
pub scene_scroll: usize,
|
||||
|
||||
pub selected: Selection,
|
||||
pub size: Measure<TuiOut>,
|
||||
pub perf: PerfModel,
|
||||
pub editing: AtomicBool,
|
||||
pub history: Vec<TekCommand>,
|
||||
pub ports: std::collections::BTreeMap<u32, Port<Unowned>>,
|
||||
|
||||
/// View definition
|
||||
pub view: SourceIter<'static>,
|
||||
// Input definitions
|
||||
pub keys: SourceIter<'static>,
|
||||
pub keys_clip: SourceIter<'static>,
|
||||
pub keys_track: SourceIter<'static>,
|
||||
pub keys_scene: SourceIter<'static>,
|
||||
pub keys_mix: SourceIter<'static>,
|
||||
|
||||
pub(crate) fmtd: Arc<RwLock<ViewCache>>,
|
||||
}
|
||||
has_size!(<TuiOut>|self: Tek|&self.size);
|
||||
has_clock!(|self: Tek|self.clock);
|
||||
has_clips!(|self: Tek|self.pool.as_ref().expect("no clip pool").clips);
|
||||
//has_sampler!(|self: Tek|{
|
||||
//sampler = self.sampler;
|
||||
//index = self.editor.as_ref().map(|e|e.note_pos()).unwrap_or(0); });
|
||||
has_editor!(|self: Tek|{
|
||||
editor = self.editor;
|
||||
editor_w = {
|
||||
let size = self.size.w();
|
||||
let editor = self.editor.as_ref().expect("missing editor");
|
||||
let time_len = editor.time_len().get();
|
||||
let time_zoom = editor.time_zoom().get().max(1);
|
||||
(5 + (time_len / time_zoom)).min(size.saturating_sub(20)).max(16)
|
||||
};
|
||||
editor_h = 15;
|
||||
is_editing = self.editing.load(Relaxed); });
|
||||
provide!(Color: |self: Tek| {});
|
||||
provide!(Arc<RwLock<MidiClip>>: |self: Tek| {});
|
||||
provide!(Option<Arc<RwLock<MidiClip>>>: |self: Tek| {
|
||||
":clip" => match self.selected {
|
||||
Selection::Clip(t, s) => self.scenes[s].clips[t].clone(),
|
||||
_ => None
|
||||
}
|
||||
});
|
||||
provide_bool!(bool: |self: Tek| {});
|
||||
provide_num!(isize: |self: Tek| {});
|
||||
provide_num!(usize: |self: Tek| {
|
||||
":scene-last" => self.scenes.len(),
|
||||
":track-last" => self.tracks.len() });
|
||||
provide!(Option<usize>: |self: Tek| {
|
||||
":scene" => self.selected.scene(),
|
||||
":track" => self.selected.track() });
|
||||
provide!(Selection: |self: Tek| {
|
||||
":scene-next" => match self.selected {
|
||||
Selection::Mix => Selection::Scene(0),
|
||||
Selection::Track(t) => Selection::Clip(t, 0),
|
||||
Selection::Scene(s) if s + 1 < self.scenes.len() => Selection::Scene(s + 1),
|
||||
Selection::Scene(s) => Selection::Mix,
|
||||
Selection::Clip(t, s) if s + 1 < self.scenes.len() => Selection::Clip(t, s + 1),
|
||||
Selection::Clip(t, s) => Selection::Track(t),
|
||||
},
|
||||
":scene-prev" => match self.selected {
|
||||
Selection::Mix => Selection::Mix,
|
||||
Selection::Track(t) => Selection::Track(t),
|
||||
Selection::Scene(0) => Selection::Mix,
|
||||
Selection::Scene(s) => Selection::Scene(s - 1),
|
||||
Selection::Clip(t, 0) => Selection::Track(t),
|
||||
Selection::Clip(t, s) => Selection::Clip(t, s - 1),
|
||||
},
|
||||
":track-next" => match self.selected {
|
||||
Selection::Mix => Selection::Track(0),
|
||||
Selection::Track(t) if t + 1 < self.tracks.len() => Selection::Track(t + 1),
|
||||
Selection::Track(t) => Selection::Mix,
|
||||
Selection::Scene(s) => Selection::Clip(0, s),
|
||||
Selection::Clip(t, s) if t + 1 < self.tracks.len() => Selection::Clip(t + 1, s),
|
||||
Selection::Clip(t, s) => Selection::Scene(s),
|
||||
},
|
||||
":track-prev" => match self.selected {
|
||||
Selection::Mix => Selection::Mix,
|
||||
Selection::Scene(s) => Selection::Scene(s),
|
||||
Selection::Track(0) => Selection::Mix,
|
||||
Selection::Track(t) => Selection::Track(t - 1),
|
||||
Selection::Clip(0, s) => Selection::Scene(s),
|
||||
Selection::Clip(t, s) => Selection::Clip(t - 1, s),
|
||||
},
|
||||
});
|
||||
impl Tek {
|
||||
fn clip (&self) -> Option<Arc<RwLock<MidiClip>>> {
|
||||
self.scene()?.clips.get(self.selected().track()?)?.clone()
|
||||
}
|
||||
fn toggle_loop (&mut self) {
|
||||
if let Some(clip) = self.clip() {
|
||||
clip.write().unwrap().toggle_loop()
|
||||
}
|
||||
}
|
||||
fn activate (&mut self) -> Usually<()> {
|
||||
let selected = self.selected().clone();
|
||||
match selected {
|
||||
Selection::Scene(s) => {
|
||||
let mut clips = vec![];
|
||||
for (t, _) in self.tracks().iter().enumerate() {
|
||||
clips.push(self.scenes()[s].clips[t].clone());
|
||||
}
|
||||
for (t, track) in self.tracks_mut().iter_mut().enumerate() {
|
||||
if track.player.play_clip.is_some() || clips[t].is_some() {
|
||||
track.player.enqueue_next(clips[t].as_ref());
|
||||
}
|
||||
}
|
||||
if self.clock().is_stopped() {
|
||||
self.clock().play_from(Some(0))?;
|
||||
}
|
||||
},
|
||||
Selection::Clip(t, s) => {
|
||||
let clip = self.scenes()[s].clips[t].clone();
|
||||
self.tracks_mut()[t].player.enqueue_next(clip.as_ref());
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
#[cfg(test)] #[test] fn test_model () {
|
||||
let mut tek = Tek::default();
|
||||
let _ = tek.clip();
|
||||
let _ = tek.toggle_loop();
|
||||
let _ = tek.activate();
|
||||
}
|
||||
98
app/src/model_scene.rs
Normal file
98
app/src/model_scene.rs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
use crate::*;
|
||||
impl Tek {
|
||||
pub fn scenes_add (&mut self, n: usize) -> Usually<()> {
|
||||
let scene_color_1 = ItemColor::random();
|
||||
let scene_color_2 = ItemColor::random();
|
||||
for i in 0..n {
|
||||
let _ = self.scene_add(None, Some(
|
||||
scene_color_1.mix(scene_color_2, i as f32 / n as f32).into()
|
||||
))?;
|
||||
}
|
||||
self.redraw_arranger();
|
||||
Ok(())
|
||||
}
|
||||
pub fn scene_add (&mut self, name: Option<&str>, color: Option<ItemPalette>)
|
||||
-> Usually<(usize, &mut Scene)>
|
||||
{
|
||||
let scene = Scene {
|
||||
name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()),
|
||||
clips: vec![None;self.tracks().len()],
|
||||
color: color.unwrap_or_else(ItemPalette::random),
|
||||
};
|
||||
self.scenes_mut().push(scene);
|
||||
let index = self.scenes().len() - 1;
|
||||
Ok((index, &mut self.scenes_mut()[index]))
|
||||
}
|
||||
pub fn scene_default_name (&self) -> Arc<str> {
|
||||
format!("Sc{:3>}", self.scenes().len() + 1).into()
|
||||
}
|
||||
}
|
||||
pub trait HasScenes: HasSelection + HasEditor + Send + Sync {
|
||||
fn scenes (&self) -> &Vec<Scene>;
|
||||
fn scenes_mut (&mut self) -> &mut Vec<Scene>;
|
||||
fn scene_longest (&self) -> usize {
|
||||
self.scenes().iter().map(|s|s.name.len()).fold(0, usize::max)
|
||||
}
|
||||
fn scene (&self) -> Option<&Scene> {
|
||||
self.selected().scene().and_then(|s|self.scenes().get(s))
|
||||
}
|
||||
fn scene_mut (&mut self) -> Option<&mut Scene> {
|
||||
self.selected().scene().and_then(|s|self.scenes_mut().get_mut(s))
|
||||
}
|
||||
fn scene_del (&mut self, index: usize) {
|
||||
self.selected().scene().and_then(|s|Some(self.scenes_mut().remove(index)));
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Default)] pub struct Scene {
|
||||
/// Name of scene
|
||||
pub name: Arc<str>,
|
||||
/// Clips in scene, one per track
|
||||
pub clips: Vec<Option<Arc<RwLock<MidiClip>>>>,
|
||||
/// Identifying color of scene
|
||||
pub color: ItemPalette,
|
||||
}
|
||||
impl Scene {
|
||||
/// Returns the pulse length of the longest clip in the scene
|
||||
fn pulses (&self) -> usize {
|
||||
self.clips.iter().fold(0, |a, p|{
|
||||
a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0))
|
||||
})
|
||||
}
|
||||
/// Returns true if all clips in the scene are
|
||||
/// currently playing on the given collection of tracks.
|
||||
fn is_playing (&self, tracks: &[Track]) -> bool {
|
||||
self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate()
|
||||
.all(|(track_index, clip)|match clip {
|
||||
Some(c) => tracks
|
||||
.get(track_index)
|
||||
.map(|track|{
|
||||
if let Some((_, Some(clip))) = track.player().play_clip() {
|
||||
*clip.read().unwrap() == *c.read().unwrap()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.unwrap_or(false),
|
||||
None => true
|
||||
})
|
||||
}
|
||||
pub fn clip (&self, index: usize) -> Option<&Arc<RwLock<MidiClip>>> {
|
||||
match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None }
|
||||
}
|
||||
}
|
||||
impl HasScenes for Tek {
|
||||
fn scenes (&self) -> &Vec<Scene> { &self.scenes }
|
||||
fn scenes_mut (&mut self) -> &mut Vec<Scene> { &mut self.scenes }
|
||||
}
|
||||
#[cfg(test)] #[test] fn test_model_scene () {
|
||||
let mut app = Tek::default();
|
||||
let _ = app.scene_longest();
|
||||
let _ = app.scene();
|
||||
let _ = app.scene_mut();
|
||||
let _ = app.scene_add(None, None);
|
||||
app.scene_del(0);
|
||||
|
||||
let scene = Scene::default();
|
||||
let _ = scene.pulses();
|
||||
let _ = scene.is_playing(&[]);
|
||||
}
|
||||
51
app/src/model_select.rs
Normal file
51
app/src/model_select.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
use crate::*;
|
||||
pub trait HasSelection {
|
||||
fn selected (&self) -> &Selection;
|
||||
fn selected_mut (&mut self) -> &mut Selection;
|
||||
}
|
||||
/// Represents the current user selection in the arranger
|
||||
#[derive(PartialEq, Clone, Copy, Debug, Default)] pub enum Selection {
|
||||
/// The whole mix is selected
|
||||
#[default] Mix,
|
||||
/// A track is selected.
|
||||
Track(usize),
|
||||
/// A scene is selected.
|
||||
Scene(usize),
|
||||
/// A clip (track × scene) is selected.
|
||||
Clip(usize, usize),
|
||||
}
|
||||
/// Focus identification methods
|
||||
impl Selection {
|
||||
fn is_mix (&self) -> bool { matches!(self, Self::Mix) }
|
||||
fn is_track (&self) -> bool { matches!(self, Self::Track(_)) }
|
||||
fn is_scene (&self) -> bool { matches!(self, Self::Scene(_)) }
|
||||
fn is_clip (&self) -> bool { matches!(self, Self::Clip(_, _)) }
|
||||
pub fn track (&self) -> Option<usize> {
|
||||
use Selection::*;
|
||||
match self { Clip(t, _) => Some(*t), Track(t) => Some(*t), _ => None }
|
||||
}
|
||||
pub fn scene (&self) -> Option<usize> {
|
||||
use Selection::*;
|
||||
match self { Clip(_, s) => Some(*s), Scene(s) => Some(*s), _ => None }
|
||||
}
|
||||
pub fn describe (&self, tracks: &[Track], scenes: &[Scene]) -> Arc<str> {
|
||||
format!("{}", match self {
|
||||
Self::Mix => "Everything".to_string(),
|
||||
Self::Track(t) => tracks.get(*t).map(|track|format!("T{t}: {}", &track.name))
|
||||
.unwrap_or_else(||"T??".into()),
|
||||
Self::Scene(s) => scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name))
|
||||
.unwrap_or_else(||"S??".into()),
|
||||
Self::Clip(t, s) => match (tracks.get(*t), scenes.get(*s)) {
|
||||
(Some(_), Some(scene)) => match scene.clip(*t) {
|
||||
Some(clip) => format!("T{t} S{s} C{}", &clip.read().unwrap().name),
|
||||
None => format!("T{t} S{s}: Empty")
|
||||
},
|
||||
_ => format!("T{t} S{s}: Empty"),
|
||||
}
|
||||
}).into()
|
||||
}
|
||||
}
|
||||
impl HasSelection for Tek {
|
||||
fn selected (&self) -> &Selection { &self.selected }
|
||||
fn selected_mut (&mut self) -> &mut Selection { &mut self.selected }
|
||||
}
|
||||
104
app/src/model_track.rs
Normal file
104
app/src/model_track.rs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
use crate::*;
|
||||
impl Tek {
|
||||
pub fn tracks_add (
|
||||
&mut self, count: usize, width: Option<usize>,
|
||||
midi_from: &[PortConnect], midi_to: &[PortConnect],
|
||||
) -> Usually<()> {
|
||||
let jack = self.jack().clone();
|
||||
let track_color_1 = ItemColor::random();
|
||||
let track_color_2 = ItemColor::random();
|
||||
for i in 0..count {
|
||||
let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into();
|
||||
let mut track = self.track_add(None, Some(color), midi_from, midi_to)?.1;
|
||||
if let Some(width) = width {
|
||||
track.width = width;
|
||||
}
|
||||
}
|
||||
self.redraw_arranger();
|
||||
Ok(())
|
||||
}
|
||||
pub fn track_add (
|
||||
&mut self, name: Option<&str>, color: Option<ItemPalette>,
|
||||
midi_froms: &[PortConnect],
|
||||
midi_tos: &[PortConnect],
|
||||
) -> Usually<(usize, &mut Track)> {
|
||||
let name = name.map_or_else(||self.track_next_name(), |x|x.to_string().into());
|
||||
let mut track = Track {
|
||||
width: (name.len() + 2).max(12),
|
||||
color: color.unwrap_or_else(ItemPalette::random),
|
||||
player: MidiPlayer::new(
|
||||
&format!("{name}"),
|
||||
self.jack(),
|
||||
Some(self.clock()),
|
||||
None,
|
||||
midi_froms,
|
||||
midi_tos
|
||||
)?,
|
||||
name,
|
||||
..Default::default()
|
||||
};
|
||||
self.tracks_mut().push(track);
|
||||
let len = self.tracks().len();
|
||||
let index = len - 1;
|
||||
for scene in self.scenes_mut().iter_mut() {
|
||||
while scene.clips.len() < len {
|
||||
scene.clips.push(None);
|
||||
}
|
||||
}
|
||||
Ok((index, &mut self.tracks_mut()[index]))
|
||||
}
|
||||
pub fn track_del (&mut self, index: usize) {
|
||||
self.tracks_mut().remove(index);
|
||||
for scene in self.scenes_mut().iter_mut() {
|
||||
scene.clips.remove(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
pub trait HasTracks: HasSelection + HasClock + HasJack + HasEditor + Send + Sync {
|
||||
fn midi_ins (&self) -> &Vec<JackMidiIn>;
|
||||
fn midi_outs (&self) -> &Vec<JackMidiOut>;
|
||||
fn tracks (&self) -> &Vec<Track>;
|
||||
fn tracks_mut (&mut self) -> &mut Vec<Track>;
|
||||
fn track_longest (&self) -> usize {
|
||||
self.tracks().iter().map(|s|s.name.len()).fold(0, usize::max)
|
||||
}
|
||||
const WIDTH_OFFSET: usize = 1;
|
||||
fn track_next_name (&self) -> Arc<str> {
|
||||
format!("Track{:02}", self.tracks().len() + 1).into()
|
||||
}
|
||||
fn track (&self) -> Option<&Track> {
|
||||
self.selected().track().and_then(|s|self.tracks().get(s))
|
||||
}
|
||||
fn track_mut (&mut self) -> Option<&mut Track> {
|
||||
self.selected().track().and_then(|s|self.tracks_mut().get_mut(s))
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Default)] pub struct Track {
|
||||
/// Name of track
|
||||
pub name: Arc<str>,
|
||||
/// Preferred width of track column
|
||||
pub width: usize,
|
||||
/// Identifying color of track
|
||||
pub color: ItemPalette,
|
||||
/// MIDI player state
|
||||
pub player: MidiPlayer,
|
||||
/// Device chain
|
||||
pub devices: Vec<Box<dyn Device>>,
|
||||
/// Inputs of 1st device
|
||||
pub audio_ins: Vec<JackAudioIn>,
|
||||
/// Outputs of last device
|
||||
pub audio_outs: Vec<JackAudioOut>,
|
||||
}
|
||||
has_clock!(|self: Track|self.player.clock);
|
||||
has_player!(|self: Track|self.player);
|
||||
impl Track {
|
||||
const MIN_WIDTH: usize = 9;
|
||||
fn width_inc (&mut self) { self.width += 1; }
|
||||
fn width_dec (&mut self) { if self.width > Track::MIN_WIDTH { self.width -= 1; } }
|
||||
}
|
||||
impl HasTracks for Tek {
|
||||
fn midi_ins (&self) -> &Vec<JackMidiIn> { &self.midi_ins }
|
||||
fn midi_outs (&self) -> &Vec<JackMidiOut> { &self.midi_outs }
|
||||
fn tracks (&self) -> &Vec<Track> { &self.tracks }
|
||||
fn tracks_mut (&mut self) -> &mut Vec<Track> { &mut self.tracks }
|
||||
}
|
||||
138
app/src/view.rs
Normal file
138
app/src/view.rs
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
use crate::*;
|
||||
pub(crate) use std::fmt::Write;
|
||||
pub(crate) use ::tek_tui::ratatui::prelude::Position;
|
||||
pub(crate) trait ScenesColors<'a> = Iterator<Item=SceneWithColor<'a>>;
|
||||
pub(crate) type SceneWithColor<'a> = (usize, &'a Scene, usize, usize, Option<ItemPalette>);
|
||||
view!(TuiOut: |self: Tek| self.size.of(View(self, self.view)); {
|
||||
":arranger" => self.view_arranger().boxed(),
|
||||
":editor" => self.editor.as_ref().map(|e|Bsp::e(e.clip_status(), e.edit_status())).boxed(),
|
||||
":inputs" => self.view_inputs().boxed(),
|
||||
":outputs" => self.view_outputs().boxed(),
|
||||
":sample" => ().boxed(),//self.view_sample(self.is_editing()).boxed(),
|
||||
":sampler" => ().boxed(),//self.view_sampler(self.is_editing(), &self.editor).boxed(),
|
||||
":scene-add" => self.view_scene_add().boxed(),
|
||||
":scenes" => self.view_scenes().boxed(),
|
||||
":transport" => self.view_transport().boxed(),
|
||||
":status" => self.view_status().boxed(),
|
||||
":tracks" => self.view_tracks().boxed(),
|
||||
":pool" => self.pool.as_ref()
|
||||
.map(|pool|Fixed::x(self.w_sidebar(), PoolView(self.is_editing(), pool)))
|
||||
.boxed(),
|
||||
});
|
||||
provide_num!(u16: |self: Tek| {
|
||||
":h-ins" => self.h_inputs(),
|
||||
":h-outs" => self.h_outputs(),
|
||||
":h-sample" => if self.is_editing() { 0 } else { 5 },
|
||||
":w-samples" => if self.is_editing() { 4 } else { 11 },
|
||||
":w-sidebar" => self.w_sidebar(),
|
||||
":y-ins" => (self.size.h() as u16).saturating_sub(self.h_inputs() + 1),
|
||||
":y-outs" => (self.size.h() as u16).saturating_sub(self.h_outputs() + 1),
|
||||
":y-samples" => if self.is_editing() { 1 } else { 0 },
|
||||
});
|
||||
pub(crate) fn row <'a> (
|
||||
w: u16,
|
||||
h: u16,
|
||||
s: u16,
|
||||
a: impl Content<TuiOut> + 'a,
|
||||
b: impl Content<TuiOut> + 'a,
|
||||
c: impl Content<TuiOut> + 'a,
|
||||
) -> impl Content<TuiOut> + 'a {
|
||||
Fixed::y(h, Bsp::a(
|
||||
Fill::xy(Align::c(Fixed::x(w, Align::x(b)))),
|
||||
Bsp::a(
|
||||
Fill::xy(Align::w(Fixed::x(s, a))),
|
||||
Fill::xy(Align::e(Fixed::x(s, c))),
|
||||
),
|
||||
))
|
||||
}
|
||||
pub(crate) fn row_top <'a> (
|
||||
w: u16,
|
||||
h: u16,
|
||||
s: u16,
|
||||
a: impl Content<TuiOut> + 'a,
|
||||
b: impl Content<TuiOut> + 'a,
|
||||
c: impl Content<TuiOut> + 'a,
|
||||
) -> impl Content<TuiOut> + 'a {
|
||||
Fixed::y(h, Bsp::a(
|
||||
Fill::x(Align::n(Fixed::x(w, Align::x(Tui::bg(Reset, b))))),
|
||||
Bsp::a(
|
||||
Fill::x(Align::nw(Fixed::x(s, Tui::bg(Reset, a)))),
|
||||
Fill::x(Align::ne(Fixed::x(s, Tui::bg(Reset, c)))),
|
||||
),
|
||||
))
|
||||
}
|
||||
pub(crate) fn wrap (
|
||||
bg: Color,
|
||||
fg: Color,
|
||||
content: impl Content<TuiOut>
|
||||
) -> impl Content<TuiOut> {
|
||||
Bsp::e(Tui::fg_bg(bg, Reset, "▐"),
|
||||
Bsp::w(Tui::fg_bg(bg, Reset, "▌"),
|
||||
Tui::fg_bg(fg, bg, content)))
|
||||
}
|
||||
pub(crate) fn button_2 <'a, K, L> (
|
||||
key: K,
|
||||
label: L,
|
||||
editing: bool,
|
||||
) -> impl Content<TuiOut> + 'a where
|
||||
K: Content<TuiOut> + 'a,
|
||||
L: Content<TuiOut> + 'a,
|
||||
{
|
||||
let key = Tui::fg_bg(Tui::g(0), Tui::orange(), Bsp::e(
|
||||
Tui::fg_bg(Tui::orange(), Reset, "▐"),
|
||||
Bsp::e(key, Tui::fg(Tui::g(96), "▐"))
|
||||
));
|
||||
let label = When::new(!editing, Tui::fg_bg(Tui::g(255), Tui::g(96), label));
|
||||
Tui::bold(true, Bsp::e(key, label))
|
||||
}
|
||||
pub(crate) fn button_3 <'a, K, L, V> (
|
||||
key: K,
|
||||
label: L,
|
||||
value: V,
|
||||
editing: bool,
|
||||
) -> impl Content<TuiOut> + 'a where
|
||||
K: Content<TuiOut> + 'a,
|
||||
L: Content<TuiOut> + 'a,
|
||||
V: Content<TuiOut> + 'a,
|
||||
{
|
||||
let key = Tui::fg_bg(Tui::g(0), Tui::orange(),
|
||||
Bsp::e(Tui::fg_bg(Tui::orange(), Reset, "▐"), Bsp::e(key, Tui::fg(if editing {
|
||||
Tui::g(128)
|
||||
} else {
|
||||
Tui::g(96)
|
||||
}, "▐"))));
|
||||
let label = Bsp::e(
|
||||
When::new(!editing, Bsp::e(
|
||||
Tui::fg_bg(Tui::g(255), Tui::g(96), label),
|
||||
Tui::fg_bg(Tui::g(128), Tui::g(96), "▐"),
|
||||
)),
|
||||
Bsp::e(
|
||||
Tui::fg_bg(Tui::g(224), Tui::g(128), value),
|
||||
Tui::fg_bg(Tui::g(128), Reset, "▌"),
|
||||
));
|
||||
Tui::bold(true, Bsp::e(key, label))
|
||||
}
|
||||
fn heading <'a> (
|
||||
key: &'a str,
|
||||
label: &'a str,
|
||||
count: usize,
|
||||
content: impl Content<TuiOut> + Send + Sync + 'a,
|
||||
editing: bool,
|
||||
) -> impl Content<TuiOut> + 'a {
|
||||
let count = format!("{count}");
|
||||
Fill::xy(Align::w(Bsp::s(Fill::x(Align::w(button_3(key, label, count, editing))), content)))
|
||||
}
|
||||
#[cfg(test)] mod test {
|
||||
use super::*;
|
||||
#[test] fn test_view () {
|
||||
let _ = button_2("", "", true);
|
||||
let _ = button_2("", "", false);
|
||||
let _ = button_3("", "", "", true);
|
||||
let _ = button_3("", "", "", false);
|
||||
let _ = heading("", "", 0, "", true);
|
||||
let _ = heading("", "", 0, "", false);
|
||||
let _ = wrap(Reset, Reset, "");
|
||||
let _ = row(0, 0, 0, "", "", "");
|
||||
let _ = row_top(0, 0, 0, "", "", "");
|
||||
}
|
||||
}
|
||||
1
app/src/view_arranger.edn
Normal file
1
app/src/view_arranger.edn
Normal file
|
|
@ -0,0 +1 @@
|
|||
(bsp/n (fixed/y 2 :transport) (bsp/s (fixed/y 2 :status) (fill/xy (bsp/a (fill/xy (align/e :pool)) :arranger))))
|
||||
388
app/src/view_arranger.rs
Normal file
388
app/src/view_arranger.rs
Normal file
|
|
@ -0,0 +1,388 @@
|
|||
use crate::*;
|
||||
impl Tek {
|
||||
|
||||
// FIXME: The memoized arranger is too complex.
|
||||
// Just render a slice of the arranger for now,
|
||||
// starting from tracks[scroll_offset] and ending
|
||||
// at the last track that fits fully.
|
||||
|
||||
/// Blit the currently visible section of the arranger view buffer to the output.
|
||||
///
|
||||
/// If the arranger is larger than the available display area,
|
||||
/// the scrollbars determine the portion that will be shown.
|
||||
///
|
||||
/// This function is called on every frame, but is relatively cheap
|
||||
/// as the rendering logic of [redraw_arranger] is only invoked on
|
||||
/// changes to the content.
|
||||
pub fn view_arranger (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
Fill::xy(ThunkRender::new(move|to: &mut TuiOut|{
|
||||
let [x0, y0, w, h] = to.area().xywh();
|
||||
let source = self.arranger.read().unwrap();
|
||||
for (source_x, target_x) in (x0..x0+w).enumerate() {
|
||||
for (source_y, target_y) in (y0..y0+h).enumerate() {
|
||||
let target_pos = Position::from((target_x, target_y));
|
||||
if let Some(target) = to.buffer.cell_mut(target_pos) {
|
||||
target.set_bg(Color::Rgb(128,0,0));
|
||||
let source_pos = Position::from((source_x as u16, source_y as u16));
|
||||
if let Some(source) = source.cell(source_pos) {
|
||||
*target = source.clone();
|
||||
target.set_bg(Color::Rgb(0,128,0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/// Draw the full arranger to the arranger view buffer.
|
||||
///
|
||||
/// This should happen on changes to the arrangement contents,
|
||||
/// i.e. not on scroll. Scrolling just determines which part
|
||||
/// of the arranger buffer to blit in [view_arranger].
|
||||
pub fn redraw_arranger (&self) {
|
||||
let width = self.w_tracks();
|
||||
let height = self.h_scenes() + self.h_inputs() + self.h_outputs();
|
||||
let buffer = Buffer::empty(ratatui::prelude::Rect { x: 0, y: 0, width, height });
|
||||
let mut output = TuiOut { buffer, area: [0, 0, width, height] };
|
||||
let layout = Bsp::s(self.view_inputs(),
|
||||
Bsp::s(self.view_tracks(),
|
||||
Bsp::n(self.view_outputs(),
|
||||
self.view_scenes())));
|
||||
Content::render(&layout, &mut output);
|
||||
*self.arranger.write().unwrap() = output.buffer;
|
||||
}
|
||||
|
||||
/// Display the current scene scroll state.
|
||||
fn scene_scrollbar (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
let offset = self.scene_scroll;
|
||||
let length = self.h_tracks_area() as usize;
|
||||
let total = self.h_scenes() as usize;
|
||||
Fill::y(Fixed::x(1, ScrollbarV { offset, length, total }))
|
||||
}
|
||||
|
||||
/// Display the current track scroll state.
|
||||
fn track_scrollbar (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
let offset = self.track_scroll;
|
||||
let length = self.w_tracks_area() as usize;
|
||||
let total = self.w_tracks() as usize;
|
||||
Fill::x(Fixed::y(1, ScrollbarH { offset, length, total }))
|
||||
}
|
||||
|
||||
/// Render something centered for each track.
|
||||
fn per_track <'a, T: Content<TuiOut> + 'a> (
|
||||
&'a self, f: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
|
||||
) -> impl Content<TuiOut> + 'a {
|
||||
self.per_track_top(move|index, track|Fill::y(Align::y(f(index, track))))
|
||||
}
|
||||
|
||||
/// Render something top-aligned for each track.
|
||||
fn per_track_top <'a, T: Content<TuiOut> + 'a> (
|
||||
&'a self, f: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
|
||||
) -> impl Content<TuiOut> + 'a {
|
||||
let width = self.w_tracks_area();
|
||||
let filter = move|(t, track, x1, x2)|if x2 as u16 >= width {
|
||||
None
|
||||
} else {
|
||||
Some((t, track, x1, x2))
|
||||
};
|
||||
let tracks = move||self.tracks_sizes().map_while(filter);
|
||||
Align::x(Tui::bg(Reset, Map::new(tracks, move|(index, track, x1, x2), _|{
|
||||
let width = (x2 - x1) as u16;
|
||||
map_east(x1 as u16, width, Fixed::x(width, Tui::fg_bg(
|
||||
track.color.lightest.rgb,
|
||||
track.color.base.rgb,
|
||||
f(index, track))))
|
||||
})))
|
||||
}
|
||||
|
||||
pub fn view_tracks (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
let w = (self.size.w() as u16).saturating_sub(2 * self.w_sidebar());
|
||||
let s = self.w_sidebar() as u16;
|
||||
let data = (self.selected.track().unwrap_or(0), self.tracks().len());
|
||||
let editing = self.is_editing();
|
||||
self.fmtd.write().unwrap().trks.update(Some(data), rewrite!(buf, "{}/{}", data.0, data.1));
|
||||
row(w, 1, s, button_3("t", "track", self.fmtd.read().unwrap().trks.view.clone(), editing),
|
||||
self.per_track(|t, track|track_header(t, track, self.selected().track() == Some(t))),
|
||||
button_2("T", "add track", editing))
|
||||
}
|
||||
|
||||
pub fn view_scenes (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
let editing = self.is_editing();
|
||||
let s = self.w_sidebar() as u16;
|
||||
let w = self.w_tracks_area();
|
||||
let w_full = self.w();
|
||||
let h = self.h_scenes();
|
||||
let h_area = self.h_tracks_area();
|
||||
let selected_track = self.selected().track();
|
||||
let selected_scene = self.selected().scene();
|
||||
let track_scroll = self.track_scrollbar();
|
||||
let scene_scroll = self.scene_scrollbar();
|
||||
Tui::bg(Reset, Bsp::s(track_scroll, Bsp::e(scene_scroll, Fixed::y(h_area, row(w, h, s,
|
||||
Map::new(
|
||||
move||self.scenes_with_colors(editing, h_area),
|
||||
move|(s, scene, y1, y2, prev): SceneWithColor, _|self.view_scene_name(
|
||||
w_full, (1 + y2 - y1) as u16, y1 as u16, s, scene, prev)),
|
||||
self.per_track(move|t, track|Map::new(
|
||||
move||self.scenes_with_track_colors(editing, h_area, t),
|
||||
move|(s, scene, y1, y2, prev): SceneWithColor, _|self.view_scene_clip(
|
||||
w, (1 + y2 - y1) as u16, y1 as u16,
|
||||
scene, prev, s, t, editing, selected_track == Some(t), selected_scene))),
|
||||
() )))))
|
||||
}
|
||||
|
||||
fn view_scene_name (
|
||||
&self,
|
||||
width: u16,
|
||||
height: u16,
|
||||
offset: u16,
|
||||
index: usize,
|
||||
scene: &Scene,
|
||||
prev: Option<ItemPalette>,
|
||||
) -> impl Content<TuiOut> + use<'_> {
|
||||
Fill::x(map_south(offset, height, Fixed::y(height, scene_cell(
|
||||
index == self.scenes.len().saturating_sub(1),
|
||||
self.selected().scene(),
|
||||
true,
|
||||
index,
|
||||
&scene.color,
|
||||
prev,
|
||||
Some(scene.name.clone()),
|
||||
" ⯈ ",
|
||||
scene.color.lightest.rgb
|
||||
))))
|
||||
}
|
||||
|
||||
fn view_scene_clip (
|
||||
&self,
|
||||
width: u16,
|
||||
height: u16,
|
||||
offset: u16,
|
||||
scene: &Scene,
|
||||
prev: Option<ItemPalette>,
|
||||
scene_index: usize,
|
||||
track_index: usize,
|
||||
editing: bool,
|
||||
same_track: bool,
|
||||
selected_scene: Option<usize>
|
||||
) -> impl Content<TuiOut> + use<'_> {
|
||||
let (name, fg, bg) = if let Some(clip) = &scene.clips[track_index] {
|
||||
let clip = clip.read().unwrap();
|
||||
(Some(clip.name.clone()), clip.color.lightest.rgb, clip.color)
|
||||
} else {
|
||||
(None, Tui::g(96), ItemPalette::G[32])
|
||||
};
|
||||
let active = editing && same_track && selected_scene == Some(scene_index);
|
||||
let edit = |x|Bsp::b(x, When(active, &self.editor));
|
||||
map_south(offset, height, edit(Fixed::y(height, scene_cell(
|
||||
scene_index == self.scenes.len().saturating_sub(1),
|
||||
self.selected().scene(),
|
||||
same_track,
|
||||
scene_index,
|
||||
&bg,
|
||||
prev,
|
||||
name,
|
||||
" ⏹ ",
|
||||
fg
|
||||
))))
|
||||
}
|
||||
|
||||
pub fn view_scene_add (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
let editing = self.is_editing();
|
||||
let data = (self.selected().scene().unwrap_or(0), self.scenes().len());
|
||||
self.fmtd.write().unwrap().scns.update(Some(data), rewrite!(buf, "({}/{})", data.0, data.1));
|
||||
button_3("S", "add scene", self.fmtd.read().unwrap().scns.view.clone(), editing)
|
||||
}
|
||||
|
||||
pub fn view_outputs (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
let editing = self.is_editing();
|
||||
let w = self.w_tracks_area();
|
||||
let s = self.w_sidebar() as u16;
|
||||
let fg = Tui::g(224);
|
||||
let nexts = self.per_track_top(|t, track|Either(
|
||||
track.player.next_clip.is_some(),
|
||||
Thunk::new(||Tui::bg(Reset, format!("{:?}",
|
||||
track.player.next_clip.as_ref()
|
||||
.map(|(moment, clip)|clip.as_ref()
|
||||
.map(|clip|clip.read().unwrap().name.clone()))
|
||||
.flatten().as_ref()))),
|
||||
Thunk::new(||Tui::bg(Reset, " ------ "))));
|
||||
let nexts = row_top(w, 2, s, Align::ne("Next:"), nexts, ());
|
||||
let froms = self.per_track_top(|_, _|Tui::bg(Reset, Align::c(Bsp::s(" ------ ", OctaveVertical::default(),))));
|
||||
let froms = row_top(w, 2, s, Align::ne("From:"), froms, ());
|
||||
let ports = row_top(w, 1, s,
|
||||
button_3("o", "midi outs", format!("{}", self.midi_outs.len()), editing),
|
||||
self.per_track_top(move|t, track|{
|
||||
let mute = false;
|
||||
let solo = false;
|
||||
let mute = if mute { White } else { track.color.darkest.rgb };
|
||||
let solo = if solo { White } else { track.color.darkest.rgb };
|
||||
let bg = if self.selected().track() == Some(t) {
|
||||
track.color.light.rgb
|
||||
} else {
|
||||
track.color.base.rgb
|
||||
};
|
||||
let bg2 = if t > 0 { self.tracks()[t].color.base.rgb } else { Reset };
|
||||
wrap(bg, fg, Tui::bold(true, Fill::x(Bsp::e(
|
||||
Tui::fg_bg(mute, bg, "Play "),
|
||||
Tui::fg_bg(solo, bg, "Solo ")))))}),
|
||||
button_2("O", "add midi out", editing));
|
||||
let routes = row_top(w, self.h_outputs() - 1, s,
|
||||
io_ports(fg, Tui::g(32), ||self.outputs_sizes()),
|
||||
self.per_track_top(move|t, track|io_conns(
|
||||
track.color.dark.rgb, track.color.darker.rgb, ||self.outputs_sizes())), ());
|
||||
Align::n(Bsp::s(Bsp::s(nexts, froms), Bsp::s(ports, routes)))
|
||||
}
|
||||
|
||||
pub fn view_inputs (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
let editing = self.is_editing();
|
||||
let w = (self.size.w() as u16).saturating_sub(2 * self.w_sidebar());
|
||||
let s = self.w_sidebar() as u16;
|
||||
let fg = Tui::g(224);
|
||||
let routes = row_top(w, self.h_inputs() - 1, s,
|
||||
io_ports(fg, Tui::g(32), ||self.inputs_sizes()),
|
||||
self.per_track_top(move|t, track|io_conns(
|
||||
track.color.dark.rgb,
|
||||
track.color.darker.rgb,
|
||||
||self.inputs_sizes()
|
||||
)), ());
|
||||
let ports = row_top(w, 1, s,
|
||||
button_3("i", "midi ins", format!("{}", self.midi_ins.len()), editing),
|
||||
self.per_track_top(move|t, track|{
|
||||
let rec = track.player.recording;
|
||||
let mon = track.player.monitoring;
|
||||
let rec = if rec { White } else { track.color.darkest.rgb };
|
||||
let mon = if mon { White } else { track.color.darkest.rgb };
|
||||
let bg = if self.selected().track() == Some(t) {
|
||||
track.color.light.rgb
|
||||
} else {
|
||||
track.color.base.rgb
|
||||
};
|
||||
let bg2 = if t > 0 { self.tracks()[t - 1].color.base.rgb } else { Reset };
|
||||
wrap(bg, fg, Tui::bold(true, Fill::x(Bsp::e(
|
||||
Tui::fg_bg(rec, bg, "Rec "),
|
||||
Tui::fg_bg(mon, bg, "Mon ")))))
|
||||
}),
|
||||
button_2("I", "add midi in", editing));
|
||||
Bsp::s(
|
||||
Bsp::s(routes, ports),
|
||||
row_top(w, 2, s,
|
||||
Bsp::s(Align::e("Input:"), Align::e("Into:")),
|
||||
self.per_track_top(|_, _|Tui::bg(Reset, Align::c(Bsp::s(
|
||||
OctaveVertical::default(),
|
||||
" ------ ")))), ())
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fn scene_cell <'a> (
|
||||
is_last: bool,
|
||||
selected_scene: Option<usize>,
|
||||
same_track: bool,
|
||||
scene: usize,
|
||||
color: &ItemPalette,
|
||||
prev: Option<ItemPalette>,
|
||||
name: Option<Arc<str>>,
|
||||
icon: &'a str,
|
||||
fg: Color,
|
||||
) -> impl Content<TuiOut> + use<'a> {
|
||||
Phat {
|
||||
width: 0,
|
||||
height: 0,
|
||||
content: Fill::x(Align::w(Tui::bold(true, Bsp::e(icon, name)))),
|
||||
colors: Tek::colors(
|
||||
color,
|
||||
prev,
|
||||
same_track && selected_scene == Some(scene),
|
||||
same_track && scene > 0 && selected_scene == Some(scene - 1),
|
||||
is_last
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn track_header <'a> (t: usize, track: &'a Track, active: bool) -> impl Content<TuiOut> + use<'a> {
|
||||
let name = &track.name;
|
||||
let fg = track.color.lightest.rgb;
|
||||
let bg = if active { track.color.light.rgb } else { track.color.base.rgb };
|
||||
let bg2 = Reset;//if t > 0 { self.tracks()[t - 1].color.base.rgb } else { Reset };
|
||||
let bfg = if active { Rgb(255,255,255) } else { Rgb(0,0,0) };
|
||||
wrap(bg, fg, Tui::bold(true, Fill::x(Align::nw(name))))
|
||||
}
|
||||
|
||||
fn io_ports <'a, T: PortsSizes<'a>> (
|
||||
fg: Color,
|
||||
bg: Color,
|
||||
iter: impl Fn()->T + Send + Sync + 'a
|
||||
) -> impl Content<TuiOut> + 'a {
|
||||
Map::new(iter,
|
||||
move|(index, name, connections, y, y2), _|map_south(y as u16, (y2-y) as u16, Bsp::s(
|
||||
Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(Bsp::e(" ", name))))),
|
||||
Map::new(||connections.iter(), move|connect, index|map_south(index as u16, 1,
|
||||
Fill::x(Align::w(Tui::bold(false, Tui::fg_bg(fg, bg,
|
||||
&connect.info)))))))))
|
||||
}
|
||||
|
||||
fn io_conns <'a, T: PortsSizes<'a>> (
|
||||
fg: Color,
|
||||
bg: Color,
|
||||
iter: impl Fn()->T + Send + Sync + 'a
|
||||
) -> impl Content<TuiOut> + 'a {
|
||||
Map::new(iter,
|
||||
move|(index, name, connections, y, y2), _|map_south(y as u16, (y2-y) as u16, Bsp::s(
|
||||
Fill::x(Tui::bold(true, wrap(bg, fg, Fill::x(Align::w("▞▞▞▞ ▞▞▞▞"))))),
|
||||
Map::new(||connections.iter(), move|connect, index|map_south(index as u16, 1,
|
||||
Fill::x(Align::w(Tui::bold(false, wrap(bg, fg, Fill::x(""))))))))))
|
||||
}
|
||||
|
||||
#[cfg(test)] mod test {
|
||||
use super::*;
|
||||
#[test] fn test_view_arranger () {
|
||||
let mut output = TuiOut::default();
|
||||
output.area[2] = 9;
|
||||
output.area[3] = 9;
|
||||
|
||||
let mut app = Tek::default();
|
||||
app.editor = Some(Default::default());
|
||||
app.scenes_add(5);
|
||||
app.tracks_add(5, Some(5), &[], &[]);
|
||||
|
||||
Content::render(&io_ports(Reset, Reset, ||app.inputs_sizes()), &mut output);
|
||||
Content::render(&io_conns(Reset, Reset, ||app.outputs_sizes()), &mut output);
|
||||
Content::render(&app.per_track(|_, _|()), &mut output);
|
||||
Content::render(&app.per_track_top(|_, _|()), &mut output);
|
||||
|
||||
app.redraw_arranger();
|
||||
Content::render(&app.view_arranger(), &mut output);
|
||||
Content::render(&app.view_inputs(), &mut output);
|
||||
Content::render(&app.view_outputs(), &mut output);
|
||||
Content::render(&app.view_scenes(), &mut output);
|
||||
|
||||
Content::render(
|
||||
&app.view_scene_name(0, 0, 0, 0, &Default::default(), None),
|
||||
&mut output);
|
||||
|
||||
Content::render(
|
||||
&app.view_scene_clip(0, 0, 0, &{
|
||||
let mut scene: Scene = Default::default();
|
||||
scene.clips.push(Some(Default::default()));
|
||||
scene
|
||||
}, None, 0, 0, false, false, None),
|
||||
&mut output);
|
||||
|
||||
Content::render(
|
||||
&track_header(0, &Default::default(), true),
|
||||
&mut output);
|
||||
|
||||
Content::render(&scene_cell(
|
||||
false,
|
||||
None,
|
||||
false,
|
||||
0,
|
||||
&Default::default(),
|
||||
None,
|
||||
None,
|
||||
&"",
|
||||
Default::default(),
|
||||
), &mut output);
|
||||
}
|
||||
}
|
||||
84
app/src/view_clock.rs
Normal file
84
app/src/view_clock.rs
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
use crate::*;
|
||||
impl Tek {
|
||||
fn update_clock (&self) {
|
||||
let compact = self.size.w() > 80;
|
||||
let clock = self.clock();
|
||||
let rate = clock.timebase.sr.get();
|
||||
let chunk = clock.chunk.load(Relaxed) as f64;
|
||||
let lat = chunk / rate * 1000.;
|
||||
let delta = |start: &Moment|clock.global.usec.get() - start.usec.get();
|
||||
let mut fmtd = self.fmtd.write().unwrap();
|
||||
fmtd.buf.update(Some(chunk), rewrite!(buf, "{chunk}"));
|
||||
fmtd.lat.update(Some(lat), rewrite!(buf, "{lat:.1}ms"));
|
||||
fmtd.sr.update(Some((compact, rate)), |buf,_,_|if compact {
|
||||
buf.clear(); write!(buf, "{:.1}kHz", rate / 1000.)
|
||||
} else {
|
||||
buf.clear(); write!(buf, "{:.0}Hz", rate)
|
||||
});
|
||||
if let Some(now) = clock.started.read().unwrap().as_ref().map(delta) {
|
||||
let pulse = clock.timebase.usecs_to_pulse(now);
|
||||
let time = now/1000000.;
|
||||
let bpm = clock.timebase.bpm.get();
|
||||
fmtd.beat.update(Some(pulse),
|
||||
|buf, _, _|{buf.clear();clock.timebase.format_beats_1_to(buf, pulse)});
|
||||
fmtd.time.update(Some(time), rewrite!(buf, "{:.3}s", time));
|
||||
fmtd.bpm.update(Some(bpm), rewrite!(buf, "{:.3}", bpm));
|
||||
} else {
|
||||
fmtd.beat.update(None, rewrite!(buf, "-.-.--"));
|
||||
fmtd.time.update(None, rewrite!(buf, "-.---s"));
|
||||
fmtd.bpm.update(None, rewrite!(buf, "---.---"));
|
||||
}
|
||||
}
|
||||
pub(crate) fn view_status (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
self.update_clock();
|
||||
let theme = ItemPalette::G[96];
|
||||
let fmtd = self.fmtd.read().unwrap();
|
||||
Tui::bg(Black, row!(Bsp::a(
|
||||
Fill::xy(Align::w(button_play_pause(self.clock.is_rolling()))),
|
||||
Fill::xy(Align::e(row!(
|
||||
FieldH(theme, "BPM", fmtd.bpm.view.clone()),
|
||||
FieldH(theme, "Beat", fmtd.beat.view.clone()),
|
||||
FieldH(theme, "Time", fmtd.time.view.clone())
|
||||
)))
|
||||
)))
|
||||
}
|
||||
pub(crate) fn view_transport (&self) -> impl Content<TuiOut> + use<'_> {
|
||||
self.update_clock();
|
||||
let theme = ItemPalette::G[96];
|
||||
let fmtd = self.fmtd.read().unwrap();
|
||||
Tui::bg(Black, row!(Bsp::a(
|
||||
Fill::xy(Align::w(
|
||||
FieldH(theme, "Selected", self.selected.describe(&self.tracks, &self.scenes))
|
||||
)),
|
||||
Fill::xy(Align::e(row!(
|
||||
FieldH(theme, "SR", fmtd.sr.view.clone()),
|
||||
FieldH(theme, "Buf", fmtd.buf.view.clone()),
|
||||
FieldH(theme, "Lat", fmtd.lat.view.clone()),
|
||||
)))
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
fn button_play_pause (playing: bool) -> impl Content<TuiOut> {
|
||||
let compact = true;//self.is_editing();
|
||||
Tui::bg(
|
||||
if playing{Rgb(0,128,0)}else{Rgb(128,64,0)},
|
||||
Either::new(compact,
|
||||
Thunk::new(move||Fixed::x(9, Either::new(playing,
|
||||
Tui::fg(Rgb(0, 255, 0), " PLAYING "),
|
||||
Tui::fg(Rgb(255, 128, 0), " STOPPED ")))),
|
||||
Thunk::new(move||Fixed::x(5, Either::new(playing,
|
||||
Tui::fg(Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)),
|
||||
Tui::fg(Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",)))))))
|
||||
}
|
||||
|
||||
#[cfg(test)] mod test {
|
||||
use super::*;
|
||||
#[test] fn test_view_clock () {
|
||||
let _ = button_play_pause(true);
|
||||
let mut app = Tek::default();
|
||||
let _ = app.view_transport();
|
||||
let _ = app.view_status();
|
||||
let _ = app.update_clock();
|
||||
}
|
||||
}
|
||||
22
app/src/view_color.rs
Normal file
22
app/src/view_color.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
use crate::*;
|
||||
impl Tek {
|
||||
pub(crate) fn colors (
|
||||
theme: &ItemPalette,
|
||||
prev: Option<ItemPalette>,
|
||||
selected: bool,
|
||||
neighbor: bool,
|
||||
is_last: bool,
|
||||
) -> [Color;4] {
|
||||
let fg = theme.lightest.rgb;
|
||||
let bg = if selected { theme.light } else { theme.base }.rgb;
|
||||
let hi = Self::color_hi(prev, neighbor);
|
||||
let lo = Self::color_lo(theme, is_last, selected);
|
||||
[fg, bg, hi, lo]
|
||||
}
|
||||
pub(crate) fn color_hi (prev: Option<ItemPalette>, neighbor: bool) -> Color {
|
||||
prev.map(|prev|if neighbor { prev.light.rgb } else { prev.base.rgb }).unwrap_or(Reset)
|
||||
}
|
||||
pub(crate) fn color_lo (theme: &ItemPalette, is_last: bool, selected: bool) -> Color {
|
||||
if is_last { Reset } else if selected { theme.light.rgb } else { theme.base.rgb }
|
||||
}
|
||||
}
|
||||
6
app/src/view_groovebox.edn
Normal file
6
app/src/view_groovebox.edn
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
(bsp/s (fixed/y 2 :toolbar)
|
||||
(bsp/s :sample (bsp/n
|
||||
(fixed/y 2 :status)
|
||||
(bsp/w
|
||||
(fixed/x :pool-w :pool)
|
||||
(bsp/e :sampler :editor)))))
|
||||
88
app/src/view_iter.rs
Normal file
88
app/src/view_iter.rs
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
use crate::*;
|
||||
impl Tek {
|
||||
pub(crate) fn inputs_sizes (&self) -> impl PortsSizes<'_> {
|
||||
let mut y = 0;
|
||||
self.midi_ins.iter().enumerate().map(move|(i, input)|{
|
||||
let height = 1 + input.conn().len();
|
||||
let data = (i, input.name(), input.conn(), y, y + height);
|
||||
y += height;
|
||||
data
|
||||
})
|
||||
}
|
||||
pub(crate) fn outputs_sizes (&self) -> impl PortsSizes<'_> {
|
||||
let mut y = 0;
|
||||
self.midi_outs.iter().enumerate().map(move|(i, output)|{
|
||||
let height = 1 + output.conn().len();
|
||||
let data = (i, output.name(), output.conn(), y, y + height);
|
||||
y += height;
|
||||
data
|
||||
})
|
||||
}
|
||||
pub(crate) fn tracks_sizes <'a> (&'a self) -> impl TracksSizes<'a> {
|
||||
let editing = self.is_editing();
|
||||
let bigger = self.editor_w();
|
||||
let mut x = 0;
|
||||
let active = match self.selected() {
|
||||
Selection::Track(t) if editing => Some(t),
|
||||
Selection::Clip(t, _) if editing => Some(t),
|
||||
_ => None
|
||||
};
|
||||
self.tracks().iter().enumerate().map(move |(index, track)|{
|
||||
let width = if Some(index) == active.copied() { bigger } else { track.width.max(8) };
|
||||
let data = (index, track, x, x + width);
|
||||
x += width + Self::TRACK_SPACING;
|
||||
data
|
||||
})
|
||||
}
|
||||
pub(crate) fn scenes_sizes (&self, editing: bool, height: usize, larger: usize) -> impl ScenesSizes<'_> {
|
||||
let (selected_track, selected_scene) = match self.selected() {
|
||||
Selection::Track(t) => (Some(*t), None),
|
||||
Selection::Scene(s) => (None, Some(*s)),
|
||||
Selection::Clip(t, s) => (Some(*t), Some(*s)),
|
||||
_ => (None, None)
|
||||
};
|
||||
let mut y = 0;
|
||||
self.scenes().iter().enumerate().map(move|(s, scene)|{
|
||||
let active = editing && selected_track.is_some() && selected_scene == Some(s);
|
||||
let height = if active { larger } else { height };
|
||||
let data = (s, scene, y, y + height);
|
||||
y += height;
|
||||
data
|
||||
})
|
||||
}
|
||||
pub(crate) fn scenes_with_colors (&self, editing: bool, h: u16) -> impl ScenesColors<'_> {
|
||||
self.scenes_sizes(editing, Self::H_SCENE, Self::H_EDITOR).map_while(
|
||||
move|(s, scene, y1, y2)|if y2 as u16 > h {
|
||||
None
|
||||
} else { Some((s, scene, y1, y2, if s == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(self.scenes[s-1].color)
|
||||
}))
|
||||
})
|
||||
}
|
||||
pub(crate) fn scenes_with_track_colors (&self, editing: bool, h: u16, t: usize) -> impl ScenesColors<'_> {
|
||||
self.scenes_sizes(editing, Self::H_SCENE, Self::H_EDITOR).map_while(
|
||||
move|(s, scene, y1, y2)|if y2 as u16 > h {
|
||||
None
|
||||
} else { Some((s, scene, y1, y2, if s == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(self.scenes[s-1].clips[t].as_ref()
|
||||
.map(|c|c.read().unwrap().color)
|
||||
.unwrap_or(ItemPalette::G[32]))
|
||||
}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)] #[test] fn test_view_iter () {
|
||||
let mut tek = Tek::default();
|
||||
tek.editor = Some(Default::default());
|
||||
let _: Vec<_> = tek.inputs_sizes().collect();
|
||||
let _: Vec<_> = tek.outputs_sizes().collect();
|
||||
let _: Vec<_> = tek.tracks_sizes().collect();
|
||||
let _: Vec<_> = tek.scenes_sizes(true, 10, 10).collect();
|
||||
let _: Vec<_> = tek.scenes_with_colors(true, 10).collect();
|
||||
let _: Vec<_> = tek.scenes_with_track_colors(true, 10, 10).collect();
|
||||
}
|
||||
51
app/src/view_memo.rs
Normal file
51
app/src/view_memo.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
use crate::*;
|
||||
|
||||
/// Clear a pre-allocated buffer, then write into it.
|
||||
#[macro_export] macro_rules! rewrite {
|
||||
($buf:ident, $($rest:tt)*) => { |$buf,_,_|{$buf.clear();write!($buf, $($rest)*)} } }
|
||||
|
||||
#[derive(Debug, Default)] pub(crate) struct ViewMemo<T, U> {
|
||||
pub(crate) value: T,
|
||||
pub(crate) view: Arc<RwLock<U>>
|
||||
}
|
||||
impl<T: PartialEq, U> ViewMemo<T, U> {
|
||||
fn new (value: T, view: U) -> Self {
|
||||
Self { value, view: Arc::new(view.into()) }
|
||||
}
|
||||
pub(crate) fn update <R> (&mut self, newval: T, render: impl Fn(&mut U, &T, &T)->R) -> Option<R> {
|
||||
if newval != self.value {
|
||||
let result = render(&mut*self.view.write().unwrap(), &newval, &self.value);
|
||||
self.value = newval;
|
||||
return Some(result);
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
#[derive(Debug)] pub(crate) struct ViewCache {
|
||||
pub(crate) sr: ViewMemo<Option<(bool, f64)>, String>,
|
||||
pub(crate) buf: ViewMemo<Option<f64>, String>,
|
||||
pub(crate) lat: ViewMemo<Option<f64>, String>,
|
||||
pub(crate) bpm: ViewMemo<Option<f64>, String>,
|
||||
pub(crate) beat: ViewMemo<Option<f64>, String>,
|
||||
pub(crate) time: ViewMemo<Option<f64>, String>,
|
||||
pub(crate) scns: ViewMemo<Option<(usize, usize)>, String>,
|
||||
pub(crate) trks: ViewMemo<Option<(usize, usize)>, String>,
|
||||
pub(crate) stop: Arc<str>,
|
||||
pub(crate) edit: Arc<str>,
|
||||
}
|
||||
impl Default for ViewCache {
|
||||
fn default () -> Self {
|
||||
Self {
|
||||
beat: ViewMemo::new(None, String::with_capacity(16)),
|
||||
time: ViewMemo::new(None, String::with_capacity(16)),
|
||||
bpm: ViewMemo::new(None, String::with_capacity(16)),
|
||||
sr: ViewMemo::new(None, String::with_capacity(16)),
|
||||
buf: ViewMemo::new(None, String::with_capacity(16)),
|
||||
lat: ViewMemo::new(None, String::with_capacity(16)),
|
||||
scns: ViewMemo::new(None, String::with_capacity(16)),
|
||||
trks: ViewMemo::new(None, String::with_capacity(16)),
|
||||
stop: "⏹".into(),
|
||||
edit: "edit".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
48
app/src/view_meter.rs
Normal file
48
app/src/view_meter.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
use crate::*;
|
||||
fn view_meter <'a> (label: &'a str, value: f32) -> impl Content<TuiOut> + 'a {
|
||||
col!(
|
||||
FieldH(ItemPalette::G[128], label, format!("{:>+9.3}", value)),
|
||||
Fixed::xy(if value >= 0.0 { 13 }
|
||||
else if value >= -1.0 { 12 }
|
||||
else if value >= -2.0 { 11 }
|
||||
else if value >= -3.0 { 10 }
|
||||
else if value >= -4.0 { 9 }
|
||||
else if value >= -6.0 { 8 }
|
||||
else if value >= -9.0 { 7 }
|
||||
else if value >= -12.0 { 6 }
|
||||
else if value >= -15.0 { 5 }
|
||||
else if value >= -20.0 { 4 }
|
||||
else if value >= -25.0 { 3 }
|
||||
else if value >= -30.0 { 2 }
|
||||
else if value >= -40.0 { 1 }
|
||||
else { 0 }, 1, Tui::bg(if value >= 0.0 { Red }
|
||||
else if value >= -3.0 { Yellow }
|
||||
else { Green }, ())))
|
||||
}
|
||||
fn view_meters (values: &[f32;2]) -> impl Content<TuiOut> + use<'_> {
|
||||
Bsp::s(
|
||||
format!("L/{:>+9.3}", values[0]),
|
||||
format!("R/{:>+9.3}", values[1]),
|
||||
)
|
||||
}
|
||||
#[cfg(test)] mod test_view_meter {
|
||||
use super::*;
|
||||
use proptest::prelude::*;
|
||||
#[test] fn test_view_meter () {
|
||||
let _ = view_meter("", 0.0);
|
||||
let _ = view_meters(&[0.0, 0.0]);
|
||||
}
|
||||
proptest! {
|
||||
#[test] fn proptest_view_meter (
|
||||
label in "\\PC*", value in f32::MIN..f32::MAX
|
||||
) {
|
||||
let _ = view_meter(&label, value);
|
||||
}
|
||||
#[test] fn proptest_view_meters (
|
||||
value1 in f32::MIN..f32::MAX,
|
||||
value2 in f32::MIN..f32::MAX
|
||||
) {
|
||||
let _ = view_meters(&[value1, value2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
4
app/src/view_sequencer.edn
Normal file
4
app/src/view_sequencer.edn
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
(bsp/s (fixed/y 2 :toolbar)
|
||||
(bsp/n (fixed/y 2 :status) (bsp/w
|
||||
(fixed/x :pool-w :pool)
|
||||
:editor)))
|
||||
73
app/src/view_sizes.rs
Normal file
73
app/src/view_sizes.rs
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
use crate::*;
|
||||
|
||||
/// Define a type alias for iterators of sized items (columns).
|
||||
macro_rules! def_sizes_iter {
|
||||
($Type:ident => $($Item:ty),+) => {
|
||||
pub(crate) trait $Type<'a> =
|
||||
Iterator<Item=(usize, $(&'a $Item,)+ usize, usize)> + Send + Sync + 'a;}}
|
||||
|
||||
def_sizes_iter!(ScenesSizes => Scene);
|
||||
def_sizes_iter!(TracksSizes => Track);
|
||||
def_sizes_iter!(InputsSizes => JackMidiIn);
|
||||
def_sizes_iter!(OutputsSizes => JackMidiOut);
|
||||
def_sizes_iter!(PortsSizes => Arc<str>, [PortConnect]);
|
||||
|
||||
impl Tek {
|
||||
/// Spacing between tracks.
|
||||
pub(crate) const TRACK_SPACING: usize = 0;
|
||||
/// Default scene height.
|
||||
pub(crate) const H_SCENE: usize = 2;
|
||||
/// Default editor height.
|
||||
pub(crate) const H_EDITOR: usize = 15;
|
||||
|
||||
/// Width of display
|
||||
pub(crate) fn w (&self) -> u16 {
|
||||
self.size.w() as u16
|
||||
}
|
||||
pub(crate) fn w_sidebar (&self) -> u16 {
|
||||
self.w() / if self.is_editing() { 16 } else { 8 } as u16
|
||||
}
|
||||
/// Width taken by all tracks.
|
||||
pub(crate) fn w_tracks (&self) -> u16 {
|
||||
self.tracks_sizes().last().map(|(_, _, _, x)|x as u16).unwrap_or(0)
|
||||
}
|
||||
/// Width available to display tracks.
|
||||
pub(crate) fn w_tracks_area (&self) -> u16 {
|
||||
self.w().saturating_sub(2 * self.w_sidebar())
|
||||
}
|
||||
/// Height of display
|
||||
pub(crate) fn h (&self) -> u16 {
|
||||
self.size.h() as u16
|
||||
}
|
||||
/// Height available to display tracks.
|
||||
pub(crate) fn h_tracks_area (&self) -> u16 {
|
||||
self.h().saturating_sub(self.h_inputs() + self.h_outputs() + 10)
|
||||
}
|
||||
/// Height taken by all inputs.
|
||||
pub(crate) fn h_inputs (&self) -> u16 {
|
||||
1 + self.inputs_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0)
|
||||
}
|
||||
/// Height taken by all outputs.
|
||||
pub(crate) fn h_outputs (&self) -> u16 {
|
||||
1 + self.outputs_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0)
|
||||
}
|
||||
/// Height taken by all scenes.
|
||||
pub(crate) fn h_scenes (&self) -> u16 {
|
||||
self.scenes_sizes(self.is_editing(), Self::H_SCENE, Self::H_EDITOR).last()
|
||||
.map(|(_, _, _, y)|y as u16).unwrap_or(0)
|
||||
}
|
||||
}
|
||||
#[cfg(test)] mod test {
|
||||
use super::*;
|
||||
#[test] fn test_view_size () {
|
||||
let app = Tek::default();
|
||||
let _ = app.w();
|
||||
let _ = app.w_sidebar();
|
||||
let _ = app.w_tracks_area();
|
||||
let _ = app.h();
|
||||
let _ = app.h_tracks_area();
|
||||
let _ = app.h_inputs();
|
||||
let _ = app.h_outputs();
|
||||
let _ = app.h_scenes();
|
||||
}
|
||||
}
|
||||
0
app/src/view_transport.edn
Normal file
0
app/src/view_transport.edn
Normal file
Loading…
Add table
Add a link
Reference in a new issue