mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 19:56:42 +01:00
wip: fold inwards
This commit is contained in:
parent
5d3e564949
commit
91d6bcc870
7 changed files with 542 additions and 553 deletions
|
|
@ -1,376 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
use EdnItem::*;
|
|
||||||
use ClockCommand::{Play, Pause};
|
|
||||||
pub const TRACK_MIN_WIDTH: usize = 9;
|
|
||||||
impl HasJack for App {
|
|
||||||
fn jack (&self) -> &Arc<RwLock<JackConnection>> { &self.jack }
|
|
||||||
}
|
|
||||||
/// Hosts the JACK callback for a collection of tracks
|
|
||||||
pub struct TracksAudio<'a>(
|
|
||||||
// Track collection
|
|
||||||
pub &'a mut [ArrangerTrack],
|
|
||||||
/// Note buffer
|
|
||||||
pub &'a mut Vec<u8>,
|
|
||||||
/// Note chunk buffer
|
|
||||||
pub &'a mut Vec<Vec<Vec<u8>>>,
|
|
||||||
);
|
|
||||||
impl Audio for TracksAudio<'_> {
|
|
||||||
#[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
|
||||||
let model = &mut self.0;
|
|
||||||
let note_buffer = &mut self.1;
|
|
||||||
let output_buffer = &mut self.2;
|
|
||||||
for track in model.iter_mut() {
|
|
||||||
if PlayerAudio(track.player_mut(), note_buffer, output_buffer).process(client, scope) == Control::Quit {
|
|
||||||
return Control::Quit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Control::Continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
command!(|self: ClipCommand, state: App|match self { _ => todo!("clip command") });
|
|
||||||
command!(|self: SceneCommand, state: App|match self { _ => todo!("scene command") });
|
|
||||||
command!(|self: TrackCommand, state: App|match self { _ => todo!("track command") });
|
|
||||||
#[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),
|
|
||||||
}
|
|
||||||
edn_command!(ClipCommand: |state: App| {
|
|
||||||
("get" [a: usize
|
|
||||||
,b: usize] Self::Get(a.unwrap(), b.unwrap()))
|
|
||||||
|
|
||||||
("put" [a: usize
|
|
||||||
,b: usize
|
|
||||||
,c: Option<Arc<RwLock<MidiClip>>>] Self::Put(a, b, c))
|
|
||||||
|
|
||||||
("enqueue" [a: usize
|
|
||||||
,b: usize] Self::Enqueue(a, b))
|
|
||||||
|
|
||||||
("edit" [a: Option<Arc<RwLock<MidiClip>>>] Self::Edit(a))
|
|
||||||
|
|
||||||
("loop" [a: usize
|
|
||||||
,b: usize
|
|
||||||
,c: bool] Self::SetLoop(a, b, c))
|
|
||||||
|
|
||||||
("color" [a: usize
|
|
||||||
,b: usize] Self::SetColor(a, b, ItemPalette::random()))
|
|
||||||
});
|
|
||||||
#[derive(Clone, Debug)] pub enum SceneCommand {
|
|
||||||
Add,
|
|
||||||
Del(usize),
|
|
||||||
Swap(usize, usize),
|
|
||||||
SetSize(usize),
|
|
||||||
SetZoom(usize),
|
|
||||||
SetColor(usize, ItemPalette),
|
|
||||||
Enqueue(usize),
|
|
||||||
}
|
|
||||||
edn_command!(SceneCommand: |state: App| {
|
|
||||||
("add" [] Self::Add)
|
|
||||||
("del" [a: usize] Self::Del(0))
|
|
||||||
("zoom" [a: usize] Self::SetZoom(a))
|
|
||||||
("color" [a: usize] Self::SetColor(a, ItemPalette::random()))
|
|
||||||
("enqueue" [a: usize] Self::Enqueue(a))
|
|
||||||
("swap" [a: usize, b: usize] Self::Swap(a, b))
|
|
||||||
});
|
|
||||||
#[derive(Clone, Debug)] pub enum TrackCommand {
|
|
||||||
Add,
|
|
||||||
Del(usize),
|
|
||||||
Stop(usize),
|
|
||||||
Swap(usize, usize),
|
|
||||||
SetSize(usize),
|
|
||||||
SetZoom(usize),
|
|
||||||
SetColor(usize, ItemPalette),
|
|
||||||
}
|
|
||||||
edn_command!(TrackCommand: |state: App| {
|
|
||||||
("add" [] Self::Add)
|
|
||||||
("size" [a: usize] Self::SetSize(a))
|
|
||||||
("zoom" [a: usize] Self::SetZoom(a))
|
|
||||||
("color" [a: usize] Self::SetColor(a, ItemPalette::random()))
|
|
||||||
("del" [a: usize] Self::Del(a))
|
|
||||||
("stop" [a: usize] Self::Stop(a))
|
|
||||||
("swap" [a: usize, b: usize] Self::Swap(a, b))
|
|
||||||
});
|
|
||||||
pub trait Arrangement: HasClock + HasJack {
|
|
||||||
fn tracks (&self) -> &Vec<ArrangerTrack>;
|
|
||||||
fn tracks_mut (&mut self) -> &mut Vec<ArrangerTrack>;
|
|
||||||
fn scenes (&self) -> &Vec<ArrangerScene>;
|
|
||||||
fn scenes_mut (&mut self) -> &mut Vec<ArrangerScene>;
|
|
||||||
fn selected (&self) -> &ArrangerSelection;
|
|
||||||
fn selected_mut (&mut self) -> &mut ArrangerSelection;
|
|
||||||
|
|
||||||
fn track_next_name (&self) -> Arc<str> {
|
|
||||||
format!("Trk{:02}", self.tracks().len() + 1).into()
|
|
||||||
}
|
|
||||||
fn track (&self) -> Option<&ArrangerTrack> {
|
|
||||||
self.selected().track().and_then(|s|self.tracks().get(s))
|
|
||||||
}
|
|
||||||
fn track_mut (&mut self) -> Option<&mut ArrangerTrack> {
|
|
||||||
self.selected().track().and_then(|s|self.tracks_mut().get_mut(s))
|
|
||||||
}
|
|
||||||
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 track_add (&mut self, name: Option<&str>, color: Option<ItemPalette>)
|
|
||||||
-> Usually<&mut ArrangerTrack>
|
|
||||||
{
|
|
||||||
let name = name.map_or_else(||self.track_next_name(), |x|x.to_string().into());
|
|
||||||
let track = ArrangerTrack {
|
|
||||||
width: (name.len() + 2).max(9),
|
|
||||||
color: color.unwrap_or_else(ItemPalette::random),
|
|
||||||
player: MidiPlayer::from(self.clock()),
|
|
||||||
name,
|
|
||||||
};
|
|
||||||
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(&mut self.tracks_mut()[index])
|
|
||||||
}
|
|
||||||
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))?;
|
|
||||||
track.width = width;
|
|
||||||
let port = JackPort::<MidiIn>::new(&jack, &format!("{}I", &track.name), midi_from)?;
|
|
||||||
track.player.midi_ins.push(port);
|
|
||||||
let port = JackPort::<MidiOut>::new(&jack, &format!("{}O", &track.name), midi_to)?;
|
|
||||||
track.player.midi_outs.push(port);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn scene_default_name (&self) -> Arc<str> {
|
|
||||||
format!("Sc{:3>}", self.scenes().len() + 1).into()
|
|
||||||
}
|
|
||||||
fn scene (&self) -> Option<&ArrangerScene> {
|
|
||||||
self.selected().scene().and_then(|s|self.scenes().get(s))
|
|
||||||
}
|
|
||||||
fn scene_mut (&mut self) -> Option<&mut ArrangerScene> {
|
|
||||||
self.selected().scene().and_then(|s|self.scenes_mut().get_mut(s))
|
|
||||||
}
|
|
||||||
fn scene_del (&mut self, index: usize) {
|
|
||||||
todo!("delete scene");
|
|
||||||
}
|
|
||||||
fn scene_add (&mut self, name: Option<&str>, color: Option<ItemPalette>)
|
|
||||||
-> Usually<&mut ArrangerScene>
|
|
||||||
{
|
|
||||||
let scene = ArrangerScene {
|
|
||||||
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(&mut self.scenes_mut()[index])
|
|
||||||
}
|
|
||||||
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 _scene = self.scene_add(None, Some(
|
|
||||||
scene_color_1.mix(scene_color_2, i as f32 / n as f32).into()
|
|
||||||
))?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
fn activate (&mut self) -> Usually<()> {
|
|
||||||
let selected = self.selected().clone();
|
|
||||||
match selected {
|
|
||||||
ArrangerSelection::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))?;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ArrangerSelection::Clip(t, s) => {
|
|
||||||
let clip = self.scenes()[s].clips[t].clone();
|
|
||||||
self.tracks_mut()[t].player.enqueue_next(clip.as_ref());
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//fn randomize_color (&mut self) {
|
|
||||||
//match self.selected {
|
|
||||||
//ArrangerSelection::Mix => { self.color = ItemPalette::random() },
|
|
||||||
//ArrangerSelection::Track(t) => { self.tracks[t].color = ItemPalette::random() },
|
|
||||||
//ArrangerSelection::Scene(s) => { self.scenes[s].color = ItemPalette::random() },
|
|
||||||
//ArrangerSelection::Clip(t, s) => if let Some(clip) = &self.scenes[s].clips[t] {
|
|
||||||
//clip.write().unwrap().color = ItemPalette::random();
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)] pub struct ArrangerScene {
|
|
||||||
/// Name of scene
|
|
||||||
pub(crate) name: Arc<str>,
|
|
||||||
/// Clips in scene, one per track
|
|
||||||
pub(crate) clips: Vec<Option<Arc<RwLock<MidiClip>>>>,
|
|
||||||
/// Identifying color of scene
|
|
||||||
pub(crate) color: ItemPalette,
|
|
||||||
}
|
|
||||||
impl ArrangerScene {
|
|
||||||
pub fn longest_name (scenes: &[Self]) -> usize {
|
|
||||||
scenes.iter().map(|s|s.name.len()).fold(0, usize::max)
|
|
||||||
}
|
|
||||||
/// Returns the pulse length of the longest clip in the scene
|
|
||||||
pub 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.
|
|
||||||
pub fn is_playing (&self, tracks: &[ArrangerTrack]) -> bool {
|
|
||||||
self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate()
|
|
||||||
.all(|(track_index, clip)|match clip {
|
|
||||||
Some(c) => tracks
|
|
||||||
.get(track_index)
|
|
||||||
.map(|track|{
|
|
||||||
if let Some((_, Some(clip))) = track.player().play_clip() {
|
|
||||||
*clip.read().unwrap() == *c.read().unwrap()
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap_or(false),
|
|
||||||
None => true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
pub fn clip (&self, index: usize) -> Option<&Arc<RwLock<MidiClip>>> {
|
|
||||||
match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[derive(Debug)] pub struct ArrangerTrack {
|
|
||||||
/// 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,
|
|
||||||
}
|
|
||||||
has_clock!(|self:ArrangerTrack|self.player.clock());
|
|
||||||
has_player!(|self:ArrangerTrack|self.player);
|
|
||||||
impl ArrangerTrack {
|
|
||||||
fn longest_name (tracks: &[Self]) -> usize {
|
|
||||||
tracks.iter().map(|s|s.name.len()).fold(0, usize::max)
|
|
||||||
}
|
|
||||||
fn width_inc (&mut self) {
|
|
||||||
self.width += 1;
|
|
||||||
}
|
|
||||||
fn width_dec (&mut self) {
|
|
||||||
if self.width > TRACK_MIN_WIDTH {
|
|
||||||
self.width -= 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[derive(PartialEq, Clone, Copy, Debug, Default)]
|
|
||||||
/// Represents the current user selection in the arranger
|
|
||||||
pub enum ArrangerSelection {
|
|
||||||
/// 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 ArrangerSelection {
|
|
||||||
pub fn track (&self) -> Option<usize> {
|
|
||||||
use ArrangerSelection::*;
|
|
||||||
match self {
|
|
||||||
Clip(t, _) => Some(*t),
|
|
||||||
Track(t) => Some(*t),
|
|
||||||
_ => None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn scene (&self) -> Option<usize> {
|
|
||||||
use ArrangerSelection::*;
|
|
||||||
match self {
|
|
||||||
Clip(_, s) => Some(*s),
|
|
||||||
Scene(s) => Some(*s),
|
|
||||||
_ => None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn is_mix (&self) -> bool { matches!(self, Self::Mix) }
|
|
||||||
pub fn is_track (&self) -> bool { matches!(self, Self::Track(_)) }
|
|
||||||
pub fn is_scene (&self) -> bool { matches!(self, Self::Scene(_)) }
|
|
||||||
pub fn is_clip (&self) -> bool { matches!(self, Self::Clip(_, _)) }
|
|
||||||
pub fn description (
|
|
||||||
&self,
|
|
||||||
tracks: &[ArrangerTrack],
|
|
||||||
scenes: &[ArrangerScene],
|
|
||||||
) -> 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 Arrangement for App {
|
|
||||||
fn tracks (&self) -> &Vec<ArrangerTrack> { &self.tracks }
|
|
||||||
fn tracks_mut (&mut self) -> &mut Vec<ArrangerTrack> { &mut self.tracks }
|
|
||||||
fn scenes (&self) -> &Vec<ArrangerScene> { &self.scenes }
|
|
||||||
fn scenes_mut (&mut self) -> &mut Vec<ArrangerScene> { &mut self.scenes }
|
|
||||||
fn selected (&self) -> &ArrangerSelection { &self.selected }
|
|
||||||
fn selected_mut (&mut self) -> &mut ArrangerSelection { &mut self.selected }
|
|
||||||
}
|
|
||||||
//impl Arrangement for Arranger {
|
|
||||||
//fn tracks (&self) -> &Vec<ArrangerTrack> { &self.tracks }
|
|
||||||
//fn tracks_mut (&mut self) -> &mut Vec<ArrangerTrack> { &mut self.tracks }
|
|
||||||
//fn scenes (&self) -> &Vec<ArrangerScene> { &self.scenes }
|
|
||||||
//fn scenes_mut (&mut self) -> &mut Vec<ArrangerScene> { &mut self.scenes }
|
|
||||||
//fn selected (&self) -> &ArrangerSelection { &self.selected }
|
|
||||||
//fn selected_mut (&mut self) -> &mut ArrangerSelection { &mut self.selected }
|
|
||||||
//}
|
|
||||||
694
tek/src/lib.rs
694
tek/src/lib.rs
|
|
@ -9,8 +9,7 @@
|
||||||
pub type Usually<T> = std::result::Result<T, Box<dyn Error>>;
|
pub type Usually<T> = std::result::Result<T, Box<dyn Error>>;
|
||||||
/// Standard optional result type.
|
/// Standard optional result type.
|
||||||
pub type Perhaps<T> = std::result::Result<Option<T>, Box<dyn Error>>;
|
pub type Perhaps<T> = std::result::Result<Option<T>, Box<dyn Error>>;
|
||||||
pub mod arranger; pub use self::arranger::*;
|
pub mod mixer; pub use self::mixer::*;
|
||||||
pub mod mixer; pub use self::mixer::*;
|
|
||||||
pub use ::tek_tui::{
|
pub use ::tek_tui::{
|
||||||
*,
|
*,
|
||||||
tek_edn::*,
|
tek_edn::*,
|
||||||
|
|
@ -67,14 +66,34 @@ pub(crate) use std::ffi::OsString;
|
||||||
pub audio_ins: Vec<JackPort<AudioIn>>,
|
pub audio_ins: Vec<JackPort<AudioIn>>,
|
||||||
pub audio_outs: Vec<JackPort<AudioOut>>,
|
pub audio_outs: Vec<JackPort<AudioOut>>,
|
||||||
pub note_buf: Vec<u8>,
|
pub note_buf: Vec<u8>,
|
||||||
pub tracks: Vec<ArrangerTrack>,
|
pub tracks: Vec<Track>,
|
||||||
pub scenes: Vec<ArrangerScene>,
|
pub scenes: Vec<Scene>,
|
||||||
pub selected: ArrangerSelection,
|
pub selected: ArrangerSelection,
|
||||||
pub splits: Vec<u16>,
|
pub splits: Vec<u16>,
|
||||||
pub size: Measure<TuiOut>,
|
pub size: Measure<TuiOut>,
|
||||||
pub perf: PerfModel,
|
pub perf: PerfModel,
|
||||||
pub compact: bool,
|
pub compact: bool,
|
||||||
}
|
}
|
||||||
|
#[derive(Debug, Default)] struct Meter<'a>(pub &'a str, pub f32);
|
||||||
|
#[derive(Debug, Default)] struct Meters<'a>(pub &'a[f32]);
|
||||||
|
#[derive(Debug, Default)] struct Track {
|
||||||
|
/// Name of track
|
||||||
|
name: Arc<str>,
|
||||||
|
/// Preferred width of track column
|
||||||
|
width: usize,
|
||||||
|
/// Identifying color of track
|
||||||
|
color: ItemPalette,
|
||||||
|
/// MIDI player state
|
||||||
|
player: MidiPlayer,
|
||||||
|
}
|
||||||
|
#[derive(Default)] pub struct Scene {
|
||||||
|
/// Name of scene
|
||||||
|
pub(crate) name: Arc<str>,
|
||||||
|
/// Clips in scene, one per track
|
||||||
|
pub(crate) clips: Vec<Option<Arc<RwLock<MidiClip>>>>,
|
||||||
|
/// Identifying color of scene
|
||||||
|
pub(crate) color: ItemPalette,
|
||||||
|
}
|
||||||
impl App {
|
impl App {
|
||||||
pub fn sequencer (
|
pub fn sequencer (
|
||||||
jack: &Arc<RwLock<JackConnection>>,
|
jack: &Arc<RwLock<JackConnection>>,
|
||||||
|
|
@ -85,7 +104,7 @@ impl App {
|
||||||
midi_tos: &[PortConnection],
|
midi_tos: &[PortConnection],
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
edn: include_str!("../edn/sequencer-view.edn").to_string(),
|
edn: include_str!("./sequencer-view.edn").to_string(),
|
||||||
jack: jack.clone(),
|
jack: jack.clone(),
|
||||||
pool: Some(pool),
|
pool: Some(pool),
|
||||||
editor: Some(editor),
|
editor: Some(editor),
|
||||||
|
|
@ -108,7 +127,7 @@ impl App {
|
||||||
audio_tos: &[&[PortConnection]],
|
audio_tos: &[&[PortConnection]],
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
edn: include_str!("../edn/groovebox-view.edn").to_string(),
|
edn: include_str!("./groovebox-view.edn").to_string(),
|
||||||
sampler: Some(sampler),
|
sampler: Some(sampler),
|
||||||
..Self::sequencer(
|
..Self::sequencer(
|
||||||
jack, pool, editor,
|
jack, pool, editor,
|
||||||
|
|
@ -130,7 +149,7 @@ impl App {
|
||||||
track_width: usize,
|
track_width: usize,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let mut arranger = Self {
|
let mut arranger = Self {
|
||||||
edn: include_str!("../edn/arranger-view.edn").to_string(),
|
edn: include_str!("./arranger-view.edn").to_string(),
|
||||||
..Self::groovebox(
|
..Self::groovebox(
|
||||||
jack, pool, editor,
|
jack, pool, editor,
|
||||||
None, midi_froms, midi_tos,
|
None, midi_froms, midi_tos,
|
||||||
|
|
@ -141,7 +160,205 @@ impl App {
|
||||||
arranger.tracks_add(tracks, track_width, &[], &[]);
|
arranger.tracks_add(tracks, track_width, &[], &[]);
|
||||||
arranger
|
arranger
|
||||||
}
|
}
|
||||||
|
fn compact (&self) -> bool { false }
|
||||||
|
fn editor (&self) -> impl Content<TuiOut> + '_ { &self.editor }
|
||||||
|
fn w (&self) -> u16 { self.tracks_with_sizes().last().map(|x|x.3 as u16).unwrap_or(0) }
|
||||||
|
fn pool (&self) -> impl Content<TuiOut> + use<'_> { self.pool.as_ref().map(|pool|Align::e(Fixed::x(self.sidebar_w(), PoolView(self.compact(), pool)))) }
|
||||||
|
fn sample <'a> (&'a self) -> impl Content<TuiOut> + 'a {
|
||||||
|
let compact = self.is_editing();
|
||||||
|
if let (Some(editor), Some(sampler)) = (&self.editor, &self.sampler) {
|
||||||
|
let note_pt = editor.note_point();
|
||||||
|
let sample_h = if compact { 0 } else { 5 };
|
||||||
|
return Some(Max::y(sample_h, Fill::x(sampler.viewer(note_pt))))
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
fn sampler (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
let compact = self.is_editing();
|
||||||
|
if let (Some(editor), Some(sampler)) = (&self.editor, &self.sampler) {
|
||||||
|
let note_pt = editor.note_point();
|
||||||
|
let w = if compact { 4 } else { 40 };
|
||||||
|
let y = if compact { 1 } else { 0 };
|
||||||
|
return Some(Fixed::x(w, Push::y(y, Fill::y(sampler.list(compact, editor)))))
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
fn 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::xy(self.sidebar_w() as u16, h, a),
|
||||||
|
Fill::x(Align::c(Fixed::xy(w, h, b)))
|
||||||
|
))
|
||||||
|
}
|
||||||
|
pub fn tracks_with_sizes (&self)
|
||||||
|
-> impl Iterator<Item = (usize, &Track, usize, usize)>
|
||||||
|
{
|
||||||
|
tracks_with_sizes(self.tracks.iter(), match self.selected {
|
||||||
|
ArrangerSelection::Track(t) if self.is_editing() => Some(t),
|
||||||
|
ArrangerSelection::Clip(t, _) if self.is_editing() => Some(t),
|
||||||
|
_ => None
|
||||||
|
}, self.editor_w())
|
||||||
|
}
|
||||||
|
pub fn scenes_with_sizes (&self, h: usize)
|
||||||
|
-> impl Iterator<Item = (usize, &Scene, usize, usize)>
|
||||||
|
{
|
||||||
|
scenes_with_sizes(self.scenes.iter(), &self.selected, self.is_editing(), 2, 15)
|
||||||
|
}
|
||||||
|
fn is_editing (&self) -> bool {
|
||||||
|
self.editing.load(Relaxed)
|
||||||
|
}
|
||||||
|
fn editor_w (&self) -> usize {
|
||||||
|
let editor = self.editor.as_ref().expect("missing editor");
|
||||||
|
(5 + (editor.time_len().get() / editor.time_zoom().get()))
|
||||||
|
.min(self.size.w().saturating_sub(20))
|
||||||
|
.max(16)
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
impl HasJack for App { fn jack (&self) -> &Arc<RwLock<JackConnection>> { &self.jack } }
|
||||||
|
has_size!(<TuiOut>|self: App|&self.size);
|
||||||
|
has_clock!(|self: App|&self.clock);
|
||||||
|
has_clips!(|self: App|self.pool.as_ref().expect("no clip pool").clips);
|
||||||
|
has_clock!(|self:Track|self.player.clock());
|
||||||
|
has_player!(|self:Track|self.player);
|
||||||
|
has_editor!(|self: App|self.editor.as_ref().expect("no editor"));
|
||||||
|
edn_provide!(u16: |self: App|{
|
||||||
|
":sample-h" => if self.compact() { 0 } else { 5 },
|
||||||
|
":samples-w" => if self.compact() { 4 } else { 11 },
|
||||||
|
":samples-y" => if self.compact() { 1 } else { 0 },
|
||||||
|
":pool-w" => if self.compact() { 5 } else {
|
||||||
|
let w = self.size.w();
|
||||||
|
if w > 60 { 20 } else if w > 40 { 15 } else { 10 }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
edn_provide!(Color: |self: App| { _ => return None });
|
||||||
|
edn_provide!(usize: |self: App| { _ => return None });
|
||||||
|
edn_provide!(isize: |self: App| { _ => return None });
|
||||||
|
edn_provide!(bool: |self: App| { _ => return None });
|
||||||
|
edn_provide!(ArrangerSelection: |self: App| { _ => return None });
|
||||||
|
edn_provide!(Arc<RwLock<MidiClip>>: |self: App| { _ => return None });
|
||||||
|
edn_provide!(Option<Arc<RwLock<MidiClip>>>: |self: App| { _ => return None });
|
||||||
|
edn_provide!('a: Box<dyn Render<TuiOut> + 'a>: |self: App|{
|
||||||
|
":editor" => (&self.editor).boxed(),
|
||||||
|
":pool" => self.pool.as_ref().map(|pool|Align::e(Fixed::x(self.sidebar_w(), PoolView(self.compact(), pool)))).boxed(),
|
||||||
|
":sample" => self.sample().boxed(),
|
||||||
|
":sampler" => self.sampler().boxed(),
|
||||||
|
":status" => self.editor.as_ref().map(|e|Bsp::e(e.clip_status(), e.edit_status())).boxed(),
|
||||||
|
":toolbar" => Fill::x(Fixed::y(2, Align::x(ClockView::new(true, &self.clock)))).boxed(),
|
||||||
|
":tracks" => self.row(self.w(), 3,
|
||||||
|
track_header(&self), track_cells(&self)).boxed(),
|
||||||
|
":inputs" => self.row(self.w(), 3,
|
||||||
|
input_header(&self), input_cells(&self)).boxed(),
|
||||||
|
":outputs" => self.row(self.w(), 3,
|
||||||
|
output_header(&self), output_cells(&self)).boxed(),
|
||||||
|
":scenes" => self.row(self.w(), self.size.h().saturating_sub(9) as u16,
|
||||||
|
scene_header(&self), scene_cells(&self)).boxed(),
|
||||||
|
});
|
||||||
|
render!(TuiOut: (self: App) => self.size.of(EdnView::from_source(self, self.edn.as_ref())));
|
||||||
|
handle!(TuiIn: |self: App, input| Ok(None));
|
||||||
|
#[derive(Clone, Debug)] pub enum AppCommand {
|
||||||
|
Clear,
|
||||||
|
Clip(ClipCommand),
|
||||||
|
Clock(ClockCommand),
|
||||||
|
Color(ItemPalette),
|
||||||
|
Compact(Option<bool>),
|
||||||
|
Editor(MidiEditCommand),
|
||||||
|
Enqueue(Option<Arc<RwLock<MidiClip>>>),
|
||||||
|
History(isize),
|
||||||
|
Pool(PoolCommand),
|
||||||
|
Sampler(SamplerCommand),
|
||||||
|
Scene(SceneCommand),
|
||||||
|
Select(ArrangerSelection),
|
||||||
|
StopAll,
|
||||||
|
Track(TrackCommand),
|
||||||
|
Zoom(Option<usize>),
|
||||||
|
}
|
||||||
|
edn_command!(AppCommand: |state: App| {
|
||||||
|
("clear" [] Self::Clear)
|
||||||
|
("stop-all" [] Self::StopAll)
|
||||||
|
("compact" [c: bool ] Self::Compact(c))
|
||||||
|
("color" [c: Color] Self::Color(c.map(ItemPalette::from).unwrap_or_default()))
|
||||||
|
("history" [d: isize] Self::History(d.unwrap_or(0)))
|
||||||
|
("zoom" [z: usize] Self::Zoom(z))
|
||||||
|
("select" [s: ArrangerSelection] Self::Select(s.expect("no selection")))
|
||||||
|
("enqueue" [c: Arc<RwLock<MidiClip>>] Self::Enqueue(c))
|
||||||
|
("clock" [a, ..b] Self::Clock(ClockCommand::from_edn(state, &a.to_ref(), b)))
|
||||||
|
("track" [a, ..b] Self::Track(TrackCommand::from_edn(state, &a.to_ref(), b)))
|
||||||
|
("scene" [a, ..b] Self::Scene(SceneCommand::from_edn(state, &a.to_ref(), b)))
|
||||||
|
("clip" [a, ..b] Self::Clip(ClipCommand::from_edn(state, &a.to_ref(), b)))
|
||||||
|
("pool" [a, ..b] Self::Pool(PoolCommand::from_edn(state.pool.as_ref().expect("no pool"), &a.to_ref(), b)))
|
||||||
|
("editor" [a, ..b] Self::Editor(MidiEditCommand::from_edn(state.editor.as_ref().expect("no editor"), &a.to_ref(), b)))
|
||||||
|
("sampler" [a, ..b] Self::Sampler(SamplerCommand::from_edn(state.sampler.as_ref().expect("no sampler"), &a.to_ref(), b)))
|
||||||
|
});
|
||||||
|
#[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),
|
||||||
|
}
|
||||||
|
edn_command!(ClipCommand: |state: App| {
|
||||||
|
("get" [a: usize
|
||||||
|
,b: usize] Self::Get(a.unwrap(), b.unwrap()))
|
||||||
|
|
||||||
|
("put" [a: usize
|
||||||
|
,b: usize
|
||||||
|
,c: Option<Arc<RwLock<MidiClip>>>] Self::Put(a.unwrap(), b.unwrap(), c.unwrap()))
|
||||||
|
|
||||||
|
("enqueue" [a: usize
|
||||||
|
,b: usize] Self::Enqueue(a.unwrap(), b.unwrap()))
|
||||||
|
|
||||||
|
("edit" [a: Option<Arc<RwLock<MidiClip>>>] Self::Edit(a.unwrap()))
|
||||||
|
|
||||||
|
("loop" [a: usize
|
||||||
|
,b: usize
|
||||||
|
,c: bool] Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap()))
|
||||||
|
|
||||||
|
("color" [a: usize
|
||||||
|
,b: usize] Self::SetColor(a.unwrap(), b.unwrap(), ItemPalette::random()))
|
||||||
|
});
|
||||||
|
#[derive(Clone, Debug)] pub enum SceneCommand {
|
||||||
|
Add,
|
||||||
|
Del(usize),
|
||||||
|
Swap(usize, usize),
|
||||||
|
SetSize(usize),
|
||||||
|
SetZoom(usize),
|
||||||
|
SetColor(usize, ItemPalette),
|
||||||
|
Enqueue(usize),
|
||||||
|
}
|
||||||
|
edn_command!(SceneCommand: |state: App| {
|
||||||
|
("add" [] Self::Add)
|
||||||
|
("del" [a: usize] Self::Del(0))
|
||||||
|
("zoom" [a: usize] Self::SetZoom(a.unwrap()))
|
||||||
|
("color" [a: usize] Self::SetColor(a.unwrap(), ItemPalette::random()))
|
||||||
|
("enqueue" [a: usize] Self::Enqueue(a.unwrap()))
|
||||||
|
("swap" [a: usize, b: usize] Self::Swap(a.unwrap(), b.unwrap()))
|
||||||
|
});
|
||||||
|
#[derive(Clone, Debug)] pub enum TrackCommand {
|
||||||
|
Add,
|
||||||
|
Del(usize),
|
||||||
|
Stop(usize),
|
||||||
|
Swap(usize, usize),
|
||||||
|
SetSize(usize),
|
||||||
|
SetZoom(usize),
|
||||||
|
SetColor(usize, ItemPalette),
|
||||||
|
}
|
||||||
|
edn_command!(TrackCommand: |state: App| {
|
||||||
|
("add" [] Self::Add)
|
||||||
|
("size" [a: usize] Self::SetSize(a.unwrap()))
|
||||||
|
("zoom" [a: usize] Self::SetZoom(a.unwrap()))
|
||||||
|
("color" [a: usize] Self::SetColor(a.unwrap(), ItemPalette::random()))
|
||||||
|
("del" [a: usize] Self::Del(a.unwrap()))
|
||||||
|
("stop" [a: usize] Self::Stop(a.unwrap()))
|
||||||
|
("swap" [a: usize, b: usize] Self::Swap(a.unwrap(), b.unwrap()))
|
||||||
|
});
|
||||||
audio!(|self: App, client, scope|{
|
audio!(|self: App, client, scope|{
|
||||||
// Start profiling cycle
|
// Start profiling cycle
|
||||||
let t0 = self.perf.get_t0();
|
let t0 = self.perf.get_t0();
|
||||||
|
|
@ -237,61 +454,32 @@ audio!(|self: App, client, scope|{
|
||||||
self.perf.update(t0, scope);
|
self.perf.update(t0, scope);
|
||||||
Control::Continue
|
Control::Continue
|
||||||
});
|
});
|
||||||
has_size!(<TuiOut>|self: App|&self.size);
|
/// Hosts the JACK callback for a collection of tracks
|
||||||
has_clock!(|self: App|&self.clock);
|
struct TracksAudio<'a>(
|
||||||
has_clips!(|self: App|self.pool.as_ref().expect("no clip pool").clips);
|
// Track collection
|
||||||
has_editor!(|self: App|self.editor.as_ref().expect("no editor"));
|
&'a mut [Track],
|
||||||
edn_provide!(u16: |self: App|{
|
/// Note buffer
|
||||||
":sample-h" => if self.compact() { 0 } else { 5 },
|
&'a mut Vec<u8>,
|
||||||
":samples-w" => if self.compact() { 4 } else { 11 },
|
/// Note chunk buffer
|
||||||
":samples-y" => if self.compact() { 1 } else { 0 },
|
&'a mut Vec<Vec<Vec<u8>>>,
|
||||||
":pool-w" => if self.compact() { 5 } else {
|
);
|
||||||
let w = self.size.w();
|
impl Audio for TracksAudio<'_> {
|
||||||
if w > 60 { 20 } else if w > 40 { 15 } else { 10 }
|
fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
||||||
|
let model = &mut self.0;
|
||||||
|
let note_buffer = &mut self.1;
|
||||||
|
let output_buffer = &mut self.2;
|
||||||
|
for track in model.iter_mut() {
|
||||||
|
if PlayerAudio(track.player_mut(), note_buffer, output_buffer).process(client, scope) == Control::Quit {
|
||||||
|
return Control::Quit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Control::Continue
|
||||||
}
|
}
|
||||||
});
|
|
||||||
edn_provide!(Color: |self: App| { _ => return None });
|
|
||||||
edn_provide!(usize: |self: App| { _ => return None });
|
|
||||||
edn_provide!(isize: |self: App| { _ => return None });
|
|
||||||
edn_provide!(bool: |self: App| { _ => return None });
|
|
||||||
edn_provide!(ArrangerSelection: |self: App| { _ => return None });
|
|
||||||
edn_provide!(Arc<RwLock<MidiClip>>: |self: App| { _ => return None });
|
|
||||||
handle!(TuiIn: |self: App, input| Ok(None));
|
|
||||||
#[derive(Clone, Debug)] pub enum AppCommand {
|
|
||||||
Clear,
|
|
||||||
Clip(ClipCommand),
|
|
||||||
Clock(ClockCommand),
|
|
||||||
Color(ItemPalette),
|
|
||||||
Compact(Option<bool>),
|
|
||||||
Editor(MidiEditCommand),
|
|
||||||
Enqueue(Option<Arc<RwLock<MidiClip>>>),
|
|
||||||
History(isize),
|
|
||||||
Pool(PoolCommand),
|
|
||||||
Sampler(SamplerCommand),
|
|
||||||
Scene(SceneCommand),
|
|
||||||
Select(ArrangerSelection),
|
|
||||||
StopAll,
|
|
||||||
Track(TrackCommand),
|
|
||||||
Zoom(Option<usize>),
|
|
||||||
}
|
}
|
||||||
edn_command!(AppCommand: |state: App| {
|
command!(|self: ClipCommand, state: App|match self { _ => todo!("clip command") });
|
||||||
("clear" [] Self::Clear)
|
command!(|self: SceneCommand, state: App|match self { _ => todo!("scene command") });
|
||||||
("stop-all" [] Self::StopAll)
|
command!(|self: TrackCommand, state: App|match self { _ => todo!("track command") });
|
||||||
("compact" [c: bool ] Self::Compact(c))
|
command!(|self: AppCommand, state: App|match self {
|
||||||
("color" [c: Color] Self::Color(c.map(ItemPalette::from).unwrap_or_default()))
|
|
||||||
("history" [d: isize] Self::History(d.unwrap_or(0)))
|
|
||||||
("zoom" [z: usize] Self::Zoom(z))
|
|
||||||
("select" [s: ArrangerSelection] Self::Select(s.expect("no selection")))
|
|
||||||
("enqueue" [c: Arc<RwLock<MidiClip>>] Self::Enqueue(c))
|
|
||||||
("clock" [a, ..b] Self::Clock(ClockCommand::from_edn(state, &a.to_ref(), b)))
|
|
||||||
("track" [a, ..b] Self::Track(TrackCommand::from_edn(state, &a.to_ref(), b)))
|
|
||||||
("scene" [a, ..b] Self::Scene(SceneCommand::from_edn(state, &a.to_ref(), b)))
|
|
||||||
("clip" [a, ..b] Self::Clip(ClipCommand::from_edn(state, &a.to_ref(), b)))
|
|
||||||
("pool" [a, ..b] Self::Pool(PoolCommand::from_edn(state.pool.as_ref().expect("no pool"), &a.to_ref(), b)))
|
|
||||||
("editor" [a, ..b] Self::Editor(MidiEditCommand::from_edn(state.editor.as_ref().expect("no editor"), &a.to_ref(), b)))
|
|
||||||
("sampler" [a, ..b] Self::Sampler(SamplerCommand::from_edn(state.sampler.as_ref().expect("no sampler"), &a.to_ref(), b)))
|
|
||||||
});
|
|
||||||
command!(|self: AppCommand, state: App|match self {
|
|
||||||
Self::Clear => { todo!() },
|
Self::Clear => { todo!() },
|
||||||
Self::Zoom(_) => { todo!(); },
|
Self::Zoom(_) => { todo!(); },
|
||||||
Self::History(delta) => { todo!("undo/redo") },
|
Self::History(delta) => { todo!("undo/redo") },
|
||||||
|
|
@ -382,88 +570,13 @@ command!(|self: AppCommand, state: App|match self {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
render!(TuiOut: (self: App) => self.size.of(EdnView::from_source(self, self.edn.as_ref())));
|
|
||||||
edn_provide!('a: Box<dyn Render<TuiOut> + 'a>: |self: App|{
|
|
||||||
":editor" => (&self.editor).boxed(),
|
|
||||||
":pool" => self.pool.as_ref().map(|pool|Align::e(Fixed::x(self.sidebar_w(), PoolView(self.compact(), pool)))).boxed(),
|
|
||||||
":sample" => self.sample().boxed(),
|
|
||||||
":sampler" => self.sampler().boxed(),
|
|
||||||
":status" => self.editor.as_ref().map(|e|Bsp::e(e.clip_status(), e.edit_status())).boxed(),
|
|
||||||
":toolbar" => Fill::x(Fixed::y(2, Align::x(ClockView::new(true, &self.clock)))).boxed(),
|
|
||||||
":tracks" => self.row(self.w(), 3, track_header(&self), track_cells(&self)).boxed(),
|
|
||||||
":inputs" => self.row(self.w(), 3, input_header(&self), input_cells(&self)).boxed(),
|
|
||||||
":outputs" => self.row(self.w(), 3, output_header(&self), output_cells(&self)).boxed(),
|
|
||||||
":scenes" => self.scene_row(self.w(), self.size.h().saturating_sub(9) as u16).boxed(),
|
|
||||||
});
|
|
||||||
impl App {
|
|
||||||
fn compact (&self) -> bool { false }
|
|
||||||
fn editor (&self) -> impl Content<TuiOut> + '_ { &self.editor }
|
|
||||||
fn w (&self) -> u16 { self.tracks_with_sizes().last().map(|x|x.3 as u16).unwrap_or(0) }
|
|
||||||
fn pool (&self) -> impl Content<TuiOut> + use<'_> { self.pool.as_ref().map(|pool|Align::e(Fixed::x(self.sidebar_w(), PoolView(self.compact(), pool)))) }
|
|
||||||
fn sample <'a> (&'a self) -> impl Content<TuiOut> + 'a {
|
|
||||||
let compact = self.is_editing();
|
|
||||||
if let (Some(editor), Some(sampler)) = (&self.editor, &self.sampler) {
|
|
||||||
let note_pt = editor.note_point();
|
|
||||||
let sample_h = if compact { 0 } else { 5 };
|
|
||||||
return Some(Max::y(sample_h, Fill::x(sampler.viewer(note_pt))))
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
fn sampler (&self) -> impl Content<TuiOut> + use<'_> {
|
|
||||||
let compact = self.is_editing();
|
|
||||||
if let (Some(editor), Some(sampler)) = (&self.editor, &self.sampler) {
|
|
||||||
let note_pt = editor.note_point();
|
|
||||||
let w = if compact { 4 } else { 40 };
|
|
||||||
let y = if compact { 1 } else { 0 };
|
|
||||||
return Some(Fixed::x(w, Push::y(y, Fill::y(sampler.list(compact, editor)))))
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
fn 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::xy(self.sidebar_w() as u16, h, a),
|
|
||||||
Fill::x(Align::c(Fixed::xy(w, h, b)))
|
|
||||||
))
|
|
||||||
}
|
|
||||||
pub fn tracks_with_sizes (&self)
|
|
||||||
-> impl Iterator<Item = (usize, &ArrangerTrack, usize, usize)>
|
|
||||||
{
|
|
||||||
tracks_with_sizes(self.tracks.iter(), match self.selected {
|
|
||||||
ArrangerSelection::Track(t) if self.is_editing() => Some(t),
|
|
||||||
ArrangerSelection::Clip(t, _) if self.is_editing() => Some(t),
|
|
||||||
_ => None
|
|
||||||
}, self.editor_w())
|
|
||||||
}
|
|
||||||
pub fn scenes_with_sizes (&self, h: usize)
|
|
||||||
-> impl Iterator<Item = (usize, &ArrangerScene, usize, usize)>
|
|
||||||
{
|
|
||||||
scenes_with_sizes(self.scenes.iter(), &self.selected, self.is_editing(), 2, 15)
|
|
||||||
}
|
|
||||||
fn is_editing (&self) -> bool {
|
|
||||||
self.editing.load(Relaxed)
|
|
||||||
}
|
|
||||||
fn editor_w (&self) -> usize {
|
|
||||||
let editor = self.editor.as_ref().expect("missing editor");
|
|
||||||
(5 + (editor.time_len().get() / editor.time_zoom().get()))
|
|
||||||
.min(self.size.w().saturating_sub(20))
|
|
||||||
.max(16)
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn scenes_with_sizes <'a>(
|
pub fn scenes_with_sizes <'a>(
|
||||||
scenes: impl Iterator<Item=&'a ArrangerScene> + 'a,
|
scenes: impl Iterator<Item=&'a Scene> + 'a,
|
||||||
selected: &'a ArrangerSelection,
|
selected: &'a ArrangerSelection,
|
||||||
editing: bool,
|
editing: bool,
|
||||||
scene_height: usize,
|
scene_height: usize,
|
||||||
scene_larger: usize,
|
scene_larger: usize,
|
||||||
) -> impl Iterator<Item = (usize, &'a ArrangerScene, usize, usize)> + 'a {
|
) -> impl Iterator<Item = (usize, &'a Scene, usize, usize)> + 'a {
|
||||||
let mut y = 0;
|
let mut y = 0;
|
||||||
let (selected_track, selected_scene) = match selected {
|
let (selected_track, selected_scene) = match selected {
|
||||||
ArrangerSelection::Clip(t, s) => (Some(t), Some(s)),
|
ArrangerSelection::Clip(t, s) => (Some(t), Some(s)),
|
||||||
|
|
@ -478,10 +591,10 @@ pub fn scenes_with_sizes <'a>(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
pub fn tracks_with_sizes <'a> (
|
pub fn tracks_with_sizes <'a> (
|
||||||
tracks: impl Iterator<Item=&'a ArrangerTrack>,
|
tracks: impl Iterator<Item=&'a Track>,
|
||||||
active: Option<usize>,
|
active: Option<usize>,
|
||||||
bigger: usize
|
bigger: usize
|
||||||
) -> impl Iterator<Item=(usize,&'a ArrangerTrack,usize,usize)> {
|
) -> impl Iterator<Item=(usize,&'a Track,usize,usize)> {
|
||||||
let mut x = 0;
|
let mut x = 0;
|
||||||
tracks.enumerate().map(move |(index, track)|{
|
tracks.enumerate().map(move |(index, track)|{
|
||||||
let width = if Some(index) == active { bigger } else { track.width.max(8) };
|
let width = if Some(index) == active { bigger } else { track.width.max(8) };
|
||||||
|
|
@ -491,17 +604,10 @@ pub fn tracks_with_sizes <'a> (
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
pub fn track_header <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> {
|
pub fn track_header <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> {
|
||||||
(||Tui::bg(TuiTheme::g(32), Tui::bold(true, Bsp::s(
|
(||Tui::bg(TuiTheme::g(32), Bsp::s(
|
||||||
row!(
|
help_tag("add ", "t", "rack"),
|
||||||
Tui::fg(TuiTheme::g(128), "add "),
|
help_tag("", "a", "dd scene"),
|
||||||
Tui::fg(TuiTheme::orange(), "t"),
|
)).boxed()).into()
|
||||||
Tui::fg(TuiTheme::g(128), "rack"),
|
|
||||||
),
|
|
||||||
row!(
|
|
||||||
Tui::fg(TuiTheme::orange(), "a"),
|
|
||||||
Tui::fg(TuiTheme::g(128), "dd scene"),
|
|
||||||
),
|
|
||||||
))).boxed()).into()
|
|
||||||
}
|
}
|
||||||
pub fn track_cells <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> {
|
pub fn track_cells <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> {
|
||||||
let iter = ||state.tracks_with_sizes();
|
let iter = ||state.tracks_with_sizes();
|
||||||
|
|
@ -518,7 +624,7 @@ pub fn track_cells <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> {
|
||||||
))
|
))
|
||||||
})).boxed()).into()
|
})).boxed()).into()
|
||||||
}
|
}
|
||||||
fn help_tag <'a>(before: &'a str, key: &'a str, after: &'a str) -> impl Content<TuiOut> + 'a {
|
fn help_tag <'a> (before: &'a str, key: &'a str, after: &'a str) -> impl Content<TuiOut> + 'a {
|
||||||
let lo = TuiTheme::g(128);
|
let lo = TuiTheme::g(128);
|
||||||
let hi = TuiTheme::orange();
|
let hi = TuiTheme::orange();
|
||||||
Tui::bold(true, row!(Tui::fg(lo, before), Tui::fg(hi, key), Tui::fg(lo, after)))
|
Tui::bold(true, row!(Tui::fg(lo, before), Tui::fg(hi, key), Tui::fg(lo, after)))
|
||||||
|
|
@ -632,7 +738,7 @@ fn scene_cells <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> {
|
||||||
}))).boxed()).into()
|
}))).boxed()).into()
|
||||||
}
|
}
|
||||||
fn cell_clip <'a> (
|
fn cell_clip <'a> (
|
||||||
scene: &'a ArrangerScene, index: usize, track: &'a ArrangerTrack, w: u16, h: u16
|
scene: &'a Scene, index: usize, track: &'a Track, w: u16, h: u16
|
||||||
) -> impl Content<TuiOut> + use<'a> {
|
) -> impl Content<TuiOut> + use<'a> {
|
||||||
scene.clips.get(index).map(|clip|clip.as_ref().map(|clip|{
|
scene.clips.get(index).map(|clip|clip.as_ref().map(|clip|{
|
||||||
let clip = clip.read().unwrap();
|
let clip = clip.read().unwrap();
|
||||||
|
|
@ -668,6 +774,290 @@ fn mute_solo (bg: Color, mute: bool, solo: bool) -> impl Content<TuiOut> {
|
||||||
fn cell <T: Content<TuiOut>> (color: ItemPalette, field: T) -> impl Content<TuiOut> {
|
fn cell <T: Content<TuiOut>> (color: ItemPalette, field: T) -> impl Content<TuiOut> {
|
||||||
Tui::fg_bg(color.lightest.rgb, color.base.rgb, Fixed::y(1, field))
|
Tui::fg_bg(color.lightest.rgb, color.base.rgb, Fixed::y(1, field))
|
||||||
}
|
}
|
||||||
|
pub const TRACK_MIN_WIDTH: usize = 9;
|
||||||
|
pub trait Arrangement: HasClock + HasJack {
|
||||||
|
fn tracks (&self) -> &Vec<Track>;
|
||||||
|
fn tracks_mut (&mut self) -> &mut Vec<Track>;
|
||||||
|
fn scenes (&self) -> &Vec<Scene>;
|
||||||
|
fn scenes_mut (&mut self) -> &mut Vec<Scene>;
|
||||||
|
fn selected (&self) -> &ArrangerSelection;
|
||||||
|
fn selected_mut (&mut self) -> &mut ArrangerSelection;
|
||||||
|
|
||||||
|
fn track_next_name (&self) -> Arc<str> {
|
||||||
|
format!("Trk{: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))
|
||||||
|
}
|
||||||
|
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 track_add (&mut self, name: Option<&str>, color: Option<ItemPalette>)
|
||||||
|
-> Usually<&mut Track>
|
||||||
|
{
|
||||||
|
let name = name.map_or_else(||self.track_next_name(), |x|x.to_string().into());
|
||||||
|
let track = Track {
|
||||||
|
width: (name.len() + 2).max(9),
|
||||||
|
color: color.unwrap_or_else(ItemPalette::random),
|
||||||
|
player: MidiPlayer::from(self.clock()),
|
||||||
|
name,
|
||||||
|
};
|
||||||
|
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(&mut self.tracks_mut()[index])
|
||||||
|
}
|
||||||
|
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))?;
|
||||||
|
track.width = width;
|
||||||
|
let port = JackPort::<MidiIn>::new(&jack, &format!("{}I", &track.name), midi_from)?;
|
||||||
|
track.player.midi_ins.push(port);
|
||||||
|
let port = JackPort::<MidiOut>::new(&jack, &format!("{}O", &track.name), midi_to)?;
|
||||||
|
track.player.midi_outs.push(port);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scene_default_name (&self) -> Arc<str> {
|
||||||
|
format!("Sc{:3>}", self.scenes().len() + 1).into()
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
todo!("delete scene");
|
||||||
|
}
|
||||||
|
fn scene_add (&mut self, name: Option<&str>, color: Option<ItemPalette>)
|
||||||
|
-> Usually<&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(&mut self.scenes_mut()[index])
|
||||||
|
}
|
||||||
|
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 _scene = self.scene_add(None, Some(
|
||||||
|
scene_color_1.mix(scene_color_2, i as f32 / n as f32).into()
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn activate (&mut self) -> Usually<()> {
|
||||||
|
let selected = self.selected().clone();
|
||||||
|
match selected {
|
||||||
|
ArrangerSelection::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))?;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ArrangerSelection::Clip(t, s) => {
|
||||||
|
let clip = self.scenes()[s].clips[t].clone();
|
||||||
|
self.tracks_mut()[t].player.enqueue_next(clip.as_ref());
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//fn randomize_color (&mut self) {
|
||||||
|
//match self.selected {
|
||||||
|
//ArrangerSelection::Mix => { self.color = ItemPalette::random() },
|
||||||
|
//ArrangerSelection::Track(t) => { self.tracks[t].color = ItemPalette::random() },
|
||||||
|
//ArrangerSelection::Scene(s) => { self.scenes[s].color = ItemPalette::random() },
|
||||||
|
//ArrangerSelection::Clip(t, s) => if let Some(clip) = &self.scenes[s].clips[t] {
|
||||||
|
//clip.write().unwrap().color = ItemPalette::random();
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Scene {
|
||||||
|
pub fn longest_name (scenes: &[Self]) -> usize {
|
||||||
|
scenes.iter().map(|s|s.name.len()).fold(0, usize::max)
|
||||||
|
}
|
||||||
|
/// Returns the pulse length of the longest clip in the scene
|
||||||
|
pub 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.
|
||||||
|
pub fn is_playing (&self, tracks: &[Track]) -> bool {
|
||||||
|
self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate()
|
||||||
|
.all(|(track_index, clip)|match clip {
|
||||||
|
Some(c) => tracks
|
||||||
|
.get(track_index)
|
||||||
|
.map(|track|{
|
||||||
|
if let Some((_, Some(clip))) = track.player().play_clip() {
|
||||||
|
*clip.read().unwrap() == *c.read().unwrap()
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or(false),
|
||||||
|
None => true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn clip (&self, index: usize) -> Option<&Arc<RwLock<MidiClip>>> {
|
||||||
|
match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Track {
|
||||||
|
fn longest_name (tracks: &[Self]) -> usize {
|
||||||
|
tracks.iter().map(|s|s.name.len()).fold(0, usize::max)
|
||||||
|
}
|
||||||
|
fn width_inc (&mut self) {
|
||||||
|
self.width += 1;
|
||||||
|
}
|
||||||
|
fn width_dec (&mut self) {
|
||||||
|
if self.width > TRACK_MIN_WIDTH {
|
||||||
|
self.width -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(PartialEq, Clone, Copy, Debug, Default)]
|
||||||
|
/// Represents the current user selection in the arranger
|
||||||
|
pub enum ArrangerSelection {
|
||||||
|
/// 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 ArrangerSelection {
|
||||||
|
pub fn track (&self) -> Option<usize> {
|
||||||
|
use ArrangerSelection::*;
|
||||||
|
match self {
|
||||||
|
Clip(t, _) => Some(*t),
|
||||||
|
Track(t) => Some(*t),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn scene (&self) -> Option<usize> {
|
||||||
|
use ArrangerSelection::*;
|
||||||
|
match self {
|
||||||
|
Clip(_, s) => Some(*s),
|
||||||
|
Scene(s) => Some(*s),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn is_mix (&self) -> bool { matches!(self, Self::Mix) }
|
||||||
|
pub fn is_track (&self) -> bool { matches!(self, Self::Track(_)) }
|
||||||
|
pub fn is_scene (&self) -> bool { matches!(self, Self::Scene(_)) }
|
||||||
|
pub fn is_clip (&self) -> bool { matches!(self, Self::Clip(_, _)) }
|
||||||
|
pub 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 Arrangement for App {
|
||||||
|
fn tracks (&self) -> &Vec<Track> { &self.tracks }
|
||||||
|
fn tracks_mut (&mut self) -> &mut Vec<Track> { &mut self.tracks }
|
||||||
|
fn scenes (&self) -> &Vec<Scene> { &self.scenes }
|
||||||
|
fn scenes_mut (&mut self) -> &mut Vec<Scene> { &mut self.scenes }
|
||||||
|
fn selected (&self) -> &ArrangerSelection { &self.selected }
|
||||||
|
fn selected_mut (&mut self) -> &mut ArrangerSelection { &mut self.selected }
|
||||||
|
}
|
||||||
|
//impl Arrangement for Arranger {
|
||||||
|
//fn tracks (&self) -> &Vec<Track> { &self.tracks }
|
||||||
|
//fn tracks_mut (&mut self) -> &mut Vec<Track> { &mut self.tracks }
|
||||||
|
//fn scenes (&self) -> &Vec<Scene> { &self.scenes }
|
||||||
|
//fn scenes_mut (&mut self) -> &mut Vec<Scene> { &mut self.scenes }
|
||||||
|
//fn selected (&self) -> &ArrangerSelection { &self.selected }
|
||||||
|
//fn selected_mut (&mut self) -> &mut ArrangerSelection { &mut self.selected }
|
||||||
|
//}
|
||||||
|
render!(TuiOut: (self: Meter<'a>) => col!(
|
||||||
|
Field(TuiTheme::g(128).into(), self.0, format!("{:>+9.3}", self.1)),
|
||||||
|
Fixed::xy(if self.1 >= 0.0 { 13 }
|
||||||
|
else if self.1 >= -1.0 { 12 }
|
||||||
|
else if self.1 >= -2.0 { 11 }
|
||||||
|
else if self.1 >= -3.0 { 10 }
|
||||||
|
else if self.1 >= -4.0 { 9 }
|
||||||
|
else if self.1 >= -6.0 { 8 }
|
||||||
|
else if self.1 >= -9.0 { 7 }
|
||||||
|
else if self.1 >= -12.0 { 6 }
|
||||||
|
else if self.1 >= -15.0 { 5 }
|
||||||
|
else if self.1 >= -20.0 { 4 }
|
||||||
|
else if self.1 >= -25.0 { 3 }
|
||||||
|
else if self.1 >= -30.0 { 2 }
|
||||||
|
else if self.1 >= -40.0 { 1 }
|
||||||
|
else { 0 }, 1, Tui::bg(if self.1 >= 0.0 { Color::Red }
|
||||||
|
else if self.1 >= -3.0 { Color::Yellow }
|
||||||
|
else { Color::Green }, ()))));
|
||||||
|
render!(TuiOut: (self: Meters<'a>) => col!(
|
||||||
|
format!("L/{:>+9.3}", self.0[0]),
|
||||||
|
format!("R/{:>+9.3}", self.0[1])));
|
||||||
|
|
||||||
#[cfg(test)] fn test_tek () {
|
#[cfg(test)] fn test_tek () {
|
||||||
// TODO
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,5 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
pub struct Meter<'a>(pub &'a str, pub f32);
|
|
||||||
render!(TuiOut: (self: Meter<'a>) => col!(
|
|
||||||
Field(TuiTheme::g(128).into(), self.0, format!("{:>+9.3}", self.1)),
|
|
||||||
Fixed::xy(if self.1 >= 0.0 { 13 }
|
|
||||||
else if self.1 >= -1.0 { 12 }
|
|
||||||
else if self.1 >= -2.0 { 11 }
|
|
||||||
else if self.1 >= -3.0 { 10 }
|
|
||||||
else if self.1 >= -4.0 { 9 }
|
|
||||||
else if self.1 >= -6.0 { 8 }
|
|
||||||
else if self.1 >= -9.0 { 7 }
|
|
||||||
else if self.1 >= -12.0 { 6 }
|
|
||||||
else if self.1 >= -15.0 { 5 }
|
|
||||||
else if self.1 >= -20.0 { 4 }
|
|
||||||
else if self.1 >= -25.0 { 3 }
|
|
||||||
else if self.1 >= -30.0 { 2 }
|
|
||||||
else if self.1 >= -40.0 { 1 }
|
|
||||||
else { 0 }, 1, Tui::bg(if self.1 >= 0.0 { Color::Red }
|
|
||||||
else if self.1 >= -3.0 { Color::Yellow }
|
|
||||||
else { Color::Green }, ()))));
|
|
||||||
|
|
||||||
pub struct Meters<'a>(pub &'a[f32]);
|
|
||||||
render!(TuiOut: (self: Meters<'a>) => col!(
|
|
||||||
format!("L/{:>+9.3}", self.0[0]),
|
|
||||||
format!("R/{:>+9.3}", self.0[1])));
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Mixer {
|
pub struct Mixer {
|
||||||
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue