break down tek/src/lib.rs into lib,model,view,keys,cli,audio

This commit is contained in:
🪞👃🪞 2025-01-21 15:29:05 +01:00
parent 751e7d2160
commit 415dc444ea
7 changed files with 1334 additions and 1303 deletions

View file

@ -10,7 +10,7 @@ pub(crate) use self::ParseError::*;
pub(crate) use konst::iter::{ConstIntoIter, IsIteratorKind}; pub(crate) use konst::iter::{ConstIntoIter, IsIteratorKind};
pub(crate) use konst::string::{split_at, str_range, char_indices}; pub(crate) use konst::string::{split_at, str_range, char_indices};
pub(crate) use std::error::Error; pub(crate) use std::error::Error;
pub(crate) use std::fmt::{Debug, Display, Formatter, Result as FormatResult, Error as FormatError}; pub(crate) use std::fmt::{Debug, Display, Formatter, Result as FormatResult};
/// Static iteration helper. /// Static iteration helper.
#[macro_export] macro_rules! iterate { #[macro_export] macro_rules! iterate {
($expr:expr => $arg: pat => $body:expr) => { ($expr:expr => $arg: pat => $body:expr) => {

71
tek/src/audio.rs Normal file
View file

@ -0,0 +1,71 @@
use crate::*;
has_jack!(|self: Tek|&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
});

182
tek/src/cli.rs Normal file
View file

@ -0,0 +1,182 @@
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 = 1)] scenes: usize,
/// Number of tracks
#[arg(short = 'x', long, default_value_t = 1)] tracks: usize,
/// Width of tracks
#[arg(short = 'w', long, default_value_t = 9)] 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 = JackConnection::new(name)?;
let engine = Tui::new()?;
let empty = &[] as &[&str];
let midi_froms = PortConnection::collect(&self.midi_from, &self.midi_from_re, empty);
let midi_tos = PortConnection::collect(&self.midi_to, &self.midi_to_re, empty);
let left_froms = PortConnection::collect(&self.left_from, empty, empty);
let left_tos = PortConnection::collect(&self.left_to, empty, empty);
let right_froms = PortConnection::collect(&self.right_from, empty, empty);
let right_tos = PortConnection::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() ];
engine.run(&jack.activate_with(|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!()
})?)
}
}
impl Tek {
pub fn new_clock (
jack: &Arc<RwLock<JackConnection>>,
bpm: Option<f64>, sync_lead: bool, sync_follow: bool,
midi_froms: &[PortConnection], midi_tos: &[PortConnection],
) -> 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: vec![JackPort::<MidiIn>::new(jack, "GlobalI", midi_froms)?],
midi_outs: vec![JackPort::<MidiOut>::new(jack, "GlobalO", midi_tos)?],
keys: SourceIter(KEYS_APP),
keys_clip: SourceIter(KEYS_CLIP),
keys_track: SourceIter(KEYS_TRACK),
keys_scene: SourceIter(KEYS_SCENE),
keys_mix: SourceIter(KEYS_MIX),
fmtd_beat: Arc::new(RwLock::new(String::with_capacity(16))),
fmtd_time: Arc::new(RwLock::new(String::with_capacity(16))),
fmtd_bpm: Arc::new(RwLock::new(String::with_capacity(16))),
fmtd_sr: Arc::new(RwLock::new(String::with_capacity(16))),
fmtd_buf: Arc::new(RwLock::new(String::with_capacity(16))),
fmtd_lat: Arc::new(RwLock::new(String::with_capacity(16))),
fmtd_stop: "".into(),
..Default::default()
};
tek.sync_lead(sync_lead);
tek.sync_follow(sync_follow);
Ok(tek)
}
pub fn new_sequencer (
jack: &Arc<RwLock<JackConnection>>,
bpm: Option<f64>, sync_lead: bool, sync_follow: bool,
midi_froms: &[PortConnection], midi_tos: &[PortConnection],
) -> Usually<Self> {
let clip = MidiClip::new("Clip", true, 384usize, None, Some(ItemColor::random().into()));
let clip = Arc::new(RwLock::new(clip));
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],
player: Some(MidiPlayer::new(&jack, "sequencer", Some(&clip), &midi_froms, &midi_tos)?),
..Self::new_clock(jack, bpm, sync_lead, sync_follow, midi_froms, midi_tos)?
})
}
pub fn new_groovebox (
jack: &Arc<RwLock<JackConnection>>,
bpm: Option<f64>, sync_lead: bool, sync_follow: bool,
midi_froms: &[PortConnection], midi_tos: &[PortConnection],
audio_froms: &[&[PortConnection];2], audio_tos: &[&[PortConnection];2],
) -> Usually<Self> {
let app = Self {
view: SourceIter(include_str!("./view_groovebox.edn")),
sampler: Some(Sampler::new(jack, &"sampler", midi_froms, audio_froms, audio_tos)?),
..Self::new_sequencer(jack, bpm, sync_lead, sync_follow, midi_froms, midi_tos)?
};
if let Some(sampler) = app.sampler.as_ref().unwrap().midi_in.as_ref() {
jack.connect_ports(&app.player.as_ref().unwrap().midi_outs[0].port, &sampler.port)?;
}
Ok(app)
}
pub fn new_arranger (
jack: &Arc<RwLock<JackConnection>>,
bpm: Option<f64>, sync_lead: bool, sync_follow: bool,
midi_froms: &[PortConnection], midi_tos: &[PortConnection],
audio_froms: &[&[PortConnection];2], audio_tos: &[&[PortConnection];2],
scenes: usize, tracks: usize, track_width: usize,
) -> Usually<Self> {
let mut arranger = Self {
view: SourceIter(include_str!("./view_arranger.edn")),
..Self::new_groovebox(
jack, bpm, sync_lead, sync_follow,
midi_froms, midi_tos, audio_froms, audio_tos,
)?
};
arranger.scenes_add(scenes);
arranger.tracks_add(tracks, track_width, midi_froms, midi_tos);
arranger.selected = Selection::Clip(1, 1);
Ok(arranger)
}
}
#[cfg(test)] fn test_tek_cli () {
use clap::CommandFactory;
TekCli::command().debug_assert();
}

308
tek/src/keys.rs Normal file
View file

@ -0,0 +1,308 @@
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");
pub struct Keymaps {
}
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),
Pool(PoolCommand),
Sampler(SamplerCommand),
Scene(SceneCommand),
Select(Selection),
StopAll,
Track(TrackCommand),
Zoom(Option<usize>),
}
atom_command!(TekCommand: |app: Tek| {
("stop" [] Self::StopAll)
("undo" [d: usize] Self::History(-(d.unwrap_or(0)as isize)))
("redo" [d: usize] Self::History(d.unwrap_or(0) as isize))
("zoom" [z: usize] Self::Zoom(z))
("edit" [] Self::Edit(None))
("edit" [c: bool] Self::Edit(c))
("color" [c: Color] Self::Color(c.map(ItemPalette::from).unwrap_or_default()))
("enqueue" [c: Arc<RwLock<MidiClip>>] Self::Enqueue(c))
("select" [t: usize, s: usize] 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)),
})
("clip" [,..a] Self::Clip(
ClipCommand::try_from_expr(app, a).expect("invalid command")))
("clock" [,..a] Self::Clock(
ClockCommand::try_from_expr(app.clock(), a).expect("invalid command")))
("editor" [,..a] Self::Editor(
MidiEditCommand::try_from_expr(app.editor.as_ref().expect("no editor"), a).expect("invalid command")))
("pool" [,..a] Self::Pool(
PoolCommand::try_from_expr(app.pool.as_ref().expect("no pool"), a).expect("invalid command")))
("sampler" [,..a] Self::Sampler(
SamplerCommand::try_from_expr(app.sampler.as_ref().expect("no sampler"), a).expect("invalid command")))
("scene" [,..a] Self::Scene(
SceneCommand::try_from_expr(app, a).expect("invalid command")))
("track" [,..a] Self::Track(
TrackCommand::try_from_expr(app, a).expect("invalid command")))
});
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.saturating_sub(1)) {
if let Some(slot) = scene.clips.get_mut(t.saturating_sub(1)) {
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.saturating_sub(1)].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::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::Color(palette) => {
use Selection::*;
Some(Self::Color(match app.selected {
Mix => {
let old = app.color;
app.color = palette;
old
},
Track(t) => {
let t = t.saturating_sub(1);
let old = app.tracks[t].color;
app.tracks[t].color = palette;
old
}
Scene(s) => {
let s = s.saturating_sub(1);
let old = app.scenes[s].color;
app.scenes[s].color = palette;
old
}
Clip(t, s) => {
let t = t.saturating_sub(1);
let s = s.saturating_sub(1);
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
},
});
#[derive(Clone, Debug)] pub enum TrackCommand {
Add,
Del(usize),
Stop(usize),
Swap(usize, usize),
SetSize(usize),
SetZoom(usize),
SetColor(usize, ItemPalette),
}
atom_command!(TrackCommand: |app: Tek| {
("add" [] Self::Add)
("size" [a: usize] Self::SetSize(a.unwrap()))
("zoom" [a: usize] Self::SetZoom(a.unwrap()))
("color" [a: usize] Self::SetColor(a.unwrap().saturating_sub(1), ItemPalette::random()))
("del" [a: usize] Self::Del(a.unwrap().saturating_sub(1)))
("stop" [a: usize] Self::Stop(a.unwrap().saturating_sub(1)))
("swap" [a: usize, b: usize] Self::Swap(a.unwrap(), b.unwrap()))
});
command!(|self: TrackCommand, app: Tek|match self {
Self::Add => {
use Selection::*;
let index = app.track_add(None, None, &[], &[])?.0 + 1;
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))
},
_ => None
});
#[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" [] Self::Add)
("del" [a: usize] Self::Del(0))
("zoom" [a: usize] Self::SetZoom(a.unwrap()))
("color" [a: usize] Self::SetColor(a.unwrap().saturating_sub(1), ItemPalette::random()))
("enqueue" [a: usize] Self::Enqueue(a.unwrap().saturating_sub(1)))
("swap" [a: usize, b: usize] 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 + 1;
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
});
#[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]
Self::Get(a.unwrap().saturating_sub(1), b.unwrap().saturating_sub(1)))
("put" [a: usize, b: usize, c: Option<Arc<RwLock<MidiClip>>>]
Self::Put(a.unwrap().saturating_sub(1), b.unwrap().saturating_sub(1), c.unwrap()))
("enqueue" [a: usize, b: usize]
Self::Enqueue(a.unwrap().saturating_sub(1), b.unwrap().saturating_sub(1)))
("edit" [a: Option<Arc<RwLock<MidiClip>>>]
Self::Edit(a.unwrap()))
("loop" [a: usize, b: usize, c: bool]
Self::SetLoop(a.unwrap().saturating_sub(1), b.unwrap().saturating_sub(1), c.unwrap()))
("color" [a: usize, b: usize]
Self::SetColor(a.unwrap().saturating_sub(1), b.unwrap().saturating_sub(1), 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
},
_ => None
});

File diff suppressed because it is too large Load diff

390
tek/src/model.rs Normal file
View file

@ -0,0 +1,390 @@
use crate::*;
#[derive(Default, Debug)] pub struct Tek {
/// Must not be dropped for the duration of the process
pub jack: Arc<RwLock<JackConnection>>,
/// Source of time
pub clock: Clock,
/// Theme
pub color: ItemPalette,
pub pool: Option<MidiPool>,
pub editor: Option<MidiEditor>,
pub player: Option<MidiPlayer>,
pub sampler: Option<Sampler>,
pub midi_buf: Vec<Vec<Vec<u8>>>,
pub midi_ins: Vec<JackPort<MidiIn>>,
pub midi_outs: Vec<JackPort<MidiOut>>,
pub audio_ins: Vec<JackPort<AudioIn>>,
pub audio_outs: Vec<JackPort<AudioOut>>,
pub note_buf: Vec<u8>,
pub tracks: Vec<Track>,
pub scenes: Vec<Scene>,
pub selected: Selection,
pub splits: Vec<u16>,
pub size: Measure<TuiOut>,
pub perf: PerfModel,
pub editing: AtomicBool,
pub history: Vec<TekCommand>,
/// 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 fmtd_beat: Arc<RwLock<String>>,
pub fmtd_time: Arc<RwLock<String>>,
pub fmtd_bpm: Arc<RwLock<String>>,
pub fmtd_sr: Arc<RwLock<String>>,
pub fmtd_buf: Arc<RwLock<String>>,
pub fmtd_lat: Arc<RwLock<String>>,
pub fmtd_stop: Arc<str>,
}
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!(Selection: |self: Tek| {});
provide!(Arc<RwLock<MidiClip>>: |self: Tek| {});
provide!(Option<Arc<RwLock<MidiClip>>>: |self: Tek| {});
provide_bool!(bool: |self: Tek| {});
provide_num!(isize: |self: Tek| {});
provide_num!(usize: |self: Tek| {
":scene" => self.selected.scene().unwrap_or(0),
":scene-next" => (self.selected.scene().unwrap_or(0) + 1).min(self.scenes.len()),
":scene-prev" => self.selected.scene().unwrap_or(0).saturating_sub(1),
":track" => self.selected.track().unwrap_or(0),
":track-next" => (self.selected.track().unwrap_or(0) + 1).min(self.tracks.len()),
":track-prev" => self.selected.track().unwrap_or(0).saturating_sub(1) });
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()
))?;
}
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 fn tracks_add (
&mut self, count: usize, width: usize,
midi_from: &[PortConnection], midi_to: &[PortConnection],
) -> 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;
track.width = width;
}
Ok(())
}
pub fn track_add (
&mut self, name: Option<&str>, color: Option<ItemPalette>,
midi_from: &[PortConnection], midi_to: &[PortConnection],
) -> 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(9),
color: color.unwrap_or_else(ItemPalette::random),
player: MidiPlayer::from(self.clock()),
name,
..Default::default()
};
track.player.midi_ins.push(JackPort::<MidiIn>::new(
&self.jack, &format!("{}I", &track.name), midi_from
)?);
track.player.midi_outs.push(JackPort::<MidiOut>::new(
&self.jack, &format!("{}O", &track.name), midi_to
)?);
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 sync_lead (&self, enable: bool) -> Usually<()> {
if enable {
self.jack.read().unwrap().client().register_timebase_callback(false, |mut state|{
let clock = self.clock();
clock.playhead.update_from_sample(state.position.frame() as f64);
state.position.bbt = Some(clock.bbt());
state.position
})?;
}
Ok(())
}
pub fn sync_follow (&self, enable: bool) -> Usually<()> {
// TODO: sync follow
Ok(())
}
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()
}
}
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);
}
}
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(())
}
fn cell <T: Content<TuiOut>> (theme: ItemPalette, field: T) -> impl Content<TuiOut> {
Tui::fg_bg(theme.lightest.rgb, theme.base.rgb, Fixed::y(1, field))
}
}
/// 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 }
}
fn description (&self, tracks: &[Track], scenes: &[Scene]) -> Arc<str> {
format!("Selected: {}", 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 }
}
pub trait HasSelection {
fn selected (&self) -> &Selection;
fn selected_mut (&mut self) -> &mut Selection;
}
#[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<JackPort<AudioIn>>,
/// Outputs of last device
pub audio_outs: Vec<JackPort<AudioOut>>,
}
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<JackPort<MidiIn>> { &self.midi_ins }
fn midi_outs (&self) -> &Vec<JackPort<MidiOut>> { &self.midi_outs }
fn tracks (&self) -> &Vec<Track> { &self.tracks }
fn tracks_mut (&mut self) -> &mut Vec<Track> { &mut self.tracks }
}
pub trait HasTracks: HasSelection + HasClock + HasJack + HasEditor + Send + Sync {
fn midi_ins (&self) -> &Vec<JackPort<MidiIn>>;
fn midi_outs (&self) -> &Vec<JackPort<MidiOut>>;
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 tracks_sizes <'a> (&'a self, editing: bool, bigger: usize)
-> impl Iterator<Item=(usize, &'a Track, usize, usize)> + Send + Sync + 'a
{
let mut x = 0;
let active = match self.selected() {
Selection::Track(t) if editing => Some(t.saturating_sub(1)),
Selection::Clip(t, _) if editing => Some(t.saturating_sub(1)),
_ => None
};
self.tracks().iter().enumerate().map(move |(index, track)|{
let width = if Some(index) == active { bigger } else { track.width.max(8) };
let data = (index, track, x, x + width);
x += width + Self::WIDTH_OFFSET;
data
})
}
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))
}
}
pub trait Device: Send + Sync + std::fmt::Debug {}
impl Device for Sampler {}
impl Device for Plugin {}
#[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
})
}
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 }
}
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 scenes_sizes (&self, editing: bool, height: usize, larger: usize,)
-> impl Iterator<Item = (usize, &Scene, usize, usize)> + Send + Sync
{
let mut y = 0;
let (selected_track, selected_scene) = match self.selected() {
Selection::Clip(t, s) => (Some(t.saturating_sub(1)), Some(s.saturating_sub(1))),
_ => (None, None)
};
self.scenes().iter().enumerate().map(move|(s, scene)|{
let active = editing && selected_track.is_some() && selected_scene == Some(s);
let height = if active { larger } else { height };
let data = (s, scene, y, y + height);
y += height;
data
})
}
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)));
}
}

377
tek/src/view.rs Normal file
View file

@ -0,0 +1,377 @@
use crate::*;
view!(TuiOut: |self: Tek| self.size.of(View(self, self.view)); {
":editor" => (&self.editor).boxed(),
":pool" => self.view_pool().boxed(),
":sample" => self.view_sample(self.is_editing()).boxed(),
":sampler" => self.view_sampler(self.is_editing(), &self.editor).boxed(),
":status" => self.view_editor().boxed(),
":toolbar" => self.view_clock().boxed(),
":tracks" => self.view_tracks().boxed(),
":inputs" => self.view_inputs().boxed(),
":outputs" => self.view_outputs().boxed(),
":scenes" => self.view_scenes().boxed(),
":scene-add" => Fill::x(Align::x(Fixed::x(23, button(" C-a ", format!(" add scene ({}/{})",
self.selected().scene().unwrap_or(0),
self.scenes().len()))))).boxed(),
});
provide_num!(u16: |self: Tek| {
":sidebar-w" => self.sidebar_w(),
":sample-h" => if self.is_editing() { 0 } else { 5 },
":samples-w" => if self.is_editing() { 4 } else { 11 },
":samples-y" => if self.is_editing() { 1 } else { 0 },
":pool-w" => if self.is_editing() { 5 } else {
let w = self.size.w();
if w > 60 { 20 } else if w > 40 { 15 } else { 10 }
} });
macro_rules! per_track {
(|$self:ident,$track:ident,$index:ident|$content:expr) => {{
let tracks = ||$self.tracks_sizes($self.is_editing(), $self.editor_w());
Box::new(move||Align::x(Map::new(tracks, move|(_, $track, x1, x2), $index| {
let width = (x2 - x1) as u16;
let content = Fixed::y(1, $content);
let styled = Tui::fg_bg($track.color.lightest.rgb, $track.color.base.rgb, content);
map_east(x1 as u16, width, Fixed::x(width, styled))
}))).into()
}}
}
impl Tek {
fn view_clock (&self) -> impl Content<TuiOut> + use<'_> {
Outer(false, Style::default().fg(Tui::g(0))).enclose(row!(
self.view_engine_stats(), " ",
self.view_play_pause(), " ",
self.view_beat_stats(),
))
}
fn view_beat_stats (&self) -> impl Content<TuiOut> + use<'_> {
let compact = self.size.w() > 80;
let clock = self.clock();
let delta = |start: &Moment|clock.global.usec.get() - start.usec.get();
let mut fmtd_beat = self.fmtd_beat.write().unwrap();
let mut fmtd_time = self.fmtd_time.write().unwrap();
let mut fmtd_bpm = self.fmtd_bpm.write().unwrap();
fmtd_beat.clear();
fmtd_time.clear();
fmtd_bpm.clear();
if let Some(now) = clock.started.read().unwrap().as_ref().map(delta) {
clock.timebase.format_beats_1_to(&mut*fmtd_beat, clock.timebase.usecs_to_pulse(now));
write!(&mut fmtd_time, "{:.3}s", now/1000000.);
write!(&mut fmtd_bpm, "{:.3}", clock.timebase.bpm.get());
} else {
write!(&mut fmtd_beat, "-.-.--");
write!(&mut fmtd_time, "-.---s");
write!(&mut fmtd_bpm, "---.---");
}
let theme = ItemPalette::G[128];
Thunk::new(move||Either::new(compact,
row!(FieldH(theme, "BPM", self.fmtd_bpm.clone()),
FieldH(theme, "Beat", self.fmtd_beat.clone()),
FieldH(theme, "Time", self.fmtd_time.clone())),
row!(FieldV(theme, "BPM", self.fmtd_bpm.clone()),
FieldV(theme, "Beat", self.fmtd_beat.clone()),
FieldV(theme, "Time", self.fmtd_time.clone()))))
}
fn view_engine_stats (&self) -> impl Content<TuiOut> + use<'_> {
let compact = self.size.w() > 80;
let clock = self.clock();
let rate = clock.timebase.sr.get();
let chunk = clock.chunk.load(Relaxed);
let mut fmtd_sr = self.fmtd_sr.write().unwrap();
let mut fmtd_buf = self.fmtd_buf.write().unwrap();
let mut fmtd_lat = self.fmtd_lat.write().unwrap();
fmtd_sr.clear();
write!(&mut fmtd_sr, "{}", if compact {format!("{:.1}kHz", rate / 1000.)} else {format!("{:.0}Hz", rate)});
fmtd_buf.clear();
write!(&mut fmtd_buf, "{chunk}");
fmtd_lat.clear();
write!(&mut fmtd_lat, "{:.1}ms", chunk as f64 / rate * 1000.);
let theme = ItemPalette::G[128];
Either::new(compact,
row!(FieldH(theme, "SR", self.fmtd_sr.clone()),
FieldH(theme, "Buf", self.fmtd_buf.clone()),
FieldH(theme, "Lat", self.fmtd_lat.clone())),
row!(FieldV(theme, "SR", self.fmtd_sr.clone()),
FieldV(theme, "Buf", self.fmtd_buf.clone()),
FieldV(theme, "Lat", self.fmtd_lat.clone())))
}
fn view_meter <'a> (&'a self, 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 (&self, values: &[f32;2]) -> impl Content<TuiOut> + use<'_> {
col!(
format!("L/{:>+9.3}", values[0]),
format!("R/{:>+9.3}", values[1]),
)
}
fn view_play_pause (&self) -> impl Content<TuiOut> + use<'_> {
let playing = self.clock.is_rolling();
let compact = 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(" ▗▄▖ ", " ▝▀▘ ",)))))))
}
fn view_editor (&self) -> impl Content<TuiOut> + use<'_> {
self.editor.as_ref().map(|e|Bsp::e(e.clip_status(), e.edit_status()))
}
fn view_pool (&self) -> impl Content<TuiOut> + use<'_> {
self.pool.as_ref().map(|pool|PoolView(self.is_editing(), pool))
}
fn view_scene_add (&self) -> impl Content<TuiOut> + use<'_> {
button(" C-a ", format!(" add scene ({}/{})",
self.selected().scene().unwrap_or(0),
self.scenes().len()))
}
fn view_tracks (&self) -> impl Content<TuiOut> + use<'_> {
let h = 1;
self.view_row(self.w(), 1, self.track_header(), self.track_cells())
}
fn view_inputs (&self) -> impl Content<TuiOut> + use<'_> {
let h = 1 + self.midi_ins.len() as u16;
self.view_row(self.w(), h, self.input_header(), self.input_cells())
}
fn view_outputs (&self) -> impl Content<TuiOut> + use<'_> {
let h = 1 + self.midi_outs.len();
self.view_row(self.w(), h as u16,
self.output_header(),
self.output_cells())
}
fn view_scenes (&self) -> impl Content<TuiOut> + use<'_> {
Outer(false, Style::default().fg(Tui::g(0))).enclose_bg({
let w = self.w();
let d = 6 + self.midi_ins.len() + self.midi_outs.len();
let h = self.size.h().saturating_sub(d) as u16;
self.view_row(w, h, self.scene_header(), self.clip_columns())
})
}
fn scene_header <'a> (&'a self) -> ThunkBox<'a, TuiOut> {
(move||{
let last_color = Arc::new(RwLock::new(ItemPalette::G[0]));
let iter = ||self.scenes_sizes(self.is_editing(), 2, 15);
Map::new(iter, move|(_, scene, y1, y2), i| {
let cell = phat_sel_3(
self.selected().scene() == Some(i),
Tui::bold(true, Bsp::e("🭬", &scene.name)),
Tui::bold(true, Bsp::e("🭬", &scene.name)),
if i == 0 { Some(Reset) }
else if self.selected().scene() == Some(i) { None }
else { Some(last_color.read().unwrap().base.rgb) },
if self.selected().scene() == Some(i+1) { scene.color.light } else { scene.color.base }.rgb,
Some(Reset)
);
let h = (1 + y2 - y1) as u16;
*last_color.write().unwrap() = scene.color;
map_south(y1 as u16, h, Push::y(1, Fixed::y(h,
Outer(false, Style::default().fg(Tui::g(0))).enclose(cell))))
}).boxed()
}).into()
}
fn clip_columns <'a> (&'a self) -> ThunkBox<'a, TuiOut> {
let editing = self.is_editing();
let tracks = move||self.tracks_sizes(editing, self.editor_w());
let scenes = move||self.scenes_sizes(editing, 2, 15);
let selected_track = self.selected().track();
let selected_scene = self.selected().scene();
let border = |x|Outer(false, Style::default().fg(Tui::g(0))).enclose(x);
let d = 6 + self.midi_ins.len() + self.midi_outs.len();
(move||Align::c(Map::new(tracks, {
let last_color = Arc::new(RwLock::new(ItemPalette::default()));
let area = self.size.w().saturating_sub(self.sidebar_w() as usize * 2);
move|(_, track, x1, x2), t| {
let last_color = last_color.clone();
let same_track = selected_track == Some(t+1);
let w = (x2 - x1) as u16;
map_east(x1 as u16, w, border(Map::new(scenes, move|(_, scene, y1, y2), s|{
let last_color = last_color.clone();
Either(x2 >= area, (), Thunk::new(move||{
let last_color = last_color.clone();
let mut fg = Tui::g(64);
let mut bg = ItemPalette::G[32];
if let Some(clip) = &scene.clips[t] {
let clip = clip.read().unwrap();
fg = clip.color.lightest.rgb;
bg = clip.color
};
// weird offsetting:
let selected = same_track && selected_scene == Some(s+1);
let neighbor = same_track && selected_scene == Some(s);
let active = editing && selected;
//let top = if neighbor { None } else { Some(last_color.read().unwrap().base.rgb) };
let top = if s == 0 {
Some(Reset)
} else if neighbor {
Some(last_color.read().unwrap().light.rgb)
} else {
Some(last_color.read().unwrap().base.rgb)
};
let mid = if selected { bg.light } else { bg.base }.rgb;
let low = Some(Reset);
let h = (1 + y2 - y1) as u16;
*last_color.write().unwrap() = bg;
let tab = " Tab ";
let name = if active {
self.editor.as_ref()
.map(|e|e.clip().as_ref().map(|c|c.clone()))
.flatten()
.map(|c|c.read().unwrap().name.clone())
.unwrap_or_else(||"".into())
} else {
"edit".into()
};
let label = move||{
let clip = scene.clips[t].clone();
let icon = "";
let name = clip.map(|c|c.read().unwrap().name.clone());
Align::nw(Tui::fg(fg, Bsp::e(icon, Bsp::e(Tui::bold(true, name), " "))))
};
let area = self.size.h().saturating_sub(d);//self.sidebar_w() as usize * 2);
Either(y2 > area, (), map_south(y1 as u16, h, Push::y(1, Fixed::y(h, Either::new(active,
Thunk::new(move||Bsp::a(
Fill::xy(Align::nw(button(tab, label()))),
&self.editor)),
Thunk::new(move||Bsp::a(
When::new(selected, Fill::y(Align::n(button(tab, "edit")))),
phat_sel_3(
selected,
Fill::xy(label()),
Fill::xy(label()),
top, mid, low
)
)),
)))))
}))
}))).boxed()
}
})).boxed()).into()
}
fn view_row <'a> (
&'a self, w: u16, h: u16, a: impl Content<TuiOut> + 'a, b: impl Content<TuiOut> + 'a
) -> impl Content<TuiOut> + 'a {
Fixed::y(h, Bsp::e(
Fixed::x(self.sidebar_w() as u16, a),
Fill::x(Align::c(Fixed::xy(w, h, b)))
))
}
fn w (&self) -> u16 {
self.tracks_sizes(self.is_editing(), self.editor_w())
.last()
.map(|x|x.3 as u16)
.unwrap_or(0)
}
fn sidebar_w (&self) -> u16 {
let w = self.size.w();
let w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
let w = if self.is_editing() { 8 } else { w };
w
}
fn input_header <'a> (&'a self) -> ThunkBox<'a, TuiOut> {
let fg = Tui::g(224);
let bg = Tui::g(64);
let input = move|input: &JackPort<MidiIn>|Bsp::s(
Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(input.name.clone())))),
input.connect.get(0).map(|connect|Fill::x(Align::w(Tui::bold(false,
Tui::fg_bg(fg, bg, connect.info()))))));
(move||{
let label = format!(" midi ins ({})", self.midi_ins().len());
let button = Fill::x(Align::w(self.button(" I ", label)));
Bsp::s(button, self.midi_ins().get(0).map(input)).boxed()
}).into()
}
fn input_cells <'a> (&'a self) -> ThunkBox<'a, TuiOut> {
let rec = false;
let mon = false;
per_track!(|self, track, _t|Bsp::s(Tui::bold(true, row!(
Tui::fg_bg(if rec { White } else { track.color.light.rgb }, track.color.dark.rgb, "Rcrd"),
Tui::fg_bg(if rec { White } else { track.color.dark.rgb }, track.color.dark.rgb, ""),
Tui::fg_bg(if mon { White } else { track.color.light.rgb }, track.color.dark.rgb, "Mntr"),
)), row!(
Tui::fg_bg(if rec { White } else { track.color.light.rgb }, track.color.darker.rgb, "CH**"),
Tui::fg_bg(if rec { White } else { track.color.dark.rgb }, track.color.darker.rgb, ""),
Tui::fg_bg(if mon { White } else { track.color.light.rgb }, track.color.darker.rgb, "CH**"),
)))
}
fn output_header <'a> (&'a self) -> ThunkBox<'a, TuiOut> {
let fg = Tui::g(224);
let bg = Tui::g(64);
(move||Bsp::s(Fill::x(Align::w(self.button(" O ", format!(" midi outs ({}) ", self.midi_outs().len())))), self.midi_outs().get(0).map(|out|Bsp::s(
Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(out.name.clone())))),
out.connect.get(0).map(|connect|Fill::x(Align::w(Tui::bold(false, Tui::fg_bg(fg, bg, connect.info()))))),
))).boxed()).into()
}
fn output_cells <'a> (&'a self) -> ThunkBox<'a, TuiOut> {
let mute = false;
let solo = false;
per_track!(|self, track, _t|Bsp::s(Tui::bold(true, row!(
Tui::fg_bg(if mute { White } else { track.color.light.rgb }, track.color.dark.rgb, "Mute"),
Tui::fg_bg(if mute { White } else { track.color.dark.rgb }, track.color.dark.rgb, ""),
Tui::fg_bg(if solo { White } else { track.color.light.rgb }, track.color.dark.rgb, "Solo"),
)), row!(
Tui::fg_bg(if mute { White } else { track.color.light.rgb }, track.color.darker.rgb, "CH**"),
Tui::fg_bg(if mute { White } else { track.color.darker.rgb }, track.color.darker.rgb, ""),
Tui::fg_bg(if solo { White } else { track.color.light.rgb }, track.color.darker.rgb, "CH**"),
)))
}
fn track_header <'a> (&'a self) -> ThunkBox<'a, TuiOut> {
let add_track = ||self.button(" C-t ", format!(" add track ({}/{})",
self.selected.track().unwrap_or(0),
self.tracks().len()));
(move||Tui::bg(Tui::g(32), Fill::x(Align::w(add_track()))).boxed()).into()
}
fn track_cells <'a> (&'a self) -> ThunkBox<'a, TuiOut> {
per_track!(|self, track, t|{
let active = self.selected().track() == Some(t+1);
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 = 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) };
let bs = Style::default().fg(bfg).bg(bg);
let cell = Bsp::e(
Tui::fg_bg(bg, bg2, ""),
Tui::fg_bg(fg, bg, Tui::bold(true, Fill::x(Align::nw(Bsp::e(" ", name))))));
Outer(active, bs).enclose(cell)
})
}
fn button <'a> (
&'a self, key: impl Content<TuiOut> + 'a, label: impl Content<TuiOut> + 'a
) -> impl Content<TuiOut> + 'a {
let compact = !self.is_editing();
Tui::bold(true, Bsp::e(
Margin::x(1, Tui::fg_bg(Tui::g(0), Tui::orange(), key)),
When::new(compact, Margin::x(1, Tui::fg_bg(Tui::g(255), Tui::g(96), label))),
))
}
}
fn button <'a> (
key: impl Content<TuiOut> + 'a,
label: impl Content<TuiOut> + 'a
) -> impl Content<TuiOut> + 'a {
Tui::bold(true, Bsp::e(
Margin::x(1, Tui::fg_bg(Tui::g(0), Tui::orange(), key)),
Margin::x(1, Tui::fg_bg(Tui::g(255), Tui::g(96), label)),
))
}