mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-07 12:16:42 +01:00
break down tek/src/lib.rs into lib,model,view,keys,cli,audio
This commit is contained in:
parent
751e7d2160
commit
415dc444ea
7 changed files with 1334 additions and 1303 deletions
|
|
@ -10,7 +10,7 @@ pub(crate) use self::ParseError::*;
|
|||
pub(crate) use konst::iter::{ConstIntoIter, IsIteratorKind};
|
||||
pub(crate) use konst::string::{split_at, str_range, char_indices};
|
||||
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.
|
||||
#[macro_export] macro_rules! iterate {
|
||||
($expr:expr => $arg: pat => $body:expr) => {
|
||||
|
|
|
|||
71
tek/src/audio.rs
Normal file
71
tek/src/audio.rs
Normal 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
182
tek/src/cli.rs
Normal 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
308
tek/src/keys.rs
Normal 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
|
||||
});
|
||||
1307
tek/src/lib.rs
1307
tek/src/lib.rs
File diff suppressed because it is too large
Load diff
390
tek/src/model.rs
Normal file
390
tek/src/model.rs
Normal 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
377
tek/src/view.rs
Normal 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)),
|
||||
))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue