mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-02-01 08:36:42 +01:00
wip: scaffold PhrasePool, PhraseEditor
This commit is contained in:
parent
ea3edec96b
commit
d821787fcf
8 changed files with 1549 additions and 1536 deletions
455
crates/tek_sequencer/src/arranger.rs
Normal file
455
crates/tek_sequencer/src/arranger.rs
Normal file
|
|
@ -0,0 +1,455 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// Represents the tracks and scenes of the composition.
|
||||||
|
pub struct Arranger<E: Engine> {
|
||||||
|
/// Name of arranger
|
||||||
|
pub name: Arc<RwLock<String>>,
|
||||||
|
/// Collection of tracks.
|
||||||
|
pub tracks: Vec<Sequencer<E>>,
|
||||||
|
/// Collection of scenes.
|
||||||
|
pub scenes: Vec<Scene>,
|
||||||
|
/// Currently selected element.
|
||||||
|
pub selected: ArrangerFocus,
|
||||||
|
/// Display mode of arranger
|
||||||
|
pub mode: ArrangerViewMode,
|
||||||
|
/// Slot for modal dialog displayed on top of app.
|
||||||
|
pub modal: Option<Box<dyn ContentComponent<E>>>,
|
||||||
|
/// Whether the arranger is currently focused
|
||||||
|
pub focused: bool
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: Engine> Arranger<E> {
|
||||||
|
pub fn new (name: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
name: Arc::new(RwLock::new(name.into())),
|
||||||
|
mode: ArrangerViewMode::Vertical(2),
|
||||||
|
selected: ArrangerFocus::Clip(0, 0),
|
||||||
|
scenes: vec![],
|
||||||
|
tracks: vec![],
|
||||||
|
modal: None,
|
||||||
|
focused: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn activate (&mut self) {
|
||||||
|
match self.selected {
|
||||||
|
ArrangerFocus::Scene(s) => {
|
||||||
|
for (track_index, track) in self.tracks.iter_mut().enumerate() {
|
||||||
|
track.playing_phrase = self.scenes[s].clips[track_index];
|
||||||
|
track.reset = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ArrangerFocus::Clip(t, s) => {
|
||||||
|
self.tracks[t].playing_phrase = self.scenes[s].clips[t];
|
||||||
|
self.tracks[t].reset = true;
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn sequencer (&self) -> Option<&Sequencer<E>> {
|
||||||
|
self.selected.track()
|
||||||
|
.map(|track|self.tracks.get(track))
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
pub fn sequencer_mut (&mut self) -> Option<&mut Sequencer<E>> {
|
||||||
|
self.selected.track()
|
||||||
|
.map(|track|self.tracks.get_mut(track))
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
pub fn show_phrase (&mut self) {
|
||||||
|
let (scene, track) = (self.selected.scene(), self.selected.track());
|
||||||
|
if let (Some(scene_index), Some(track_index)) = (scene, track) {
|
||||||
|
let scene = self.scenes.get(scene_index);
|
||||||
|
let track = self.tracks.get_mut(track_index);
|
||||||
|
if let (Some(scene), Some(track)) = (scene, track) {
|
||||||
|
track.viewing_phrase = scene.clips[track_index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn is_first_row (&self) -> bool {
|
||||||
|
let selected = self.selected;
|
||||||
|
selected.is_mix() || selected.is_track()
|
||||||
|
}
|
||||||
|
pub fn is_last_row (&self) -> bool {
|
||||||
|
let selected = self.selected;
|
||||||
|
(self.scenes.len() == 0 && (selected.is_mix() || selected.is_track())) || match selected {
|
||||||
|
ArrangerFocus::Scene(s) =>
|
||||||
|
s == self.scenes.len() - 1,
|
||||||
|
ArrangerFocus::Clip(_, s) =>
|
||||||
|
s == self.scenes.len() - 1,
|
||||||
|
_ => false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn track (&self) -> Option<&Sequencer<E>> {
|
||||||
|
self.selected.track().map(|t|self.tracks.get(t)).flatten()
|
||||||
|
}
|
||||||
|
pub fn track_mut (&mut self) -> Option<&mut Sequencer<E>> {
|
||||||
|
self.selected.track().map(|t|self.tracks.get_mut(t)).flatten()
|
||||||
|
}
|
||||||
|
pub fn track_next (&mut self) {
|
||||||
|
self.selected.track_next(self.tracks.len() - 1)
|
||||||
|
}
|
||||||
|
pub fn track_prev (&mut self) {
|
||||||
|
self.selected.track_prev()
|
||||||
|
}
|
||||||
|
pub fn track_add (&mut self, name: Option<&str>) -> Usually<&mut Sequencer<E>> {
|
||||||
|
self.tracks.push(name.map_or_else(
|
||||||
|
|| Sequencer::new(&self.track_default_name()),
|
||||||
|
|name| Sequencer::new(name),
|
||||||
|
));
|
||||||
|
let index = self.tracks.len() - 1;
|
||||||
|
Ok(&mut self.tracks[index])
|
||||||
|
}
|
||||||
|
pub fn track_del (&mut self) {
|
||||||
|
unimplemented!("Arranger::track_del");
|
||||||
|
}
|
||||||
|
pub fn track_default_name (&self) -> String {
|
||||||
|
format!("Track {}", self.tracks.len() + 1)
|
||||||
|
}
|
||||||
|
pub fn scene (&self) -> Option<&Scene> {
|
||||||
|
self.selected.scene().map(|s|self.scenes.get(s)).flatten()
|
||||||
|
}
|
||||||
|
pub fn scene_mut (&mut self) -> Option<&mut Scene> {
|
||||||
|
self.selected.scene().map(|s|self.scenes.get_mut(s)).flatten()
|
||||||
|
}
|
||||||
|
pub fn scene_next (&mut self) {
|
||||||
|
self.selected.scene_next(self.scenes.len() - 1)
|
||||||
|
}
|
||||||
|
pub fn scene_prev (&mut self) {
|
||||||
|
self.selected.scene_prev()
|
||||||
|
}
|
||||||
|
pub fn scene_add (&mut self, name: Option<&str>) -> Usually<&mut Scene> {
|
||||||
|
let clips = vec![None;self.tracks.len()];
|
||||||
|
self.scenes.push(match name {
|
||||||
|
Some(name) => Scene::new(name, clips),
|
||||||
|
None => Scene::new(&self.scene_default_name(), clips),
|
||||||
|
});
|
||||||
|
let index = self.scenes.len() - 1;
|
||||||
|
Ok(&mut self.scenes[index])
|
||||||
|
}
|
||||||
|
pub fn scene_del (&mut self) {
|
||||||
|
unimplemented!("Arranger::scene_del");
|
||||||
|
}
|
||||||
|
pub fn scene_default_name (&self) -> String {
|
||||||
|
format!("Scene {}", self.scenes.len() + 1)
|
||||||
|
}
|
||||||
|
pub fn phrase (&self) -> Option<&Arc<RwLock<Phrase>>> {
|
||||||
|
let track_id = self.selected.track()?;
|
||||||
|
self.tracks.get(track_id)?.phrases.get((*self.scene()?.clips.get(track_id)?)?)
|
||||||
|
}
|
||||||
|
pub fn phrase_del (&mut self) {
|
||||||
|
let track_index = self.selected.track();
|
||||||
|
let scene_index = self.selected.scene();
|
||||||
|
track_index
|
||||||
|
.and_then(|index|self.tracks.get_mut(index).map(|track|(index, track)))
|
||||||
|
.map(|(track_index, _)|{
|
||||||
|
scene_index
|
||||||
|
.and_then(|index|self.scenes.get_mut(index))
|
||||||
|
.map(|scene|scene.clips[track_index] = None);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
pub fn phrase_next (&mut self) {
|
||||||
|
let track_index = self.selected.track();
|
||||||
|
let scene_index = self.selected.scene();
|
||||||
|
track_index
|
||||||
|
.and_then(|index|self.tracks.get_mut(index).map(|track|(index, track)))
|
||||||
|
.and_then(|(track_index, track)|{
|
||||||
|
let phrases = track.phrases.len();
|
||||||
|
scene_index
|
||||||
|
.and_then(|index|self.scenes.get_mut(index))
|
||||||
|
.and_then(|scene|{
|
||||||
|
if let Some(phrase_index) = scene.clips[track_index] {
|
||||||
|
if phrase_index >= phrases - 1 {
|
||||||
|
scene.clips[track_index] = None;
|
||||||
|
} else {
|
||||||
|
scene.clips[track_index] = Some(phrase_index + 1);
|
||||||
|
}
|
||||||
|
} else if phrases > 0 {
|
||||||
|
scene.clips[track_index] = Some(0);
|
||||||
|
}
|
||||||
|
Some(())
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
pub fn phrase_prev (&mut self) {
|
||||||
|
let track_index = self.selected.track();
|
||||||
|
let scene_index = self.selected.scene();
|
||||||
|
track_index
|
||||||
|
.and_then(|index|self.tracks.get_mut(index).map(|track|(index, track)))
|
||||||
|
.and_then(|(track_index, track)|{
|
||||||
|
let phrases = track.phrases.len();
|
||||||
|
scene_index
|
||||||
|
.and_then(|index|self.scenes.get_mut(index))
|
||||||
|
.and_then(|scene|{
|
||||||
|
if let Some(phrase_index) = scene.clips[track_index] {
|
||||||
|
scene.clips[track_index] = if phrase_index == 0 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(phrase_index - 1)
|
||||||
|
};
|
||||||
|
} else if phrases > 0 {
|
||||||
|
scene.clips[track_index] = Some(phrases - 1);
|
||||||
|
}
|
||||||
|
Some(())
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
#[derive(PartialEq, Clone, Copy)]
|
||||||
|
/// Represents the current user selection in the arranger
|
||||||
|
pub enum ArrangerFocus {
|
||||||
|
/** The whole mix is selected */
|
||||||
|
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 ArrangerFocus {
|
||||||
|
pub fn description <E: Engine> (
|
||||||
|
&self,
|
||||||
|
tracks: &Vec<Sequencer<E>>,
|
||||||
|
scenes: &Vec<Scene>,
|
||||||
|
) -> String {
|
||||||
|
format!("Selected: {}", match self {
|
||||||
|
Self::Mix => format!("Everything"),
|
||||||
|
Self::Track(t) => if let Some(track) = tracks.get(*t) {
|
||||||
|
format!("T{t}: {}", &track.name.read().unwrap())
|
||||||
|
} else {
|
||||||
|
format!("T??")
|
||||||
|
},
|
||||||
|
Self::Scene(s) => if let Some(scene) = scenes.get(*s) {
|
||||||
|
format!("S{s}: {}", &scene.name.read().unwrap())
|
||||||
|
} else {
|
||||||
|
format!("S??")
|
||||||
|
},
|
||||||
|
Self::Clip(t, s) => if let (Some(track), Some(scene)) = (
|
||||||
|
tracks.get(*t),
|
||||||
|
scenes.get(*s),
|
||||||
|
) {
|
||||||
|
if let Some(Some(slot)) = scene.clips.get(*t) {
|
||||||
|
if let Some(clip) = track.phrases.get(*slot) {
|
||||||
|
format!("T{t} S{s} C{slot} ({})", &clip.read().unwrap().name.read().unwrap())
|
||||||
|
} else {
|
||||||
|
format!("T{t} S{s}: Empty")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
format!("T{t} S{s}: Empty")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
format!("T{t} S{s}: Empty")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn is_mix (&self) -> bool {
|
||||||
|
match self { Self::Mix => true, _ => false }
|
||||||
|
}
|
||||||
|
pub fn is_track (&self) -> bool {
|
||||||
|
match self { Self::Track(_) => true, _ => false }
|
||||||
|
}
|
||||||
|
pub fn is_scene (&self) -> bool {
|
||||||
|
match self { Self::Scene(_) => true, _ => false }
|
||||||
|
}
|
||||||
|
pub fn is_clip (&self) -> bool {
|
||||||
|
match self { Self::Clip(_, _) => true, _ => false }
|
||||||
|
}
|
||||||
|
pub fn track (&self) -> Option<usize> {
|
||||||
|
match self {
|
||||||
|
Self::Clip(t, _) => Some(*t),
|
||||||
|
Self::Track(t) => Some(*t),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn track_next (&mut self, last_track: usize) {
|
||||||
|
*self = match self {
|
||||||
|
Self::Mix => Self::Track(0),
|
||||||
|
Self::Track(t) => Self::Track(last_track.min(*t + 1)),
|
||||||
|
Self::Scene(s) => Self::Clip(0, *s),
|
||||||
|
Self::Clip(t, s) => Self::Clip(last_track.min(*t + 1), *s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn track_prev (&mut self) {
|
||||||
|
*self = match self {
|
||||||
|
Self::Mix => Self::Mix,
|
||||||
|
Self::Scene(s) => Self::Scene(*s),
|
||||||
|
Self::Track(t) => if *t == 0 {
|
||||||
|
Self::Mix
|
||||||
|
} else {
|
||||||
|
Self::Track(*t - 1)
|
||||||
|
},
|
||||||
|
Self::Clip(t, s) => if *t == 0 {
|
||||||
|
Self::Scene(*s)
|
||||||
|
} else {
|
||||||
|
Self::Clip(t.saturating_sub(1), *s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn scene (&self) -> Option<usize> {
|
||||||
|
match self {
|
||||||
|
Self::Clip(_, s) => Some(*s),
|
||||||
|
Self::Scene(s) => Some(*s),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn scene_next (&mut self, last_scene: usize) {
|
||||||
|
*self = match self {
|
||||||
|
Self::Mix => Self::Scene(0),
|
||||||
|
Self::Track(t) => Self::Clip(*t, 0),
|
||||||
|
Self::Scene(s) => Self::Scene(last_scene.min(*s + 1)),
|
||||||
|
Self::Clip(t, s) => Self::Clip(*t, last_scene.min(*s + 1)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn scene_prev (&mut self) {
|
||||||
|
*self = match self {
|
||||||
|
Self::Mix => Self::Mix,
|
||||||
|
Self::Track(t) => Self::Track(*t),
|
||||||
|
Self::Scene(s) => if *s == 0 {
|
||||||
|
Self::Mix
|
||||||
|
} else {
|
||||||
|
Self::Scene(*s - 1)
|
||||||
|
},
|
||||||
|
Self::Clip(t, s) => if *s == 0 {
|
||||||
|
Self::Track(*t)
|
||||||
|
} else {
|
||||||
|
Self::Clip(*t, s.saturating_sub(1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
/// Display mode of arranger
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub enum ArrangerViewMode { Horizontal, Vertical(usize) }
|
||||||
|
/// Arranger display mode can be cycled
|
||||||
|
impl ArrangerViewMode {
|
||||||
|
/// Cycle arranger display mode
|
||||||
|
pub fn to_next (&mut self) {
|
||||||
|
*self = match self {
|
||||||
|
Self::Horizontal => Self::Vertical(1),
|
||||||
|
Self::Vertical(1) => Self::Vertical(2),
|
||||||
|
Self::Vertical(2) => Self::Vertical(2),
|
||||||
|
Self::Vertical(0) => Self::Horizontal,
|
||||||
|
Self::Vertical(_) => Self::Vertical(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
pub struct VerticalArranger<'a, E: Engine>(
|
||||||
|
pub &'a Arranger<E>, pub usize
|
||||||
|
);
|
||||||
|
pub struct VerticalArrangerGrid<'a>(
|
||||||
|
pub u16, pub &'a [(usize, usize)], pub &'a [(usize, usize)]
|
||||||
|
);
|
||||||
|
pub struct VerticalArrangerCursor<'a>(
|
||||||
|
pub bool, pub ArrangerFocus, pub u16, pub &'a [(usize, usize)], pub &'a [(usize, usize)],
|
||||||
|
);
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
pub struct HorizontalArranger<'a, E: Engine>(
|
||||||
|
pub &'a Arranger<E>
|
||||||
|
);
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
/// Appears on first run (i.e. if state dir is missing).
|
||||||
|
pub struct ArrangerRenameModal<E: Engine> {
|
||||||
|
_engine: std::marker::PhantomData<E>,
|
||||||
|
pub done: bool,
|
||||||
|
pub target: ArrangerFocus,
|
||||||
|
pub value: String,
|
||||||
|
pub result: Arc<RwLock<String>>,
|
||||||
|
pub cursor: usize
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: Engine> ArrangerRenameModal<E> {
|
||||||
|
pub fn new (target: ArrangerFocus, value: &Arc<RwLock<String>>) -> Self {
|
||||||
|
Self {
|
||||||
|
_engine: Default::default(),
|
||||||
|
done: false,
|
||||||
|
value: value.read().unwrap().clone(),
|
||||||
|
cursor: value.read().unwrap().len(),
|
||||||
|
result: value.clone(),
|
||||||
|
target,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: Engine + Send> Exit for ArrangerRenameModal<E> {
|
||||||
|
fn exited (&self) -> bool { self.done }
|
||||||
|
fn exit (&mut self) { self.done = true }
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
/// A collection of phrases to play on each track.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Scene {
|
||||||
|
pub name: Arc<RwLock<String>>,
|
||||||
|
pub clips: Vec<Option<usize>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Scene {
|
||||||
|
pub fn from_edn <'a, 'e> (args: &[Edn<'e>]) -> Usually<Self> {
|
||||||
|
let mut name = None;
|
||||||
|
let mut clips = vec![];
|
||||||
|
edn!(edn in args {
|
||||||
|
Edn::Map(map) => {
|
||||||
|
let key = map.get(&Edn::Key(":name"));
|
||||||
|
if let Some(Edn::Str(n)) = key {
|
||||||
|
name = Some(*n);
|
||||||
|
} else {
|
||||||
|
panic!("unexpected key in scene '{name:?}': {key:?}")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Edn::Symbol("_") => {
|
||||||
|
clips.push(None);
|
||||||
|
},
|
||||||
|
Edn::Int(i) => {
|
||||||
|
clips.push(Some(*i as usize));
|
||||||
|
},
|
||||||
|
_ => panic!("unexpected in scene '{name:?}': {edn:?}")
|
||||||
|
});
|
||||||
|
let scene = Self::new(name.unwrap_or(""), clips);
|
||||||
|
Ok(scene)
|
||||||
|
}
|
||||||
|
pub fn new (name: impl AsRef<str>, clips: impl AsRef<[Option<usize>]>) -> Self {
|
||||||
|
let name = Arc::new(RwLock::new(name.as_ref().into()));
|
||||||
|
let clips = clips.as_ref().iter().map(|x|x.clone()).collect();
|
||||||
|
Self { name, clips, }
|
||||||
|
}
|
||||||
|
/// Returns the pulse length of the longest phrase in the scene
|
||||||
|
pub fn pulses <E: Engine> (&self, tracks: &[Sequencer<E>]) -> usize {
|
||||||
|
self.clips.iter().enumerate()
|
||||||
|
.filter_map(|(i, c)|c
|
||||||
|
.map(|c|tracks
|
||||||
|
.get(i)
|
||||||
|
.map(|track|track
|
||||||
|
.phrases
|
||||||
|
.get(c))))
|
||||||
|
.filter_map(|p|p)
|
||||||
|
.filter_map(|p|p)
|
||||||
|
.fold(0, |a, p|a.max(p.read().unwrap().length))
|
||||||
|
}
|
||||||
|
/// Returns true if all phrases in the scene are currently playing
|
||||||
|
pub fn is_playing <E: Engine> (&self, tracks: &[Sequencer<E>]) -> bool {
|
||||||
|
self.clips.iter().enumerate()
|
||||||
|
.all(|(track_index, phrase_index)|match phrase_index {
|
||||||
|
Some(i) => tracks
|
||||||
|
.get(track_index)
|
||||||
|
.map(|track|track.playing_phrase == Some(*i))
|
||||||
|
.unwrap_or(false),
|
||||||
|
None => true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
629
crates/tek_sequencer/src/arranger_tui.rs
Normal file
629
crates/tek_sequencer/src/arranger_tui.rs
Normal file
|
|
@ -0,0 +1,629 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
impl Arranger<Tui> {
|
||||||
|
pub fn rename_selected (&mut self) {
|
||||||
|
self.modal = Some(Box::new(ArrangerRenameModal::new(
|
||||||
|
self.selected,
|
||||||
|
&match self.selected {
|
||||||
|
ArrangerFocus::Mix => self.name.clone(),
|
||||||
|
ArrangerFocus::Track(t) => self.tracks[t].name.clone(),
|
||||||
|
ArrangerFocus::Scene(s) => self.scenes[s].name.clone(),
|
||||||
|
ArrangerFocus::Clip(t, s) => self.tracks[t].phrases[s].read().unwrap().name.clone(),
|
||||||
|
}
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Focusable<Tui> for Arranger<Tui> {
|
||||||
|
fn is_focused (&self) -> bool {
|
||||||
|
self.focused
|
||||||
|
}
|
||||||
|
fn set_focused (&mut self, focused: bool) {
|
||||||
|
self.focused = focused
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Handle<Tui> for Arranger<Tui> {
|
||||||
|
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||||
|
if let Some(modal) = self.modal.as_mut() {
|
||||||
|
let result = modal.handle(from)?;
|
||||||
|
if from.is_done() {
|
||||||
|
self.modal = None;
|
||||||
|
}
|
||||||
|
return Ok(result)
|
||||||
|
}
|
||||||
|
match from.event() {
|
||||||
|
// mode_switch: switch the display mode
|
||||||
|
key!(KeyCode::Char('`')) => {
|
||||||
|
self.mode.to_next()
|
||||||
|
},
|
||||||
|
// cursor_up: move cursor up
|
||||||
|
key!(KeyCode::Up) => {
|
||||||
|
match self.mode {
|
||||||
|
ArrangerViewMode::Horizontal => self.track_prev(),
|
||||||
|
_ => self.scene_prev(),
|
||||||
|
};
|
||||||
|
self.show_phrase();
|
||||||
|
},
|
||||||
|
// cursor_down
|
||||||
|
key!(KeyCode::Down) => {
|
||||||
|
match self.mode {
|
||||||
|
ArrangerViewMode::Horizontal => self.track_next(),
|
||||||
|
_ => self.scene_next(),
|
||||||
|
};
|
||||||
|
self.show_phrase();
|
||||||
|
},
|
||||||
|
// cursor left
|
||||||
|
key!(KeyCode::Left) => {
|
||||||
|
match self.mode {
|
||||||
|
ArrangerViewMode::Horizontal => self.scene_prev(),
|
||||||
|
_ => self.track_prev(),
|
||||||
|
};
|
||||||
|
self.show_phrase();
|
||||||
|
},
|
||||||
|
// cursor right
|
||||||
|
key!(KeyCode::Right) => {
|
||||||
|
match self.mode {
|
||||||
|
ArrangerViewMode::Horizontal => self.scene_next(),
|
||||||
|
_ => self.track_next(),
|
||||||
|
};
|
||||||
|
self.show_phrase();
|
||||||
|
},
|
||||||
|
// increment: remove clip
|
||||||
|
key!(KeyCode::Delete) => {
|
||||||
|
self.phrase_del();
|
||||||
|
self.show_phrase();
|
||||||
|
},
|
||||||
|
// increment: use next clip here
|
||||||
|
key!(KeyCode::Char('.')) => {
|
||||||
|
self.phrase_next();
|
||||||
|
self.show_phrase();
|
||||||
|
},
|
||||||
|
// decrement: use previous next clip here
|
||||||
|
key!(KeyCode::Char(',')) => {
|
||||||
|
self.phrase_prev();
|
||||||
|
self.show_phrase();
|
||||||
|
},
|
||||||
|
// decrement: use previous clip here
|
||||||
|
key!(KeyCode::Enter) => {
|
||||||
|
self.activate();
|
||||||
|
},
|
||||||
|
// scene_add: add a new scene
|
||||||
|
key!(Ctrl-KeyCode::Char('a')) => {
|
||||||
|
self.scene_add(None)?;
|
||||||
|
},
|
||||||
|
// track_add: add a new scene
|
||||||
|
key!(Ctrl-KeyCode::Char('t')) => {
|
||||||
|
self.track_add(None)?;
|
||||||
|
},
|
||||||
|
// rename: add a new scene
|
||||||
|
key!(KeyCode::Char('n')) => {
|
||||||
|
self.rename_selected();
|
||||||
|
},
|
||||||
|
// length: add a new scene
|
||||||
|
key!(KeyCode::Char('l')) => if let Some(phrase) = self.phrase() {
|
||||||
|
phrase.write().unwrap().toggle_loop()
|
||||||
|
},
|
||||||
|
// color: set color of item at cursor
|
||||||
|
key!(KeyCode::Char('c')) => {
|
||||||
|
todo!();
|
||||||
|
},
|
||||||
|
_ => return Ok(None)
|
||||||
|
}
|
||||||
|
Ok(Some(true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Content for Arranger<Tui> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||||
|
Layers::new(move |add|{
|
||||||
|
match self.mode {
|
||||||
|
ArrangerViewMode::Horizontal => add(&HorizontalArranger(&self)),
|
||||||
|
ArrangerViewMode::Vertical(factor) => add(&VerticalArranger(&self, factor))
|
||||||
|
}?;
|
||||||
|
add(&Align::SE(self.selected.description(
|
||||||
|
&self.tracks,
|
||||||
|
&self.scenes,
|
||||||
|
).as_str()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
impl<'a> Content for VerticalArranger<'a, Tui> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||||
|
let Self(state, factor) = self;
|
||||||
|
let ppq = 96;
|
||||||
|
let (cols, rows) = if *factor == 0 {(
|
||||||
|
track_clip_name_lengths(state.tracks.as_slice()),
|
||||||
|
scene_ppqs(state.tracks.as_slice(), state.scenes.as_slice()),
|
||||||
|
)} else {(
|
||||||
|
track_clip_name_lengths(state.tracks.as_slice()),
|
||||||
|
(0..=state.scenes.len()).map(|i|(factor*ppq, factor*ppq*i)).collect::<Vec<_>>(),
|
||||||
|
)};
|
||||||
|
//let height = rows.last().map(|(w,y)|(y+w)/ppq).unwrap_or(16);
|
||||||
|
let tracks: &[Sequencer<Tui>] = state.tracks.as_ref();
|
||||||
|
let scenes: &[Scene] = state.scenes.as_ref();
|
||||||
|
let offset = 4 + scene_name_max_len(scenes) as u16;
|
||||||
|
Layers::new(move |add|{
|
||||||
|
let rows: &[(usize, usize)] = rows.as_ref();
|
||||||
|
let cols: &[(usize, usize)] = cols.as_ref();
|
||||||
|
|
||||||
|
let track_titles = row!((track, (w, _)) in tracks.iter().zip(cols) =>
|
||||||
|
(&track.name.read().unwrap().as_str() as &dyn Widget<Engine = Tui>)
|
||||||
|
.min_xy(*w as u16, 2).push_x(offset));
|
||||||
|
|
||||||
|
let scene_name = |scene, playing: bool, height|row!(
|
||||||
|
if playing { "▶ " } else { " " },
|
||||||
|
(scene as &Scene).name.read().unwrap().as_str(),
|
||||||
|
).fixed_xy(offset.saturating_sub(1), height);
|
||||||
|
|
||||||
|
let scene_clip = |scene, track: usize, w: u16, h: u16|Layers::new(move |add|{
|
||||||
|
let mut color = Color::Rgb(40, 50, 30);
|
||||||
|
match (tracks.get(track), (scene as &Scene).clips.get(track)) {
|
||||||
|
(Some(track), Some(Some(clip))) => match track.phrases.get(*clip) {
|
||||||
|
Some(phrase) => {
|
||||||
|
let name = &(phrase as &Arc<RwLock<Phrase>>).read().unwrap().name;
|
||||||
|
let name = name.read().unwrap();
|
||||||
|
let name = format!("{clip:02} {}", name);
|
||||||
|
add(&name.as_str().push_x(1).fixed_x(w))?;
|
||||||
|
if (track as &Sequencer<_>).playing_phrase == Some(*clip) {
|
||||||
|
color = COLOR_PLAYING
|
||||||
|
} else {
|
||||||
|
color = COLOR_BG1
|
||||||
|
};
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
add(&Background(color))
|
||||||
|
}).fixed_xy(w, h);
|
||||||
|
|
||||||
|
let tracks_clips = col!((scene, (pulses, _)) in scenes.iter().zip(rows) => {
|
||||||
|
let height = 1.max((pulses / 96) as u16);
|
||||||
|
let playing = scene.is_playing(tracks);
|
||||||
|
Stack::right(move |add| {
|
||||||
|
add(&scene_name(scene, playing, height))?;
|
||||||
|
for (track, (w, _x)) in cols.iter().enumerate() {
|
||||||
|
add(&scene_clip(scene, track, *w as u16, height))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}).fixed_y(height)
|
||||||
|
});
|
||||||
|
|
||||||
|
add(&VerticalArrangerGrid(offset, &rows, &cols))?;
|
||||||
|
add(&VerticalArrangerCursor(state.focused, state.selected, offset, &cols, &rows))?;
|
||||||
|
add(&col!(track_titles, tracks_clips))?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.bg(Color::Rgb(28, 35, 25))
|
||||||
|
.border(Lozenge(Style::default()
|
||||||
|
.bg(Color::Rgb(40, 50, 30))
|
||||||
|
.fg(Color::Rgb(70, 80, 50))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn track_clip_name_lengths <E: Engine> (tracks: &[Sequencer<E>]) -> Vec<(usize, usize)> {
|
||||||
|
let mut total = 0;
|
||||||
|
let mut lengths: Vec<(usize, usize)> = tracks.iter().map(|track|{
|
||||||
|
let len = 4 + track.phrases
|
||||||
|
.iter()
|
||||||
|
.fold(track.name.read().unwrap().len(), |len, phrase|{
|
||||||
|
len.max(phrase.read().unwrap().name.read().unwrap().len())
|
||||||
|
});
|
||||||
|
total = total + len;
|
||||||
|
(len, total - len)
|
||||||
|
}).collect();
|
||||||
|
lengths.push((0, total));
|
||||||
|
lengths
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scene_ppqs <E: Engine> (tracks: &[Sequencer<E>], scenes: &[Scene]) -> Vec<(usize, usize)> {
|
||||||
|
let mut total = 0;
|
||||||
|
let mut scenes: Vec<(usize, usize)> = scenes.iter().map(|scene|{
|
||||||
|
let pulses = scene.pulses(tracks).max(96);
|
||||||
|
total = total + pulses;
|
||||||
|
(pulses, total - pulses)
|
||||||
|
}).collect();
|
||||||
|
scenes.push((0, total));
|
||||||
|
scenes
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scene_name_max_len (scenes: &[Scene]) -> usize {
|
||||||
|
scenes.iter()
|
||||||
|
.map(|s|s.name.read().unwrap().len())
|
||||||
|
.fold(0, usize::max)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Widget for VerticalArrangerGrid<'a> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||||
|
let area = to.area();
|
||||||
|
let Self(offset, rows, cols) = self;
|
||||||
|
let style = Some(Style::default().fg(COLOR_SEPARATOR));
|
||||||
|
for (_, x) in cols.iter() {
|
||||||
|
let x = offset + area.x() + *x as u16 - 1;
|
||||||
|
for y in area.y()..area.y2() {
|
||||||
|
to.blit(&"▎", x, y, style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (_, y) in rows.iter() {
|
||||||
|
let y = area.y() + (*y / 96) as u16 + 1;
|
||||||
|
if y >= to.buffer.area.height {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
for x in area.x()..area.x2().saturating_sub(2) {
|
||||||
|
if x < to.buffer.area.x && y < to.buffer.area.y {
|
||||||
|
let cell = to.buffer.get_mut(x, y);
|
||||||
|
cell.modifier = Modifier::UNDERLINED;
|
||||||
|
cell.underline_color = COLOR_SEPARATOR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Widget for VerticalArrangerCursor<'a> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||||
|
let area = to.area();
|
||||||
|
let Self(focused, selected, offset, cols, rows) = *self;
|
||||||
|
let get_track_area = |t: usize| [
|
||||||
|
offset + area.x() + cols[t].1 as u16 - 1,
|
||||||
|
area.y(),
|
||||||
|
cols[t].0 as u16,
|
||||||
|
area.h()
|
||||||
|
];
|
||||||
|
let get_scene_area = |s: usize| [
|
||||||
|
area.x(),
|
||||||
|
2 + area.y() + (rows[s].1 / 96) as u16,
|
||||||
|
area.w(),
|
||||||
|
(rows[s].0 / 96) as u16
|
||||||
|
];
|
||||||
|
let get_clip_area = |t: usize, s: usize| [
|
||||||
|
offset + area.x() + cols[t].1 as u16 - 1,
|
||||||
|
2 + area.y() + (rows[s].1 / 96) as u16,
|
||||||
|
cols[t].0 as u16,
|
||||||
|
(rows[s].0 / 96) as u16
|
||||||
|
];
|
||||||
|
let mut track_area: Option<[u16;4]> = None;
|
||||||
|
let mut scene_area: Option<[u16;4]> = None;
|
||||||
|
let mut clip_area: Option<[u16;4]> = None;
|
||||||
|
let area = match selected {
|
||||||
|
ArrangerFocus::Mix => {
|
||||||
|
if focused {
|
||||||
|
to.fill_bg(area, Color::Rgb(40, 50, 30));
|
||||||
|
}
|
||||||
|
area
|
||||||
|
},
|
||||||
|
ArrangerFocus::Track(t) => {
|
||||||
|
track_area = Some(get_track_area(t));
|
||||||
|
area
|
||||||
|
},
|
||||||
|
ArrangerFocus::Scene(s) => {
|
||||||
|
scene_area = Some(get_scene_area(s));
|
||||||
|
area
|
||||||
|
},
|
||||||
|
ArrangerFocus::Clip(t, s) => {
|
||||||
|
track_area = Some(get_track_area(t));
|
||||||
|
scene_area = Some(get_scene_area(s));
|
||||||
|
clip_area = Some(get_clip_area(t, s));
|
||||||
|
area
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if let Some([x, y, width, height]) = track_area {
|
||||||
|
to.fill_fg([x, y, 1, height], Color::Rgb(70, 80, 50));
|
||||||
|
to.fill_fg([x + width, y, 1, height], Color::Rgb(70, 80, 50));
|
||||||
|
}
|
||||||
|
if let Some([_, y, _, height]) = scene_area {
|
||||||
|
to.fill_ul([area.x(), y - 1, area.w(), 1], Color::Rgb(70, 80, 50));
|
||||||
|
to.fill_ul([area.x(), y + height - 1, area.w(), 1], Color::Rgb(70, 80, 50));
|
||||||
|
}
|
||||||
|
if focused {
|
||||||
|
if let Some(clip_area) = clip_area {
|
||||||
|
to.render_in(clip_area, &CORNERS)?;
|
||||||
|
to.fill_bg(clip_area, Color::Rgb(40, 50, 30));
|
||||||
|
} else if let Some(track_area) = track_area {
|
||||||
|
to.render_in(track_area.clip_h(2), &CORNERS)?;
|
||||||
|
to.fill_bg(track_area, Color::Rgb(40, 50, 30));
|
||||||
|
} else if let Some(scene_area) = scene_area {
|
||||||
|
to.render_in(scene_area.clip_w(offset-1), &CORNERS)?;
|
||||||
|
to.fill_bg(scene_area, Color::Rgb(40, 50, 30));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
impl<'a> Content for HorizontalArranger<'a, Tui> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||||
|
let Arranger { tracks, focused, selected, scenes, .. } = self.0;
|
||||||
|
let _tracks = tracks.as_slice();
|
||||||
|
lay!(
|
||||||
|
focused.then_some(Background(Color::Rgb(40, 50, 30))),
|
||||||
|
row!(
|
||||||
|
// name
|
||||||
|
CustomWidget::new(|_|{
|
||||||
|
todo!()
|
||||||
|
}, |_: &mut TuiOutput|{
|
||||||
|
todo!()
|
||||||
|
//let Self(tracks, selected) = self;
|
||||||
|
//let yellow = Some(Style::default().yellow().bold().not_dim());
|
||||||
|
//let white = Some(Style::default().white().bold().not_dim());
|
||||||
|
//let area = to.area();
|
||||||
|
//let area = [area.x(), area.y(), 3 + 5.max(track_name_max_len(tracks)) as u16, area.h()];
|
||||||
|
//let offset = 0; // track scroll offset
|
||||||
|
//for y in 0..area.h() {
|
||||||
|
//if y == 0 {
|
||||||
|
//to.blit(&"Mixer", area.x() + 1, area.y() + y, Some(DIM))?;
|
||||||
|
//} else if y % 2 == 0 {
|
||||||
|
//let index = (y as usize - 2) / 2 + offset;
|
||||||
|
//if let Some(track) = tracks.get(index) {
|
||||||
|
//let selected = selected.track() == Some(index);
|
||||||
|
//let style = if selected { yellow } else { white };
|
||||||
|
//to.blit(&format!(" {index:>02} "), area.x(), area.y() + y, style)?;
|
||||||
|
//to.blit(&*track.name.read().unwrap(), area.x() + 4, area.y() + y, style)?;
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//Ok(Some(area))
|
||||||
|
}),
|
||||||
|
// monitor
|
||||||
|
CustomWidget::new(|_|{
|
||||||
|
todo!()
|
||||||
|
}, |_: &mut TuiOutput|{
|
||||||
|
todo!()
|
||||||
|
//let Self(tracks) = self;
|
||||||
|
//let mut area = to.area();
|
||||||
|
//let on = Some(Style::default().not_dim().green().bold());
|
||||||
|
//let off = Some(DIM);
|
||||||
|
//area.x += 1;
|
||||||
|
//for y in 0..area.h() {
|
||||||
|
//if y == 0 {
|
||||||
|
////" MON ".blit(to.buffer, area.x, area.y + y, style2)?;
|
||||||
|
//} else if y % 2 == 0 {
|
||||||
|
//let index = (y as usize - 2) / 2;
|
||||||
|
//if let Some(track) = tracks.get(index) {
|
||||||
|
//let style = if track.monitoring { on } else { off };
|
||||||
|
//to.blit(&" MON ", area.x(), area.y() + y, style)?;
|
||||||
|
//} else {
|
||||||
|
//area.height = y;
|
||||||
|
//break
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//area.width = 4;
|
||||||
|
//Ok(Some(area))
|
||||||
|
}),
|
||||||
|
// record
|
||||||
|
CustomWidget::new(|_|{
|
||||||
|
todo!()
|
||||||
|
}, |_: &mut TuiOutput|{
|
||||||
|
todo!()
|
||||||
|
//let Self(tracks) = self;
|
||||||
|
//let mut area = to.area();
|
||||||
|
//let on = Some(Style::default().not_dim().red().bold());
|
||||||
|
//let off = Some(Style::default().dim());
|
||||||
|
//area.x += 1;
|
||||||
|
//for y in 0..area.h() {
|
||||||
|
//if y == 0 {
|
||||||
|
////" REC ".blit(to.buffer, area.x, area.y + y, style2)?;
|
||||||
|
//} else if y % 2 == 0 {
|
||||||
|
//let index = (y as usize - 2) / 2;
|
||||||
|
//if let Some(track) = tracks.get(index) {
|
||||||
|
//let style = if track.recording { on } else { off };
|
||||||
|
//to.blit(&" REC ", area.x(), area.y() + y, style)?;
|
||||||
|
//} else {
|
||||||
|
//area.height = y;
|
||||||
|
//break
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//area.width = 4;
|
||||||
|
//Ok(Some(area))
|
||||||
|
}),
|
||||||
|
// overdub
|
||||||
|
CustomWidget::new(|_|{
|
||||||
|
todo!()
|
||||||
|
}, |_: &mut TuiOutput|{
|
||||||
|
todo!()
|
||||||
|
//let Self(tracks) = self;
|
||||||
|
//let mut area = to.area();
|
||||||
|
//let on = Some(Style::default().not_dim().yellow().bold());
|
||||||
|
//let off = Some(Style::default().dim());
|
||||||
|
//area.x = area.x + 1;
|
||||||
|
//for y in 0..area.h() {
|
||||||
|
//if y == 0 {
|
||||||
|
////" OVR ".blit(to.buffer, area.x, area.y + y, style2)?;
|
||||||
|
//} else if y % 2 == 0 {
|
||||||
|
//let index = (y as usize - 2) / 2;
|
||||||
|
//if let Some(track) = tracks.get(index) {
|
||||||
|
//to.blit(&" OVR ", area.x(), area.y() + y, if track.overdub {
|
||||||
|
//on
|
||||||
|
//} else {
|
||||||
|
//off
|
||||||
|
//})?;
|
||||||
|
//} else {
|
||||||
|
//area.height = y;
|
||||||
|
//break
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//area.width = 4;
|
||||||
|
//Ok(Some(area))
|
||||||
|
}),
|
||||||
|
// erase
|
||||||
|
CustomWidget::new(|_|{
|
||||||
|
todo!()
|
||||||
|
}, |_: &mut TuiOutput|{
|
||||||
|
todo!()
|
||||||
|
//let Self(tracks) = self;
|
||||||
|
//let mut area = to.area();
|
||||||
|
//let off = Some(Style::default().dim());
|
||||||
|
//area.x = area.x + 1;
|
||||||
|
//for y in 0..area.h() {
|
||||||
|
//if y == 0 {
|
||||||
|
////" DEL ".blit(to.buffer, area.x, area.y + y, style2)?;
|
||||||
|
//} else if y % 2 == 0 {
|
||||||
|
//let index = (y as usize - 2) / 2;
|
||||||
|
//if let Some(_) = tracks.get(index) {
|
||||||
|
//to.blit(&" DEL ", area.x(), area.y() + y, off)?;
|
||||||
|
//} else {
|
||||||
|
//area.height = y;
|
||||||
|
//break
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//area.width = 4;
|
||||||
|
//Ok(Some(area))
|
||||||
|
}),
|
||||||
|
// gain
|
||||||
|
CustomWidget::new(|_|{
|
||||||
|
todo!()
|
||||||
|
}, |_: &mut TuiOutput|{
|
||||||
|
todo!()
|
||||||
|
//let Self(tracks) = self;
|
||||||
|
//let mut area = to.area();
|
||||||
|
//let off = Some(Style::default().dim());
|
||||||
|
//area.x = area.x() + 1;
|
||||||
|
//for y in 0..area.h() {
|
||||||
|
//if y == 0 {
|
||||||
|
////" GAIN ".blit(to.buffer, area.x, area.y + y, style2)?;
|
||||||
|
//} else if y % 2 == 0 {
|
||||||
|
//let index = (y as usize - 2) / 2;
|
||||||
|
//if let Some(_) = tracks.get(index) {
|
||||||
|
//to.blit(&" +0.0 ", area.x(), area.y() + y, off)?;
|
||||||
|
//} else {
|
||||||
|
//area.height = y;
|
||||||
|
//break
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//area.width = 7;
|
||||||
|
//Ok(Some(area))
|
||||||
|
}),
|
||||||
|
// scenes
|
||||||
|
CustomWidget::new(|_|{
|
||||||
|
todo!()
|
||||||
|
}, |to: &mut TuiOutput|{
|
||||||
|
let Arranger { tracks, scenes, selected, .. } = self.0;
|
||||||
|
let area = to.area();
|
||||||
|
let mut x2 = 0;
|
||||||
|
let [x, y, _, height] = area;
|
||||||
|
for (scene_index, scene) in scenes.iter().enumerate() {
|
||||||
|
let active_scene = selected.scene() == Some(scene_index);
|
||||||
|
let sep = Some(if active_scene {
|
||||||
|
Style::default().yellow().not_dim()
|
||||||
|
} else {
|
||||||
|
Style::default().dim()
|
||||||
|
});
|
||||||
|
for y in y+1..y+height {
|
||||||
|
to.blit(&"│", x + x2, y, sep);
|
||||||
|
}
|
||||||
|
let name = scene.name.read().unwrap();
|
||||||
|
let mut x3 = name.len() as u16;
|
||||||
|
to.blit(&*name, x + x2, y, sep);
|
||||||
|
for (i, clip) in scene.clips.iter().enumerate() {
|
||||||
|
let active_track = selected.track() == Some(i);
|
||||||
|
if let Some(clip) = clip {
|
||||||
|
let y2 = y + 2 + i as u16 * 2;
|
||||||
|
let label = match tracks[i].phrases.get(*clip) {
|
||||||
|
Some(phrase) => &format!("{}", phrase.read().unwrap().name.read().unwrap()),
|
||||||
|
None => "...."
|
||||||
|
};
|
||||||
|
to.blit(&label, x + x2, y2, Some(if active_track && active_scene {
|
||||||
|
Style::default().not_dim().yellow().bold()
|
||||||
|
} else {
|
||||||
|
Style::default().not_dim()
|
||||||
|
}));
|
||||||
|
x3 = x3.max(label.len() as u16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
x2 = x2 + x3 + 1;
|
||||||
|
}
|
||||||
|
//Ok(Some([x, y, x2, height]))
|
||||||
|
Ok(())
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn track_name_max_len <E: Engine> (tracks: &[Sequencer<E>]) -> usize {
|
||||||
|
tracks.iter()
|
||||||
|
.map(|s|s.name.read().unwrap().len())
|
||||||
|
.fold(0, usize::max)
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
impl Content for ArrangerRenameModal<Tui> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||||
|
todo!();
|
||||||
|
Layers::new(|add|{Ok(())})
|
||||||
|
//let area = to.area();
|
||||||
|
//let y = area.y() + area.h() / 2;
|
||||||
|
//let bg_area = [1, y - 1, area.w() - 2, 3];
|
||||||
|
//to.fill_bg(bg_area, COLOR_BG1);
|
||||||
|
//Lozenge(Style::default().bold().white().dim()).draw(to.with_rect(bg_area));
|
||||||
|
//let label = match self.target {
|
||||||
|
//ArrangerFocus::Mix => "Rename project:",
|
||||||
|
//ArrangerFocus::Track(_) => "Rename track:",
|
||||||
|
//ArrangerFocus::Scene(_) => "Rename scene:",
|
||||||
|
//ArrangerFocus::Clip(_, _) => "Rename clip:",
|
||||||
|
//};
|
||||||
|
//let style = Some(Style::default().not_bold().white().not_dim());
|
||||||
|
//to.blit(&label, area.x() + 3, y, style);
|
||||||
|
//let style = Some(Style::default().bold().white().not_dim());
|
||||||
|
//to.blit(&self.value, area.x() + 3 + label.len() as u16 + 1, y, style);
|
||||||
|
//let style = Some(Style::default().bold().white().not_dim().reversed());
|
||||||
|
//to.blit(&"▂", area.x() + 3 + label.len() as u16 + 1 + self.cursor as u16, y, style);
|
||||||
|
//Ok(Some(area))
|
||||||
|
//Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handle<Tui> for ArrangerRenameModal<Tui> {
|
||||||
|
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||||
|
match from.event() {
|
||||||
|
TuiEvent::Input(Event::Key(k)) => {
|
||||||
|
match k.code {
|
||||||
|
KeyCode::Esc => {
|
||||||
|
self.exit();
|
||||||
|
},
|
||||||
|
KeyCode::Enter => {
|
||||||
|
*self.result.write().unwrap() = self.value.clone();
|
||||||
|
self.exit();
|
||||||
|
},
|
||||||
|
KeyCode::Left => {
|
||||||
|
self.cursor = self.cursor.saturating_sub(1);
|
||||||
|
},
|
||||||
|
KeyCode::Right => {
|
||||||
|
self.cursor = self.value.len().min(self.cursor + 1)
|
||||||
|
},
|
||||||
|
KeyCode::Backspace => {
|
||||||
|
let last = self.value.len().saturating_sub(1);
|
||||||
|
self.value = format!("{}{}",
|
||||||
|
&self.value[0..self.cursor.min(last)],
|
||||||
|
&self.value[self.cursor.min(last)..last]
|
||||||
|
);
|
||||||
|
self.cursor = self.cursor.saturating_sub(1)
|
||||||
|
}
|
||||||
|
KeyCode::Char(c) => {
|
||||||
|
self.value.insert(self.cursor, c);
|
||||||
|
self.cursor = self.value.len().min(self.cursor + 1)
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
Ok(Some(true))
|
||||||
|
},
|
||||||
|
_ => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,4 +6,27 @@ pub(crate) use tek_core::midly::{num::u7, live::LiveEvent, MidiMessage};
|
||||||
pub(crate) use tek_core::jack::*;
|
pub(crate) use tek_core::jack::*;
|
||||||
pub(crate) use std::sync::{Arc, RwLock};
|
pub(crate) use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
submod! { sequencer sequencer_tui }
|
submod! {
|
||||||
|
arranger arranger_tui
|
||||||
|
sequencer sequencer_tui
|
||||||
|
transport transport_tui
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const CORNERS: CornersTall = CornersTall(NOT_DIM_GREEN);
|
||||||
|
|
||||||
|
tui_style!(GRAY_DIM =
|
||||||
|
Some(Color::Gray), None, None, Modifier::DIM, Modifier::empty());
|
||||||
|
tui_style!(GRAY_NOT_DIM_BOLD =
|
||||||
|
Some(Color::Gray), None, None, Modifier::BOLD, Modifier::DIM);
|
||||||
|
tui_style!(NOT_DIM_BOLD =
|
||||||
|
None, None, None, Modifier::BOLD, Modifier::DIM);
|
||||||
|
tui_style!(NOT_DIM_GREEN =
|
||||||
|
Some(Color::Rgb(96, 255, 32)), Some(COLOR_BG1), None, Modifier::empty(), Modifier::DIM);
|
||||||
|
tui_style!(NOT_DIM =
|
||||||
|
None, None, None, Modifier::empty(), Modifier::DIM);
|
||||||
|
tui_style!(WHITE_NOT_DIM_BOLD =
|
||||||
|
Some(Color::White), None, None, Modifier::BOLD, Modifier::DIM);
|
||||||
|
tui_style!(STYLE_LABEL =
|
||||||
|
Some(Color::Reset), None, None, Modifier::empty(), Modifier::BOLD);
|
||||||
|
tui_style!(STYLE_VALUE =
|
||||||
|
Some(Color::White), None, None, Modifier::BOLD, Modifier::DIM);
|
||||||
|
|
|
||||||
|
|
@ -67,22 +67,6 @@ struct ArrangerStandalone<E: Engine> {
|
||||||
/// Focus target that passes events down to sequencer
|
/// Focus target that passes events down to sequencer
|
||||||
sequencer_proxy: SequencerProxy<E>,
|
sequencer_proxy: SequencerProxy<E>,
|
||||||
}
|
}
|
||||||
impl<E: Engine> ArrangerStandalone<E> {
|
|
||||||
fn sequencer (&self) -> Option<&Sequencer<E>> {
|
|
||||||
if self.show_sequencer.is_some() {
|
|
||||||
self.arranger.sequencer()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn sequencer_mut (&mut self) -> Option<&mut Sequencer<E>> {
|
|
||||||
if self.show_sequencer.is_some() {
|
|
||||||
self.arranger.sequencer_mut()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// The standalone arranger consists of transport, clip grid, and sequencer.
|
/// The standalone arranger consists of transport, clip grid, and sequencer.
|
||||||
impl Content for ArrangerStandalone<Tui> {
|
impl Content for ArrangerStandalone<Tui> {
|
||||||
type Engine = Tui;
|
type Engine = Tui;
|
||||||
|
|
|
||||||
|
|
@ -1,463 +1,182 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
/// A collection of MIDI messages.
|
||||||
|
pub type PhraseData = Vec<Vec<MidiMessage>>;
|
||||||
|
|
||||||
/// Represents the tracks and scenes of the composition.
|
/// MIDI message serialized to bytes
|
||||||
pub struct Arranger<E: Engine> {
|
pub type MIDIMessage = Vec<u8>;
|
||||||
/// Name of arranger
|
|
||||||
pub name: Arc<RwLock<String>>,
|
/// Collection of serialized MIDI messages
|
||||||
/// Collection of tracks.
|
pub type MIDIChunk = [Vec<MIDIMessage>];
|
||||||
pub tracks: Vec<Sequencer<E>>,
|
|
||||||
/// Collection of scenes.
|
/// Contains all phrases in the project
|
||||||
pub scenes: Vec<Scene>,
|
pub struct PhrasePool {
|
||||||
/// Currently selected element.
|
pub phrases: Vec<Arc<RwLock<Option<Phrase>>>>,
|
||||||
pub selected: ArrangerFocus,
|
|
||||||
/// Display mode of arranger
|
|
||||||
pub mode: ArrangerViewMode,
|
|
||||||
/// Slot for modal dialog displayed on top of app.
|
|
||||||
pub modal: Option<Box<dyn ContentComponent<E>>>,
|
|
||||||
/// Whether the arranger is currently focused
|
|
||||||
pub focused: bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<E: Engine> Arranger<E> {
|
/// Contains state for viewing and editing a phrase
|
||||||
pub fn new (name: &str) -> Self {
|
pub struct PhraseEditor<E: Engine> {
|
||||||
Self {
|
_engine: PhantomData<E>,
|
||||||
name: Arc::new(RwLock::new(name.into())),
|
pub phrase: Arc<RwLock<Option<Phrase>>>,
|
||||||
mode: ArrangerViewMode::Vertical(2),
|
|
||||||
selected: ArrangerFocus::Clip(0, 0),
|
|
||||||
scenes: vec![],
|
|
||||||
tracks: vec![],
|
|
||||||
modal: None,
|
|
||||||
focused: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn activate (&mut self) {
|
|
||||||
match self.selected {
|
|
||||||
ArrangerFocus::Scene(s) => {
|
|
||||||
for (track_index, track) in self.tracks.iter_mut().enumerate() {
|
|
||||||
track.playing_phrase = self.scenes[s].clips[track_index];
|
|
||||||
track.reset = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ArrangerFocus::Clip(t, s) => {
|
|
||||||
self.tracks[t].playing_phrase = self.scenes[s].clips[t];
|
|
||||||
self.tracks[t].reset = true;
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn sequencer (&self) -> Option<&Sequencer<E>> {
|
|
||||||
self.selected.track()
|
|
||||||
.map(|track|self.tracks.get(track))
|
|
||||||
.flatten()
|
|
||||||
}
|
|
||||||
pub fn sequencer_mut (&mut self) -> Option<&mut Sequencer<E>> {
|
|
||||||
self.selected.track()
|
|
||||||
.map(|track|self.tracks.get_mut(track))
|
|
||||||
.flatten()
|
|
||||||
}
|
|
||||||
pub fn show_phrase (&mut self) {
|
|
||||||
let (scene, track) = (self.selected.scene(), self.selected.track());
|
|
||||||
if let (Some(scene_index), Some(track_index)) = (scene, track) {
|
|
||||||
let scene = self.scenes.get(scene_index);
|
|
||||||
let track = self.tracks.get_mut(track_index);
|
|
||||||
if let (Some(scene), Some(track)) = (scene, track) {
|
|
||||||
track.viewing_phrase = scene.clips[track_index]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn is_first_row (&self) -> bool {
|
|
||||||
let selected = self.selected;
|
|
||||||
selected.is_mix() || selected.is_track()
|
|
||||||
}
|
|
||||||
pub fn is_last_row (&self) -> bool {
|
|
||||||
let selected = self.selected;
|
|
||||||
(self.scenes.len() == 0 && (selected.is_mix() || selected.is_track())) || match selected {
|
|
||||||
ArrangerFocus::Scene(s) =>
|
|
||||||
s == self.scenes.len() - 1,
|
|
||||||
ArrangerFocus::Clip(_, s) =>
|
|
||||||
s == self.scenes.len() - 1,
|
|
||||||
_ => false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn track (&self) -> Option<&Sequencer<E>> {
|
|
||||||
self.selected.track().map(|t|self.tracks.get(t)).flatten()
|
|
||||||
}
|
|
||||||
pub fn track_mut (&mut self) -> Option<&mut Sequencer<E>> {
|
|
||||||
self.selected.track().map(|t|self.tracks.get_mut(t)).flatten()
|
|
||||||
}
|
|
||||||
pub fn track_next (&mut self) {
|
|
||||||
self.selected.track_next(self.tracks.len() - 1)
|
|
||||||
}
|
|
||||||
pub fn track_prev (&mut self) {
|
|
||||||
self.selected.track_prev()
|
|
||||||
}
|
|
||||||
pub fn track_add (&mut self, name: Option<&str>) -> Usually<&mut Sequencer<E>> {
|
|
||||||
self.tracks.push(name.map_or_else(
|
|
||||||
|| Sequencer::new(&self.track_default_name()),
|
|
||||||
|name| Sequencer::new(name),
|
|
||||||
));
|
|
||||||
let index = self.tracks.len() - 1;
|
|
||||||
Ok(&mut self.tracks[index])
|
|
||||||
}
|
|
||||||
pub fn track_del (&mut self) {
|
|
||||||
unimplemented!("Arranger::track_del");
|
|
||||||
}
|
|
||||||
pub fn track_default_name (&self) -> String {
|
|
||||||
format!("Track {}", self.tracks.len() + 1)
|
|
||||||
}
|
|
||||||
pub fn scene (&self) -> Option<&Scene> {
|
|
||||||
self.selected.scene().map(|s|self.scenes.get(s)).flatten()
|
|
||||||
}
|
|
||||||
pub fn scene_mut (&mut self) -> Option<&mut Scene> {
|
|
||||||
self.selected.scene().map(|s|self.scenes.get_mut(s)).flatten()
|
|
||||||
}
|
|
||||||
pub fn scene_next (&mut self) {
|
|
||||||
self.selected.scene_next(self.scenes.len() - 1)
|
|
||||||
}
|
|
||||||
pub fn scene_prev (&mut self) {
|
|
||||||
self.selected.scene_prev()
|
|
||||||
}
|
|
||||||
pub fn scene_add (&mut self, name: Option<&str>) -> Usually<&mut Scene> {
|
|
||||||
let clips = vec![None;self.tracks.len()];
|
|
||||||
self.scenes.push(match name {
|
|
||||||
Some(name) => Scene::new(name, clips),
|
|
||||||
None => Scene::new(&self.scene_default_name(), clips),
|
|
||||||
});
|
|
||||||
let index = self.scenes.len() - 1;
|
|
||||||
Ok(&mut self.scenes[index])
|
|
||||||
}
|
|
||||||
pub fn scene_del (&mut self) {
|
|
||||||
unimplemented!("Arranger::scene_del");
|
|
||||||
}
|
|
||||||
pub fn scene_default_name (&self) -> String {
|
|
||||||
format!("Scene {}", self.scenes.len() + 1)
|
|
||||||
}
|
|
||||||
pub fn phrase (&self) -> Option<&Arc<RwLock<Phrase>>> {
|
|
||||||
let track_id = self.selected.track()?;
|
|
||||||
self.tracks.get(track_id)?.phrases.get((*self.scene()?.clips.get(track_id)?)?)
|
|
||||||
}
|
|
||||||
pub fn phrase_del (&mut self) {
|
|
||||||
let track_index = self.selected.track();
|
|
||||||
let scene_index = self.selected.scene();
|
|
||||||
track_index
|
|
||||||
.and_then(|index|self.tracks.get_mut(index).map(|track|(index, track)))
|
|
||||||
.map(|(track_index, _)|{
|
|
||||||
scene_index
|
|
||||||
.and_then(|index|self.scenes.get_mut(index))
|
|
||||||
.map(|scene|scene.clips[track_index] = None);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
pub fn phrase_next (&mut self) {
|
|
||||||
let track_index = self.selected.track();
|
|
||||||
let scene_index = self.selected.scene();
|
|
||||||
track_index
|
|
||||||
.and_then(|index|self.tracks.get_mut(index).map(|track|(index, track)))
|
|
||||||
.and_then(|(track_index, track)|{
|
|
||||||
let phrases = track.phrases.len();
|
|
||||||
scene_index
|
|
||||||
.and_then(|index|self.scenes.get_mut(index))
|
|
||||||
.and_then(|scene|{
|
|
||||||
if let Some(phrase_index) = scene.clips[track_index] {
|
|
||||||
if phrase_index >= phrases - 1 {
|
|
||||||
scene.clips[track_index] = None;
|
|
||||||
} else {
|
|
||||||
scene.clips[track_index] = Some(phrase_index + 1);
|
|
||||||
}
|
|
||||||
} else if phrases > 0 {
|
|
||||||
scene.clips[track_index] = Some(0);
|
|
||||||
}
|
|
||||||
Some(())
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
pub fn phrase_prev (&mut self) {
|
|
||||||
let track_index = self.selected.track();
|
|
||||||
let scene_index = self.selected.scene();
|
|
||||||
track_index
|
|
||||||
.and_then(|index|self.tracks.get_mut(index).map(|track|(index, track)))
|
|
||||||
.and_then(|(track_index, track)|{
|
|
||||||
let phrases = track.phrases.len();
|
|
||||||
scene_index
|
|
||||||
.and_then(|index|self.scenes.get_mut(index))
|
|
||||||
.and_then(|scene|{
|
|
||||||
if let Some(phrase_index) = scene.clips[track_index] {
|
|
||||||
scene.clips[track_index] = if phrase_index == 0 {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(phrase_index - 1)
|
|
||||||
};
|
|
||||||
} else if phrases > 0 {
|
|
||||||
scene.clips[track_index] = Some(phrases - 1);
|
|
||||||
}
|
|
||||||
Some(())
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
impl<E: Engine> PhraseEditor<E> {
|
||||||
|
pub fn new () -> Self {
|
||||||
#[derive(PartialEq, Clone, Copy)]
|
|
||||||
/// Represents the current user selection in the arranger
|
|
||||||
pub enum ArrangerFocus {
|
|
||||||
/** The whole mix is selected */
|
|
||||||
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 ArrangerFocus {
|
|
||||||
pub fn description <E: Engine> (
|
|
||||||
&self,
|
|
||||||
tracks: &Vec<Sequencer<E>>,
|
|
||||||
scenes: &Vec<Scene>,
|
|
||||||
) -> String {
|
|
||||||
format!("Selected: {}", match self {
|
|
||||||
Self::Mix => format!("Everything"),
|
|
||||||
Self::Track(t) => if let Some(track) = tracks.get(*t) {
|
|
||||||
format!("T{t}: {}", &track.name.read().unwrap())
|
|
||||||
} else {
|
|
||||||
format!("T??")
|
|
||||||
},
|
|
||||||
Self::Scene(s) => if let Some(scene) = scenes.get(*s) {
|
|
||||||
format!("S{s}: {}", &scene.name.read().unwrap())
|
|
||||||
} else {
|
|
||||||
format!("S??")
|
|
||||||
},
|
|
||||||
Self::Clip(t, s) => if let (Some(track), Some(scene)) = (
|
|
||||||
tracks.get(*t),
|
|
||||||
scenes.get(*s),
|
|
||||||
) {
|
|
||||||
if let Some(Some(slot)) = scene.clips.get(*t) {
|
|
||||||
if let Some(clip) = track.phrases.get(*slot) {
|
|
||||||
format!("T{t} S{s} C{slot} ({})", &clip.read().unwrap().name.read().unwrap())
|
|
||||||
} else {
|
|
||||||
format!("T{t} S{s}: Empty")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
format!("T{t} S{s}: Empty")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
format!("T{t} S{s}: Empty")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
pub fn is_mix (&self) -> bool {
|
|
||||||
match self { Self::Mix => true, _ => false }
|
|
||||||
}
|
|
||||||
pub fn is_track (&self) -> bool {
|
|
||||||
match self { Self::Track(_) => true, _ => false }
|
|
||||||
}
|
|
||||||
pub fn is_scene (&self) -> bool {
|
|
||||||
match self { Self::Scene(_) => true, _ => false }
|
|
||||||
}
|
|
||||||
pub fn is_clip (&self) -> bool {
|
|
||||||
match self { Self::Clip(_, _) => true, _ => false }
|
|
||||||
}
|
|
||||||
pub fn track (&self) -> Option<usize> {
|
|
||||||
match self {
|
|
||||||
Self::Clip(t, _) => Some(*t),
|
|
||||||
Self::Track(t) => Some(*t),
|
|
||||||
_ => None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn track_next (&mut self, last_track: usize) {
|
|
||||||
*self = match self {
|
|
||||||
Self::Mix => Self::Track(0),
|
|
||||||
Self::Track(t) => Self::Track(last_track.min(*t + 1)),
|
|
||||||
Self::Scene(s) => Self::Clip(0, *s),
|
|
||||||
Self::Clip(t, s) => Self::Clip(last_track.min(*t + 1), *s),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn track_prev (&mut self) {
|
|
||||||
*self = match self {
|
|
||||||
Self::Mix => Self::Mix,
|
|
||||||
Self::Scene(s) => Self::Scene(*s),
|
|
||||||
Self::Track(t) => if *t == 0 {
|
|
||||||
Self::Mix
|
|
||||||
} else {
|
|
||||||
Self::Track(*t - 1)
|
|
||||||
},
|
|
||||||
Self::Clip(t, s) => if *t == 0 {
|
|
||||||
Self::Scene(*s)
|
|
||||||
} else {
|
|
||||||
Self::Clip(t.saturating_sub(1), *s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn scene (&self) -> Option<usize> {
|
|
||||||
match self {
|
|
||||||
Self::Clip(_, s) => Some(*s),
|
|
||||||
Self::Scene(s) => Some(*s),
|
|
||||||
_ => None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn scene_next (&mut self, last_scene: usize) {
|
|
||||||
*self = match self {
|
|
||||||
Self::Mix => Self::Scene(0),
|
|
||||||
Self::Track(t) => Self::Clip(*t, 0),
|
|
||||||
Self::Scene(s) => Self::Scene(last_scene.min(*s + 1)),
|
|
||||||
Self::Clip(t, s) => Self::Clip(*t, last_scene.min(*s + 1)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn scene_prev (&mut self) {
|
|
||||||
*self = match self {
|
|
||||||
Self::Mix => Self::Mix,
|
|
||||||
Self::Track(t) => Self::Track(*t),
|
|
||||||
Self::Scene(s) => if *s == 0 {
|
|
||||||
Self::Mix
|
|
||||||
} else {
|
|
||||||
Self::Scene(*s - 1)
|
|
||||||
},
|
|
||||||
Self::Clip(t, s) => if *s == 0 {
|
|
||||||
Self::Track(*t)
|
|
||||||
} else {
|
|
||||||
Self::Clip(*t, s.saturating_sub(1))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
/// Display mode of arranger
|
|
||||||
#[derive(PartialEq)]
|
|
||||||
pub enum ArrangerViewMode { Horizontal, Vertical(usize) }
|
|
||||||
/// Arranger display mode can be cycled
|
|
||||||
impl ArrangerViewMode {
|
|
||||||
/// Cycle arranger display mode
|
|
||||||
pub fn to_next (&mut self) {
|
|
||||||
*self = match self {
|
|
||||||
Self::Horizontal => Self::Vertical(1),
|
|
||||||
Self::Vertical(1) => Self::Vertical(2),
|
|
||||||
Self::Vertical(2) => Self::Vertical(2),
|
|
||||||
Self::Vertical(0) => Self::Horizontal,
|
|
||||||
Self::Vertical(_) => Self::Vertical(0),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
pub struct VerticalArranger<'a, E: Engine>(
|
|
||||||
pub &'a Arranger<E>, pub usize
|
|
||||||
);
|
|
||||||
pub struct VerticalArrangerGrid<'a>(
|
|
||||||
pub u16, pub &'a [(usize, usize)], pub &'a [(usize, usize)]
|
|
||||||
);
|
|
||||||
pub struct VerticalArrangerCursor<'a>(
|
|
||||||
pub bool, pub ArrangerFocus, pub u16, pub &'a [(usize, usize)], pub &'a [(usize, usize)],
|
|
||||||
);
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
pub struct HorizontalArranger<'a, E: Engine>(
|
|
||||||
pub &'a Arranger<E>
|
|
||||||
);
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
/// Appears on first run (i.e. if state dir is missing).
|
|
||||||
pub struct ArrangerRenameModal<E: Engine> {
|
|
||||||
_engine: std::marker::PhantomData<E>,
|
|
||||||
pub done: bool,
|
|
||||||
pub target: ArrangerFocus,
|
|
||||||
pub value: String,
|
|
||||||
pub result: Arc<RwLock<String>>,
|
|
||||||
pub cursor: usize
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E: Engine> ArrangerRenameModal<E> {
|
|
||||||
pub fn new (target: ArrangerFocus, value: &Arc<RwLock<String>>) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
_engine: Default::default(),
|
_engine: Default::default(),
|
||||||
done: false,
|
phrase: Arc::new(RwLock::new(None)),
|
||||||
value: value.read().unwrap().clone(),
|
|
||||||
cursor: value.read().unwrap().len(),
|
|
||||||
result: value.clone(),
|
|
||||||
target,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub fn show (&mut self, phrase: &Arc<RwLock<Option<Phrase>>>) {
|
||||||
|
self.phrase = phrase.clone();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<E: Engine + Send> Exit for ArrangerRenameModal<E> {
|
/// A MIDI sequence.
|
||||||
fn exited (&self) -> bool { self.done }
|
#[derive(Debug)]
|
||||||
fn exit (&mut self) { self.done = true }
|
pub struct Phrase {
|
||||||
|
/// Name of phrase
|
||||||
|
pub name: Arc<RwLock<String>>,
|
||||||
|
/// Length of phrase
|
||||||
|
pub length: usize,
|
||||||
|
/// Notes in phrase
|
||||||
|
pub notes: PhraseData,
|
||||||
|
/// Whether to loop the phrase or play it once
|
||||||
|
pub loop_on: bool,
|
||||||
|
/// Start of loop
|
||||||
|
pub loop_start: usize,
|
||||||
|
/// Length of loop
|
||||||
|
pub loop_length: usize,
|
||||||
|
/// All notes are displayed with minimum length
|
||||||
|
pub percussive: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
impl Default for Phrase {
|
||||||
|
fn default () -> Self { Self::new("", 0, None) }
|
||||||
/// A collection of phrases to play on each track.
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct Scene {
|
|
||||||
pub name: Arc<RwLock<String>>,
|
|
||||||
pub clips: Vec<Option<usize>>,
|
|
||||||
}
|
}
|
||||||
impl Scene {
|
|
||||||
pub fn from_edn <'a, 'e> (args: &[Edn<'e>]) -> Usually<Self> {
|
impl Phrase {
|
||||||
let mut name = None;
|
pub fn new (name: &str, length: usize, notes: Option<PhraseData>) -> Self {
|
||||||
let mut clips = vec![];
|
Self {
|
||||||
|
name: Arc::new(RwLock::new(name.into())),
|
||||||
|
length,
|
||||||
|
notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]),
|
||||||
|
loop_on: true,
|
||||||
|
loop_start: 0,
|
||||||
|
loop_length: length,
|
||||||
|
percussive: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn toggle_loop (&mut self) {
|
||||||
|
self.loop_on = !self.loop_on;
|
||||||
|
}
|
||||||
|
pub fn record_event (&mut self, pulse: usize, message: MidiMessage) {
|
||||||
|
if pulse >= self.length {
|
||||||
|
panic!("extend phrase first")
|
||||||
|
}
|
||||||
|
self.notes[pulse].push(message);
|
||||||
|
}
|
||||||
|
/// Check if a range `start..end` contains MIDI Note On `k`
|
||||||
|
pub fn contains_note_on (&self, k: u7, start: usize, end: usize) -> bool {
|
||||||
|
//panic!("{:?} {start} {end}", &self);
|
||||||
|
for events in self.notes[start.max(0)..end.min(self.notes.len())].iter() {
|
||||||
|
for event in events.iter() {
|
||||||
|
match event {
|
||||||
|
MidiMessage::NoteOn {key,..} => {
|
||||||
|
if *key == k {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
/// Write a chunk of MIDI events to an output port.
|
||||||
|
pub fn process_out (
|
||||||
|
&self,
|
||||||
|
output: &mut MIDIChunk,
|
||||||
|
notes_on: &mut [bool;128],
|
||||||
|
timebase: &Arc<Timebase>,
|
||||||
|
(frame0, frames, _): (usize, usize, f64),
|
||||||
|
) {
|
||||||
|
let mut buf = Vec::with_capacity(8);
|
||||||
|
for (time, tick) in Ticks(timebase.pulse_per_frame()).between_frames(
|
||||||
|
frame0, frame0 + frames
|
||||||
|
) {
|
||||||
|
let tick = tick % self.length;
|
||||||
|
for message in self.notes[tick].iter() {
|
||||||
|
buf.clear();
|
||||||
|
let channel = 0.into();
|
||||||
|
let message = *message;
|
||||||
|
LiveEvent::Midi { channel, message }.write(&mut buf).unwrap();
|
||||||
|
output[time as usize].push(buf.clone());
|
||||||
|
match message {
|
||||||
|
MidiMessage::NoteOn { key, .. } => notes_on[key.as_int() as usize] = true,
|
||||||
|
MidiMessage::NoteOff { key, .. } => notes_on[key.as_int() as usize] = false,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn from_edn <'e> (ppq: usize, args: &[Edn<'e>]) -> Usually<Self> {
|
||||||
|
let mut phrase = Self::default();
|
||||||
|
let mut name = String::new();
|
||||||
|
let mut beats = 0usize;
|
||||||
|
let mut steps = 0usize;
|
||||||
edn!(edn in args {
|
edn!(edn in args {
|
||||||
Edn::Map(map) => {
|
Edn::Map(map) => {
|
||||||
let key = map.get(&Edn::Key(":name"));
|
if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) {
|
||||||
if let Some(Edn::Str(n)) = key {
|
name = String::from(*n);
|
||||||
name = Some(*n);
|
}
|
||||||
} else {
|
if let Some(Edn::Int(b)) = map.get(&Edn::Key(":beats")) {
|
||||||
panic!("unexpected key in scene '{name:?}': {key:?}")
|
beats = *b as usize;
|
||||||
|
phrase.length = ppq * beats;
|
||||||
|
for _ in phrase.notes.len()..phrase.length {
|
||||||
|
phrase.notes.push(Vec::with_capacity(16))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(Edn::Int(s)) = map.get(&Edn::Key(":steps")) {
|
||||||
|
steps = *s as usize;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Edn::Symbol("_") => {
|
Edn::List(args) => {
|
||||||
clips.push(None);
|
let time = (match args.get(0) {
|
||||||
|
Some(Edn::Key(text)) => text[1..].parse::<f64>()?,
|
||||||
|
Some(Edn::Int(i)) => *i as f64,
|
||||||
|
Some(Edn::Double(f)) => f64::from(*f),
|
||||||
|
_ => panic!("unexpected in phrase '{name}': {:?}", args.get(0)),
|
||||||
|
} * beats as f64 * ppq as f64 / steps as f64) as usize;
|
||||||
|
for edn in args[1..].iter() {
|
||||||
|
match edn {
|
||||||
|
Edn::List(args) => if let (
|
||||||
|
Some(Edn::Int(key)),
|
||||||
|
Some(Edn::Int(vel)),
|
||||||
|
) = (
|
||||||
|
args.get(0),
|
||||||
|
args.get(1),
|
||||||
|
) {
|
||||||
|
let (key, vel) = (
|
||||||
|
u7::from((*key as u8).min(127)),
|
||||||
|
u7::from((*vel as u8).min(127)),
|
||||||
|
);
|
||||||
|
phrase.notes[time].push(MidiMessage::NoteOn { key, vel })
|
||||||
|
} else {
|
||||||
|
panic!("unexpected list in phrase '{name}'")
|
||||||
|
},
|
||||||
|
_ => panic!("unexpected in phrase '{name}': {edn:?}")
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Edn::Int(i) => {
|
_ => panic!("unexpected in phrase '{name}': {edn:?}"),
|
||||||
clips.push(Some(*i as usize));
|
|
||||||
},
|
|
||||||
_ => panic!("unexpected in scene '{name:?}': {edn:?}")
|
|
||||||
});
|
});
|
||||||
let scene = Self::new(name.unwrap_or(""), clips);
|
*phrase.name.write().unwrap() = name;
|
||||||
Ok(scene)
|
Ok(phrase)
|
||||||
}
|
|
||||||
pub fn new (name: impl AsRef<str>, clips: impl AsRef<[Option<usize>]>) -> Self {
|
|
||||||
let name = Arc::new(RwLock::new(name.as_ref().into()));
|
|
||||||
let clips = clips.as_ref().iter().map(|x|x.clone()).collect();
|
|
||||||
Self { name, clips, }
|
|
||||||
}
|
|
||||||
/// Returns the pulse length of the longest phrase in the scene
|
|
||||||
pub fn pulses <E: Engine> (&self, tracks: &[Sequencer<E>]) -> usize {
|
|
||||||
self.clips.iter().enumerate()
|
|
||||||
.filter_map(|(i, c)|c
|
|
||||||
.map(|c|tracks
|
|
||||||
.get(i)
|
|
||||||
.map(|track|track
|
|
||||||
.phrases
|
|
||||||
.get(c))))
|
|
||||||
.filter_map(|p|p)
|
|
||||||
.filter_map(|p|p)
|
|
||||||
.fold(0, |a, p|a.max(p.read().unwrap().length))
|
|
||||||
}
|
|
||||||
/// Returns true if all phrases in the scene are currently playing
|
|
||||||
pub fn is_playing <E: Engine> (&self, tracks: &[Sequencer<E>]) -> bool {
|
|
||||||
self.clips.iter().enumerate()
|
|
||||||
.all(|(track_index, phrase_index)|match phrase_index {
|
|
||||||
Some(i) => tracks
|
|
||||||
.get(track_index)
|
|
||||||
.map(|track|track.playing_phrase == Some(*i))
|
|
||||||
.unwrap_or(false),
|
|
||||||
None => true
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
/// Phrase player.
|
||||||
|
|
||||||
/// Phrase editor.
|
|
||||||
pub struct Sequencer<E: Engine> {
|
pub struct Sequencer<E: Engine> {
|
||||||
pub name: Arc<RwLock<String>>,
|
pub name: Arc<RwLock<String>>,
|
||||||
pub mode: bool,
|
pub mode: bool,
|
||||||
|
|
@ -503,6 +222,7 @@ pub struct Sequencer<E: Engine> {
|
||||||
/// Highlight keys on piano roll.
|
/// Highlight keys on piano roll.
|
||||||
pub notes_out: [bool;128],
|
pub notes_out: [bool;128],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<E: Engine> Sequencer<E> {
|
impl<E: Engine> Sequencer<E> {
|
||||||
pub fn new (name: &str) -> Self {
|
pub fn new (name: &str) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -654,6 +374,7 @@ impl<E: Engine> Sequencer<E> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add "all notes off" to the start of a buffer.
|
/// Add "all notes off" to the start of a buffer.
|
||||||
pub fn all_notes_off (output: &mut MIDIChunk) {
|
pub fn all_notes_off (output: &mut MIDIChunk) {
|
||||||
let mut buf = vec![];
|
let mut buf = vec![];
|
||||||
|
|
@ -662,6 +383,7 @@ pub fn all_notes_off (output: &mut MIDIChunk) {
|
||||||
evt.write(&mut buf).unwrap();
|
evt.write(&mut buf).unwrap();
|
||||||
output[0].push(buf);
|
output[0].push(buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return boxed iterator of MIDI events
|
/// Return boxed iterator of MIDI events
|
||||||
pub fn parse_midi_input (input: MidiIter) -> Box<dyn Iterator<Item=(usize, LiveEvent, &[u8])> + '_> {
|
pub fn parse_midi_input (input: MidiIter) -> Box<dyn Iterator<Item=(usize, LiveEvent, &[u8])> + '_> {
|
||||||
Box::new(input.map(|RawMidi { time, bytes }|(
|
Box::new(input.map(|RawMidi { time, bytes }|(
|
||||||
|
|
@ -670,6 +392,7 @@ pub fn parse_midi_input (input: MidiIter) -> Box<dyn Iterator<Item=(usize, LiveE
|
||||||
bytes
|
bytes
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write to JACK port from output buffer (containing notes from sequence and/or monitor)
|
/// Write to JACK port from output buffer (containing notes from sequence and/or monitor)
|
||||||
pub fn write_midi_output (writer: &mut MidiWriter, output: &MIDIChunk, frames: usize) {
|
pub fn write_midi_output (writer: &mut MidiWriter, output: &MIDIChunk, frames: usize) {
|
||||||
for time in 0..frames {
|
for time in 0..frames {
|
||||||
|
|
@ -679,312 +402,3 @@ pub fn write_midi_output (writer: &mut MidiWriter, output: &MIDIChunk, frames: u
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// MIDI message serialized to bytes
|
|
||||||
pub type MIDIMessage = Vec<u8>;
|
|
||||||
/// Collection of serialized MIDI messages
|
|
||||||
pub type MIDIChunk = [Vec<MIDIMessage>];
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
/// A collection of MIDI messages.
|
|
||||||
pub type PhraseData = Vec<Vec<MidiMessage>>;
|
|
||||||
/// A MIDI sequence.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Phrase {
|
|
||||||
pub name: Arc<RwLock<String>>,
|
|
||||||
pub length: usize,
|
|
||||||
pub notes: PhraseData,
|
|
||||||
pub loop_on: bool,
|
|
||||||
pub loop_start: usize,
|
|
||||||
pub loop_length: usize,
|
|
||||||
/// All notes are displayed with minimum length
|
|
||||||
pub percussive: bool,
|
|
||||||
}
|
|
||||||
impl Default for Phrase {
|
|
||||||
fn default () -> Self { Self::new("", 0, None) }
|
|
||||||
}
|
|
||||||
impl Phrase {
|
|
||||||
pub fn new (name: &str, length: usize, notes: Option<PhraseData>) -> Self {
|
|
||||||
Self {
|
|
||||||
name: Arc::new(RwLock::new(name.into())),
|
|
||||||
length,
|
|
||||||
notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]),
|
|
||||||
loop_on: true,
|
|
||||||
loop_start: 0,
|
|
||||||
loop_length: length,
|
|
||||||
percussive: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn toggle_loop (&mut self) {
|
|
||||||
self.loop_on = !self.loop_on;
|
|
||||||
}
|
|
||||||
pub fn record_event (&mut self, pulse: usize, message: MidiMessage) {
|
|
||||||
if pulse >= self.length {
|
|
||||||
panic!("extend phrase first")
|
|
||||||
}
|
|
||||||
self.notes[pulse].push(message);
|
|
||||||
}
|
|
||||||
/// Check if a range `start..end` contains MIDI Note On `k`
|
|
||||||
pub fn contains_note_on (&self, k: u7, start: usize, end: usize) -> bool {
|
|
||||||
//panic!("{:?} {start} {end}", &self);
|
|
||||||
for events in self.notes[start.max(0)..end.min(self.notes.len())].iter() {
|
|
||||||
for event in events.iter() {
|
|
||||||
match event {
|
|
||||||
MidiMessage::NoteOn {key,..} => {
|
|
||||||
if *key == k {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
/// Write a chunk of MIDI events to an output port.
|
|
||||||
pub fn process_out (
|
|
||||||
&self,
|
|
||||||
output: &mut MIDIChunk,
|
|
||||||
notes_on: &mut [bool;128],
|
|
||||||
timebase: &Arc<Timebase>,
|
|
||||||
(frame0, frames, _): (usize, usize, f64),
|
|
||||||
) {
|
|
||||||
let mut buf = Vec::with_capacity(8);
|
|
||||||
for (time, tick) in Ticks(timebase.pulse_per_frame()).between_frames(
|
|
||||||
frame0, frame0 + frames
|
|
||||||
) {
|
|
||||||
let tick = tick % self.length;
|
|
||||||
for message in self.notes[tick].iter() {
|
|
||||||
buf.clear();
|
|
||||||
let channel = 0.into();
|
|
||||||
let message = *message;
|
|
||||||
LiveEvent::Midi { channel, message }.write(&mut buf).unwrap();
|
|
||||||
output[time as usize].push(buf.clone());
|
|
||||||
match message {
|
|
||||||
MidiMessage::NoteOn { key, .. } => notes_on[key.as_int() as usize] = true,
|
|
||||||
MidiMessage::NoteOff { key, .. } => notes_on[key.as_int() as usize] = false,
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn from_edn <'e> (ppq: usize, args: &[Edn<'e>]) -> Usually<Self> {
|
|
||||||
let mut phrase = Self::default();
|
|
||||||
let mut name = String::new();
|
|
||||||
let mut beats = 0usize;
|
|
||||||
let mut steps = 0usize;
|
|
||||||
edn!(edn in args {
|
|
||||||
Edn::Map(map) => {
|
|
||||||
if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) {
|
|
||||||
name = String::from(*n);
|
|
||||||
}
|
|
||||||
if let Some(Edn::Int(b)) = map.get(&Edn::Key(":beats")) {
|
|
||||||
beats = *b as usize;
|
|
||||||
phrase.length = ppq * beats;
|
|
||||||
for _ in phrase.notes.len()..phrase.length {
|
|
||||||
phrase.notes.push(Vec::with_capacity(16))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(Edn::Int(s)) = map.get(&Edn::Key(":steps")) {
|
|
||||||
steps = *s as usize;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Edn::List(args) => {
|
|
||||||
let time = (match args.get(0) {
|
|
||||||
Some(Edn::Key(text)) => text[1..].parse::<f64>()?,
|
|
||||||
Some(Edn::Int(i)) => *i as f64,
|
|
||||||
Some(Edn::Double(f)) => f64::from(*f),
|
|
||||||
_ => panic!("unexpected in phrase '{name}': {:?}", args.get(0)),
|
|
||||||
} * beats as f64 * ppq as f64 / steps as f64) as usize;
|
|
||||||
for edn in args[1..].iter() {
|
|
||||||
match edn {
|
|
||||||
Edn::List(args) => if let (
|
|
||||||
Some(Edn::Int(key)),
|
|
||||||
Some(Edn::Int(vel)),
|
|
||||||
) = (
|
|
||||||
args.get(0),
|
|
||||||
args.get(1),
|
|
||||||
) {
|
|
||||||
let (key, vel) = (
|
|
||||||
u7::from((*key as u8).min(127)),
|
|
||||||
u7::from((*vel as u8).min(127)),
|
|
||||||
);
|
|
||||||
phrase.notes[time].push(MidiMessage::NoteOn { key, vel })
|
|
||||||
} else {
|
|
||||||
panic!("unexpected list in phrase '{name}'")
|
|
||||||
},
|
|
||||||
_ => panic!("unexpected in phrase '{name}': {edn:?}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => panic!("unexpected in phrase '{name}': {edn:?}"),
|
|
||||||
});
|
|
||||||
*phrase.name.write().unwrap() = name;
|
|
||||||
Ok(phrase)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
/// Stores and displays time-related state.
|
|
||||||
pub struct TransportToolbar<E: Engine> {
|
|
||||||
_engine: PhantomData<E>,
|
|
||||||
/// Enable metronome?
|
|
||||||
pub metronome: bool,
|
|
||||||
/// Current sample rate, tempo, and PPQ.
|
|
||||||
pub timebase: Arc<Timebase>,
|
|
||||||
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
|
||||||
pub jack: Option<JackClient>,
|
|
||||||
/// JACK transport handle.
|
|
||||||
pub transport: Option<Transport>,
|
|
||||||
/// Quantization factor
|
|
||||||
/// Global frame and usec at which playback started
|
|
||||||
pub started: Option<(usize, usize)>,
|
|
||||||
pub focused: bool,
|
|
||||||
pub focus: TransportToolbarFocus,
|
|
||||||
pub playing: Option<TransportState>,
|
|
||||||
pub bpm: f64,
|
|
||||||
pub quant: usize,
|
|
||||||
pub sync: usize,
|
|
||||||
pub frame: usize,
|
|
||||||
pub pulse: usize,
|
|
||||||
pub ppq: usize,
|
|
||||||
pub usecs: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E: Engine> TransportToolbar<E> {
|
|
||||||
pub fn standalone () -> Usually<Arc<RwLock<Self>>> where Self: 'static {
|
|
||||||
let jack = JackClient::Inactive(
|
|
||||||
Client::new("tek_transport", ClientOptions::NO_START_SERVER)?.0
|
|
||||||
);
|
|
||||||
let mut transport = Self::new(Some(jack.transport()));
|
|
||||||
transport.focused = true;
|
|
||||||
let transport = Arc::new(RwLock::new(transport));
|
|
||||||
transport.write().unwrap().jack = Some(
|
|
||||||
jack.activate(
|
|
||||||
&transport.clone(),
|
|
||||||
|state: &Arc<RwLock<TransportToolbar<E>>>, client, scope| {
|
|
||||||
state.write().unwrap().process(client, scope)
|
|
||||||
}
|
|
||||||
)?
|
|
||||||
);
|
|
||||||
Ok(transport)
|
|
||||||
}
|
|
||||||
pub fn new (transport: Option<Transport>) -> Self {
|
|
||||||
let timebase = Arc::new(Timebase::default());
|
|
||||||
Self {
|
|
||||||
_engine: Default::default(),
|
|
||||||
focused: false,
|
|
||||||
focus: TransportToolbarFocus::PlayPause,
|
|
||||||
playing: Some(TransportState::Stopped),
|
|
||||||
bpm: timebase.bpm(),
|
|
||||||
quant: 24,
|
|
||||||
sync: timebase.ppq() as usize * 4,
|
|
||||||
frame: 0,
|
|
||||||
pulse: 0,
|
|
||||||
ppq: 0,
|
|
||||||
usecs: 0,
|
|
||||||
metronome: false,
|
|
||||||
started: None,
|
|
||||||
jack: None,
|
|
||||||
transport,
|
|
||||||
timebase,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn toggle_play (&mut self) -> Usually<()> {
|
|
||||||
let transport = self.transport.as_ref().unwrap();
|
|
||||||
self.playing = match self.playing.expect("1st frame has not been processed yet") {
|
|
||||||
TransportState::Stopped => {
|
|
||||||
transport.start()?;
|
|
||||||
Some(TransportState::Starting)
|
|
||||||
},
|
|
||||||
_ => {
|
|
||||||
transport.stop()?;
|
|
||||||
transport.locate(0)?;
|
|
||||||
Some(TransportState::Stopped)
|
|
||||||
},
|
|
||||||
};
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
pub fn update (&mut self, scope: &ProcessScope) -> (bool, usize, usize, usize, usize, f64) {
|
|
||||||
let times = scope.cycle_times().unwrap();
|
|
||||||
let CycleTimes { current_frames, current_usecs, next_usecs, period_usecs } = times;
|
|
||||||
let chunk_size = scope.n_frames() as usize;
|
|
||||||
let transport = self.transport.as_ref().unwrap().query().unwrap();
|
|
||||||
self.frame = transport.pos.frame() as usize;
|
|
||||||
let mut reset = false;
|
|
||||||
if self.playing != Some(transport.state) {
|
|
||||||
match transport.state {
|
|
||||||
TransportState::Rolling => {
|
|
||||||
self.started = Some((current_frames as usize, current_usecs as usize));
|
|
||||||
},
|
|
||||||
TransportState::Stopped => {
|
|
||||||
self.started = None;
|
|
||||||
reset = true;
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.playing = Some(transport.state);
|
|
||||||
(
|
|
||||||
reset,
|
|
||||||
current_frames as usize,
|
|
||||||
chunk_size as usize,
|
|
||||||
current_usecs as usize,
|
|
||||||
next_usecs as usize,
|
|
||||||
period_usecs as f64
|
|
||||||
)
|
|
||||||
}
|
|
||||||
pub fn bpm (&self) -> usize {
|
|
||||||
self.timebase.bpm() as usize
|
|
||||||
}
|
|
||||||
pub fn ppq (&self) -> usize {
|
|
||||||
self.timebase.ppq() as usize
|
|
||||||
}
|
|
||||||
pub fn pulse (&self) -> usize {
|
|
||||||
self.timebase.frame_to_pulse(self.frame as f64) as usize
|
|
||||||
}
|
|
||||||
pub fn usecs (&self) -> usize {
|
|
||||||
self.timebase.frame_to_usec(self.frame as f64) as usize
|
|
||||||
}
|
|
||||||
pub fn quant (&self) -> usize {
|
|
||||||
self.quant
|
|
||||||
}
|
|
||||||
pub fn sync (&self) -> usize {
|
|
||||||
self.sync
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E: Engine> Audio for TransportToolbar<E> {
|
|
||||||
fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
|
|
||||||
self.update(&scope);
|
|
||||||
Control::Continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq)]
|
|
||||||
pub enum TransportToolbarFocus { PlayPause, Bpm, Quant, Sync, Clock, }
|
|
||||||
impl TransportToolbarFocus {
|
|
||||||
pub fn next (&mut self) {
|
|
||||||
*self = match self {
|
|
||||||
Self::PlayPause => Self::Bpm,
|
|
||||||
Self::Bpm => Self::Quant,
|
|
||||||
Self::Quant => Self::Sync,
|
|
||||||
Self::Sync => Self::Clock,
|
|
||||||
Self::Clock => Self::PlayPause,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn prev (&mut self) {
|
|
||||||
*self = match self {
|
|
||||||
Self::PlayPause => Self::Clock,
|
|
||||||
Self::Bpm => Self::PlayPause,
|
|
||||||
Self::Quant => Self::Bpm,
|
|
||||||
Self::Sync => Self::Quant,
|
|
||||||
Self::Clock => Self::Sync,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
|
||||||
|
|
@ -1,658 +1,5 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
const CORNERS: CornersTall = CornersTall(NOT_DIM_GREEN);
|
|
||||||
|
|
||||||
tui_style!(GRAY_DIM =
|
|
||||||
Some(Color::Gray), None, None, Modifier::DIM, Modifier::empty());
|
|
||||||
tui_style!(GRAY_NOT_DIM_BOLD =
|
|
||||||
Some(Color::Gray), None, None, Modifier::BOLD, Modifier::DIM);
|
|
||||||
tui_style!(NOT_DIM_BOLD =
|
|
||||||
None, None, None, Modifier::BOLD, Modifier::DIM);
|
|
||||||
tui_style!(NOT_DIM_GREEN =
|
|
||||||
Some(Color::Rgb(96, 255, 32)), Some(COLOR_BG1), None, Modifier::empty(), Modifier::DIM);
|
|
||||||
tui_style!(NOT_DIM =
|
|
||||||
None, None, None, Modifier::empty(), Modifier::DIM);
|
|
||||||
tui_style!(WHITE_NOT_DIM_BOLD =
|
|
||||||
Some(Color::White), None, None, Modifier::BOLD, Modifier::DIM);
|
|
||||||
tui_style!(STYLE_LABEL =
|
|
||||||
Some(Color::Reset), None, None, Modifier::empty(), Modifier::BOLD);
|
|
||||||
tui_style!(STYLE_VALUE =
|
|
||||||
Some(Color::White), None, None, Modifier::BOLD, Modifier::DIM);
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
impl Arranger<Tui> {
|
|
||||||
pub fn rename_selected (&mut self) {
|
|
||||||
self.modal = Some(Box::new(ArrangerRenameModal::new(
|
|
||||||
self.selected,
|
|
||||||
&match self.selected {
|
|
||||||
ArrangerFocus::Mix => self.name.clone(),
|
|
||||||
ArrangerFocus::Track(t) => self.tracks[t].name.clone(),
|
|
||||||
ArrangerFocus::Scene(s) => self.scenes[s].name.clone(),
|
|
||||||
ArrangerFocus::Clip(t, s) => self.tracks[t].phrases[s].read().unwrap().name.clone(),
|
|
||||||
}
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Focusable<Tui> for Arranger<Tui> {
|
|
||||||
fn is_focused (&self) -> bool {
|
|
||||||
self.focused
|
|
||||||
}
|
|
||||||
fn set_focused (&mut self, focused: bool) {
|
|
||||||
self.focused = focused
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Handle<Tui> for Arranger<Tui> {
|
|
||||||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
|
||||||
if let Some(modal) = self.modal.as_mut() {
|
|
||||||
let result = modal.handle(from)?;
|
|
||||||
if from.is_done() {
|
|
||||||
self.modal = None;
|
|
||||||
}
|
|
||||||
return Ok(result)
|
|
||||||
}
|
|
||||||
match from.event() {
|
|
||||||
// mode_switch: switch the display mode
|
|
||||||
key!(KeyCode::Char('`')) => {
|
|
||||||
self.mode.to_next()
|
|
||||||
},
|
|
||||||
// cursor_up: move cursor up
|
|
||||||
key!(KeyCode::Up) => {
|
|
||||||
match self.mode {
|
|
||||||
ArrangerViewMode::Horizontal => self.track_prev(),
|
|
||||||
_ => self.scene_prev(),
|
|
||||||
};
|
|
||||||
self.show_phrase();
|
|
||||||
},
|
|
||||||
// cursor_down
|
|
||||||
key!(KeyCode::Down) => {
|
|
||||||
match self.mode {
|
|
||||||
ArrangerViewMode::Horizontal => self.track_next(),
|
|
||||||
_ => self.scene_next(),
|
|
||||||
};
|
|
||||||
self.show_phrase();
|
|
||||||
},
|
|
||||||
// cursor left
|
|
||||||
key!(KeyCode::Left) => {
|
|
||||||
match self.mode {
|
|
||||||
ArrangerViewMode::Horizontal => self.scene_prev(),
|
|
||||||
_ => self.track_prev(),
|
|
||||||
};
|
|
||||||
self.show_phrase();
|
|
||||||
},
|
|
||||||
// cursor right
|
|
||||||
key!(KeyCode::Right) => {
|
|
||||||
match self.mode {
|
|
||||||
ArrangerViewMode::Horizontal => self.scene_next(),
|
|
||||||
_ => self.track_next(),
|
|
||||||
};
|
|
||||||
self.show_phrase();
|
|
||||||
},
|
|
||||||
// increment: remove clip
|
|
||||||
key!(KeyCode::Delete) => {
|
|
||||||
self.phrase_del();
|
|
||||||
self.show_phrase();
|
|
||||||
},
|
|
||||||
// increment: use next clip here
|
|
||||||
key!(KeyCode::Char('.')) => {
|
|
||||||
self.phrase_next();
|
|
||||||
self.show_phrase();
|
|
||||||
},
|
|
||||||
// decrement: use previous next clip here
|
|
||||||
key!(KeyCode::Char(',')) => {
|
|
||||||
self.phrase_prev();
|
|
||||||
self.show_phrase();
|
|
||||||
},
|
|
||||||
// decrement: use previous clip here
|
|
||||||
key!(KeyCode::Enter) => {
|
|
||||||
self.activate();
|
|
||||||
},
|
|
||||||
// scene_add: add a new scene
|
|
||||||
key!(Ctrl-KeyCode::Char('a')) => {
|
|
||||||
self.scene_add(None)?;
|
|
||||||
},
|
|
||||||
// track_add: add a new scene
|
|
||||||
key!(Ctrl-KeyCode::Char('t')) => {
|
|
||||||
self.track_add(None)?;
|
|
||||||
},
|
|
||||||
// rename: add a new scene
|
|
||||||
key!(KeyCode::Char('n')) => {
|
|
||||||
self.rename_selected();
|
|
||||||
},
|
|
||||||
// length: add a new scene
|
|
||||||
key!(KeyCode::Char('l')) => if let Some(phrase) = self.phrase() {
|
|
||||||
phrase.write().unwrap().toggle_loop()
|
|
||||||
},
|
|
||||||
// color: set color of item at cursor
|
|
||||||
key!(KeyCode::Char('c')) => {
|
|
||||||
todo!();
|
|
||||||
},
|
|
||||||
_ => return Ok(None)
|
|
||||||
}
|
|
||||||
Ok(Some(true))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Content for Arranger<Tui> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
|
||||||
Layers::new(move |add|{
|
|
||||||
match self.mode {
|
|
||||||
ArrangerViewMode::Horizontal => add(&HorizontalArranger(&self)),
|
|
||||||
ArrangerViewMode::Vertical(factor) => add(&VerticalArranger(&self, factor))
|
|
||||||
}?;
|
|
||||||
add(&Align::SE(self.selected.description(
|
|
||||||
&self.tracks,
|
|
||||||
&self.scenes,
|
|
||||||
).as_str()))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
impl<'a> Content for VerticalArranger<'a, Tui> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
|
||||||
let Self(state, factor) = self;
|
|
||||||
let ppq = 96;
|
|
||||||
let (cols, rows) = if *factor == 0 {(
|
|
||||||
track_clip_name_lengths(state.tracks.as_slice()),
|
|
||||||
scene_ppqs(state.tracks.as_slice(), state.scenes.as_slice()),
|
|
||||||
)} else {(
|
|
||||||
track_clip_name_lengths(state.tracks.as_slice()),
|
|
||||||
(0..=state.scenes.len()).map(|i|(factor*ppq, factor*ppq*i)).collect::<Vec<_>>(),
|
|
||||||
)};
|
|
||||||
//let height = rows.last().map(|(w,y)|(y+w)/ppq).unwrap_or(16);
|
|
||||||
let tracks: &[Sequencer<Tui>] = state.tracks.as_ref();
|
|
||||||
let scenes: &[Scene] = state.scenes.as_ref();
|
|
||||||
let offset = 4 + scene_name_max_len(scenes) as u16;
|
|
||||||
Layers::new(move |add|{
|
|
||||||
let rows: &[(usize, usize)] = rows.as_ref();
|
|
||||||
let cols: &[(usize, usize)] = cols.as_ref();
|
|
||||||
|
|
||||||
let track_titles = row!((track, (w, _)) in tracks.iter().zip(cols) =>
|
|
||||||
(&track.name.read().unwrap().as_str() as &dyn Widget<Engine = Tui>)
|
|
||||||
.min_xy(*w as u16, 2).push_x(offset));
|
|
||||||
|
|
||||||
let scene_name = |scene, playing: bool, height|row!(
|
|
||||||
if playing { "▶ " } else { " " },
|
|
||||||
(scene as &Scene).name.read().unwrap().as_str(),
|
|
||||||
).fixed_xy(offset.saturating_sub(1), height);
|
|
||||||
|
|
||||||
let scene_clip = |scene, track: usize, w: u16, h: u16|Layers::new(move |add|{
|
|
||||||
let mut color = Color::Rgb(40, 50, 30);
|
|
||||||
match (tracks.get(track), (scene as &Scene).clips.get(track)) {
|
|
||||||
(Some(track), Some(Some(clip))) => match track.phrases.get(*clip) {
|
|
||||||
Some(phrase) => {
|
|
||||||
let name = &(phrase as &Arc<RwLock<Phrase>>).read().unwrap().name;
|
|
||||||
let name = name.read().unwrap();
|
|
||||||
let name = format!("{clip:02} {}", name);
|
|
||||||
add(&name.as_str().push_x(1).fixed_x(w))?;
|
|
||||||
if (track as &Sequencer<_>).playing_phrase == Some(*clip) {
|
|
||||||
color = COLOR_PLAYING
|
|
||||||
} else {
|
|
||||||
color = COLOR_BG1
|
|
||||||
};
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
};
|
|
||||||
add(&Background(color))
|
|
||||||
}).fixed_xy(w, h);
|
|
||||||
|
|
||||||
let tracks_clips = col!((scene, (pulses, _)) in scenes.iter().zip(rows) => {
|
|
||||||
let height = 1.max((pulses / 96) as u16);
|
|
||||||
let playing = scene.is_playing(tracks);
|
|
||||||
Stack::right(move |add| {
|
|
||||||
add(&scene_name(scene, playing, height))?;
|
|
||||||
for (track, (w, _x)) in cols.iter().enumerate() {
|
|
||||||
add(&scene_clip(scene, track, *w as u16, height))?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}).fixed_y(height)
|
|
||||||
});
|
|
||||||
|
|
||||||
add(&VerticalArrangerGrid(offset, &rows, &cols))?;
|
|
||||||
add(&VerticalArrangerCursor(state.focused, state.selected, offset, &cols, &rows))?;
|
|
||||||
add(&col!(track_titles, tracks_clips))?;
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
.bg(Color::Rgb(28, 35, 25))
|
|
||||||
.border(Lozenge(Style::default()
|
|
||||||
.bg(Color::Rgb(40, 50, 30))
|
|
||||||
.fg(Color::Rgb(70, 80, 50))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn track_clip_name_lengths <E: Engine> (tracks: &[Sequencer<E>]) -> Vec<(usize, usize)> {
|
|
||||||
let mut total = 0;
|
|
||||||
let mut lengths: Vec<(usize, usize)> = tracks.iter().map(|track|{
|
|
||||||
let len = 4 + track.phrases
|
|
||||||
.iter()
|
|
||||||
.fold(track.name.read().unwrap().len(), |len, phrase|{
|
|
||||||
len.max(phrase.read().unwrap().name.read().unwrap().len())
|
|
||||||
});
|
|
||||||
total = total + len;
|
|
||||||
(len, total - len)
|
|
||||||
}).collect();
|
|
||||||
lengths.push((0, total));
|
|
||||||
lengths
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn scene_ppqs <E: Engine> (tracks: &[Sequencer<E>], scenes: &[Scene]) -> Vec<(usize, usize)> {
|
|
||||||
let mut total = 0;
|
|
||||||
let mut scenes: Vec<(usize, usize)> = scenes.iter().map(|scene|{
|
|
||||||
let pulses = scene.pulses(tracks).max(96);
|
|
||||||
total = total + pulses;
|
|
||||||
(pulses, total - pulses)
|
|
||||||
}).collect();
|
|
||||||
scenes.push((0, total));
|
|
||||||
scenes
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn scene_name_max_len (scenes: &[Scene]) -> usize {
|
|
||||||
scenes.iter()
|
|
||||||
.map(|s|s.name.read().unwrap().len())
|
|
||||||
.fold(0, usize::max)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Widget for VerticalArrangerGrid<'a> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
|
||||||
let area = to.area();
|
|
||||||
let Self(offset, rows, cols) = self;
|
|
||||||
let style = Some(Style::default().fg(COLOR_SEPARATOR));
|
|
||||||
for (_, x) in cols.iter() {
|
|
||||||
let x = offset + area.x() + *x as u16 - 1;
|
|
||||||
for y in area.y()..area.y2() {
|
|
||||||
to.blit(&"▎", x, y, style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (_, y) in rows.iter() {
|
|
||||||
let y = area.y() + (*y / 96) as u16 + 1;
|
|
||||||
if y >= to.buffer.area.height {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
for x in area.x()..area.x2().saturating_sub(2) {
|
|
||||||
if x < to.buffer.area.x && y < to.buffer.area.y {
|
|
||||||
let cell = to.buffer.get_mut(x, y);
|
|
||||||
cell.modifier = Modifier::UNDERLINED;
|
|
||||||
cell.underline_color = COLOR_SEPARATOR;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Widget for VerticalArrangerCursor<'a> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
|
||||||
let area = to.area();
|
|
||||||
let Self(focused, selected, offset, cols, rows) = *self;
|
|
||||||
let get_track_area = |t: usize| [
|
|
||||||
offset + area.x() + cols[t].1 as u16 - 1,
|
|
||||||
area.y(),
|
|
||||||
cols[t].0 as u16,
|
|
||||||
area.h()
|
|
||||||
];
|
|
||||||
let get_scene_area = |s: usize| [
|
|
||||||
area.x(),
|
|
||||||
2 + area.y() + (rows[s].1 / 96) as u16,
|
|
||||||
area.w(),
|
|
||||||
(rows[s].0 / 96) as u16
|
|
||||||
];
|
|
||||||
let get_clip_area = |t: usize, s: usize| [
|
|
||||||
offset + area.x() + cols[t].1 as u16 - 1,
|
|
||||||
2 + area.y() + (rows[s].1 / 96) as u16,
|
|
||||||
cols[t].0 as u16,
|
|
||||||
(rows[s].0 / 96) as u16
|
|
||||||
];
|
|
||||||
let mut track_area: Option<[u16;4]> = None;
|
|
||||||
let mut scene_area: Option<[u16;4]> = None;
|
|
||||||
let mut clip_area: Option<[u16;4]> = None;
|
|
||||||
let area = match selected {
|
|
||||||
ArrangerFocus::Mix => {
|
|
||||||
if focused {
|
|
||||||
to.fill_bg(area, Color::Rgb(40, 50, 30));
|
|
||||||
}
|
|
||||||
area
|
|
||||||
},
|
|
||||||
ArrangerFocus::Track(t) => {
|
|
||||||
track_area = Some(get_track_area(t));
|
|
||||||
area
|
|
||||||
},
|
|
||||||
ArrangerFocus::Scene(s) => {
|
|
||||||
scene_area = Some(get_scene_area(s));
|
|
||||||
area
|
|
||||||
},
|
|
||||||
ArrangerFocus::Clip(t, s) => {
|
|
||||||
track_area = Some(get_track_area(t));
|
|
||||||
scene_area = Some(get_scene_area(s));
|
|
||||||
clip_area = Some(get_clip_area(t, s));
|
|
||||||
area
|
|
||||||
},
|
|
||||||
};
|
|
||||||
if let Some([x, y, width, height]) = track_area {
|
|
||||||
to.fill_fg([x, y, 1, height], Color::Rgb(70, 80, 50));
|
|
||||||
to.fill_fg([x + width, y, 1, height], Color::Rgb(70, 80, 50));
|
|
||||||
}
|
|
||||||
if let Some([_, y, _, height]) = scene_area {
|
|
||||||
to.fill_ul([area.x(), y - 1, area.w(), 1], Color::Rgb(70, 80, 50));
|
|
||||||
to.fill_ul([area.x(), y + height - 1, area.w(), 1], Color::Rgb(70, 80, 50));
|
|
||||||
}
|
|
||||||
if focused {
|
|
||||||
if let Some(clip_area) = clip_area {
|
|
||||||
to.render_in(clip_area, &CORNERS)?;
|
|
||||||
to.fill_bg(clip_area, Color::Rgb(40, 50, 30));
|
|
||||||
} else if let Some(track_area) = track_area {
|
|
||||||
to.render_in(track_area.clip_h(2), &CORNERS)?;
|
|
||||||
to.fill_bg(track_area, Color::Rgb(40, 50, 30));
|
|
||||||
} else if let Some(scene_area) = scene_area {
|
|
||||||
to.render_in(scene_area.clip_w(offset-1), &CORNERS)?;
|
|
||||||
to.fill_bg(scene_area, Color::Rgb(40, 50, 30));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
impl<'a> Content for HorizontalArranger<'a, Tui> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
|
||||||
let Arranger { tracks, focused, selected, scenes, .. } = self.0;
|
|
||||||
let _tracks = tracks.as_slice();
|
|
||||||
lay!(
|
|
||||||
focused.then_some(Background(Color::Rgb(40, 50, 30))),
|
|
||||||
row!(
|
|
||||||
// name
|
|
||||||
CustomWidget::new(|_|{
|
|
||||||
todo!()
|
|
||||||
}, |_: &mut TuiOutput|{
|
|
||||||
todo!()
|
|
||||||
//let Self(tracks, selected) = self;
|
|
||||||
//let yellow = Some(Style::default().yellow().bold().not_dim());
|
|
||||||
//let white = Some(Style::default().white().bold().not_dim());
|
|
||||||
//let area = to.area();
|
|
||||||
//let area = [area.x(), area.y(), 3 + 5.max(track_name_max_len(tracks)) as u16, area.h()];
|
|
||||||
//let offset = 0; // track scroll offset
|
|
||||||
//for y in 0..area.h() {
|
|
||||||
//if y == 0 {
|
|
||||||
//to.blit(&"Mixer", area.x() + 1, area.y() + y, Some(DIM))?;
|
|
||||||
//} else if y % 2 == 0 {
|
|
||||||
//let index = (y as usize - 2) / 2 + offset;
|
|
||||||
//if let Some(track) = tracks.get(index) {
|
|
||||||
//let selected = selected.track() == Some(index);
|
|
||||||
//let style = if selected { yellow } else { white };
|
|
||||||
//to.blit(&format!(" {index:>02} "), area.x(), area.y() + y, style)?;
|
|
||||||
//to.blit(&*track.name.read().unwrap(), area.x() + 4, area.y() + y, style)?;
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//Ok(Some(area))
|
|
||||||
}),
|
|
||||||
// monitor
|
|
||||||
CustomWidget::new(|_|{
|
|
||||||
todo!()
|
|
||||||
}, |_: &mut TuiOutput|{
|
|
||||||
todo!()
|
|
||||||
//let Self(tracks) = self;
|
|
||||||
//let mut area = to.area();
|
|
||||||
//let on = Some(Style::default().not_dim().green().bold());
|
|
||||||
//let off = Some(DIM);
|
|
||||||
//area.x += 1;
|
|
||||||
//for y in 0..area.h() {
|
|
||||||
//if y == 0 {
|
|
||||||
////" MON ".blit(to.buffer, area.x, area.y + y, style2)?;
|
|
||||||
//} else if y % 2 == 0 {
|
|
||||||
//let index = (y as usize - 2) / 2;
|
|
||||||
//if let Some(track) = tracks.get(index) {
|
|
||||||
//let style = if track.monitoring { on } else { off };
|
|
||||||
//to.blit(&" MON ", area.x(), area.y() + y, style)?;
|
|
||||||
//} else {
|
|
||||||
//area.height = y;
|
|
||||||
//break
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//area.width = 4;
|
|
||||||
//Ok(Some(area))
|
|
||||||
}),
|
|
||||||
// record
|
|
||||||
CustomWidget::new(|_|{
|
|
||||||
todo!()
|
|
||||||
}, |_: &mut TuiOutput|{
|
|
||||||
todo!()
|
|
||||||
//let Self(tracks) = self;
|
|
||||||
//let mut area = to.area();
|
|
||||||
//let on = Some(Style::default().not_dim().red().bold());
|
|
||||||
//let off = Some(Style::default().dim());
|
|
||||||
//area.x += 1;
|
|
||||||
//for y in 0..area.h() {
|
|
||||||
//if y == 0 {
|
|
||||||
////" REC ".blit(to.buffer, area.x, area.y + y, style2)?;
|
|
||||||
//} else if y % 2 == 0 {
|
|
||||||
//let index = (y as usize - 2) / 2;
|
|
||||||
//if let Some(track) = tracks.get(index) {
|
|
||||||
//let style = if track.recording { on } else { off };
|
|
||||||
//to.blit(&" REC ", area.x(), area.y() + y, style)?;
|
|
||||||
//} else {
|
|
||||||
//area.height = y;
|
|
||||||
//break
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//area.width = 4;
|
|
||||||
//Ok(Some(area))
|
|
||||||
}),
|
|
||||||
// overdub
|
|
||||||
CustomWidget::new(|_|{
|
|
||||||
todo!()
|
|
||||||
}, |_: &mut TuiOutput|{
|
|
||||||
todo!()
|
|
||||||
//let Self(tracks) = self;
|
|
||||||
//let mut area = to.area();
|
|
||||||
//let on = Some(Style::default().not_dim().yellow().bold());
|
|
||||||
//let off = Some(Style::default().dim());
|
|
||||||
//area.x = area.x + 1;
|
|
||||||
//for y in 0..area.h() {
|
|
||||||
//if y == 0 {
|
|
||||||
////" OVR ".blit(to.buffer, area.x, area.y + y, style2)?;
|
|
||||||
//} else if y % 2 == 0 {
|
|
||||||
//let index = (y as usize - 2) / 2;
|
|
||||||
//if let Some(track) = tracks.get(index) {
|
|
||||||
//to.blit(&" OVR ", area.x(), area.y() + y, if track.overdub {
|
|
||||||
//on
|
|
||||||
//} else {
|
|
||||||
//off
|
|
||||||
//})?;
|
|
||||||
//} else {
|
|
||||||
//area.height = y;
|
|
||||||
//break
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//area.width = 4;
|
|
||||||
//Ok(Some(area))
|
|
||||||
}),
|
|
||||||
// erase
|
|
||||||
CustomWidget::new(|_|{
|
|
||||||
todo!()
|
|
||||||
}, |_: &mut TuiOutput|{
|
|
||||||
todo!()
|
|
||||||
//let Self(tracks) = self;
|
|
||||||
//let mut area = to.area();
|
|
||||||
//let off = Some(Style::default().dim());
|
|
||||||
//area.x = area.x + 1;
|
|
||||||
//for y in 0..area.h() {
|
|
||||||
//if y == 0 {
|
|
||||||
////" DEL ".blit(to.buffer, area.x, area.y + y, style2)?;
|
|
||||||
//} else if y % 2 == 0 {
|
|
||||||
//let index = (y as usize - 2) / 2;
|
|
||||||
//if let Some(_) = tracks.get(index) {
|
|
||||||
//to.blit(&" DEL ", area.x(), area.y() + y, off)?;
|
|
||||||
//} else {
|
|
||||||
//area.height = y;
|
|
||||||
//break
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//area.width = 4;
|
|
||||||
//Ok(Some(area))
|
|
||||||
}),
|
|
||||||
// gain
|
|
||||||
CustomWidget::new(|_|{
|
|
||||||
todo!()
|
|
||||||
}, |_: &mut TuiOutput|{
|
|
||||||
todo!()
|
|
||||||
//let Self(tracks) = self;
|
|
||||||
//let mut area = to.area();
|
|
||||||
//let off = Some(Style::default().dim());
|
|
||||||
//area.x = area.x() + 1;
|
|
||||||
//for y in 0..area.h() {
|
|
||||||
//if y == 0 {
|
|
||||||
////" GAIN ".blit(to.buffer, area.x, area.y + y, style2)?;
|
|
||||||
//} else if y % 2 == 0 {
|
|
||||||
//let index = (y as usize - 2) / 2;
|
|
||||||
//if let Some(_) = tracks.get(index) {
|
|
||||||
//to.blit(&" +0.0 ", area.x(), area.y() + y, off)?;
|
|
||||||
//} else {
|
|
||||||
//area.height = y;
|
|
||||||
//break
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//area.width = 7;
|
|
||||||
//Ok(Some(area))
|
|
||||||
}),
|
|
||||||
// scenes
|
|
||||||
CustomWidget::new(|_|{
|
|
||||||
todo!()
|
|
||||||
}, |to: &mut TuiOutput|{
|
|
||||||
let Arranger { tracks, scenes, selected, .. } = self.0;
|
|
||||||
let area = to.area();
|
|
||||||
let mut x2 = 0;
|
|
||||||
let [x, y, _, height] = area;
|
|
||||||
for (scene_index, scene) in scenes.iter().enumerate() {
|
|
||||||
let active_scene = selected.scene() == Some(scene_index);
|
|
||||||
let sep = Some(if active_scene {
|
|
||||||
Style::default().yellow().not_dim()
|
|
||||||
} else {
|
|
||||||
Style::default().dim()
|
|
||||||
});
|
|
||||||
for y in y+1..y+height {
|
|
||||||
to.blit(&"│", x + x2, y, sep);
|
|
||||||
}
|
|
||||||
let name = scene.name.read().unwrap();
|
|
||||||
let mut x3 = name.len() as u16;
|
|
||||||
to.blit(&*name, x + x2, y, sep);
|
|
||||||
for (i, clip) in scene.clips.iter().enumerate() {
|
|
||||||
let active_track = selected.track() == Some(i);
|
|
||||||
if let Some(clip) = clip {
|
|
||||||
let y2 = y + 2 + i as u16 * 2;
|
|
||||||
let label = match tracks[i].phrases.get(*clip) {
|
|
||||||
Some(phrase) => &format!("{}", phrase.read().unwrap().name.read().unwrap()),
|
|
||||||
None => "...."
|
|
||||||
};
|
|
||||||
to.blit(&label, x + x2, y2, Some(if active_track && active_scene {
|
|
||||||
Style::default().not_dim().yellow().bold()
|
|
||||||
} else {
|
|
||||||
Style::default().not_dim()
|
|
||||||
}));
|
|
||||||
x3 = x3.max(label.len() as u16)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
x2 = x2 + x3 + 1;
|
|
||||||
}
|
|
||||||
//Ok(Some([x, y, x2, height]))
|
|
||||||
Ok(())
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn track_name_max_len <E: Engine> (tracks: &[Sequencer<E>]) -> usize {
|
|
||||||
tracks.iter()
|
|
||||||
.map(|s|s.name.read().unwrap().len())
|
|
||||||
.fold(0, usize::max)
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
impl Content for ArrangerRenameModal<Tui> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
|
||||||
todo!();
|
|
||||||
Layers::new(|add|{Ok(())})
|
|
||||||
//let area = to.area();
|
|
||||||
//let y = area.y() + area.h() / 2;
|
|
||||||
//let bg_area = [1, y - 1, area.w() - 2, 3];
|
|
||||||
//to.fill_bg(bg_area, COLOR_BG1);
|
|
||||||
//Lozenge(Style::default().bold().white().dim()).draw(to.with_rect(bg_area));
|
|
||||||
//let label = match self.target {
|
|
||||||
//ArrangerFocus::Mix => "Rename project:",
|
|
||||||
//ArrangerFocus::Track(_) => "Rename track:",
|
|
||||||
//ArrangerFocus::Scene(_) => "Rename scene:",
|
|
||||||
//ArrangerFocus::Clip(_, _) => "Rename clip:",
|
|
||||||
//};
|
|
||||||
//let style = Some(Style::default().not_bold().white().not_dim());
|
|
||||||
//to.blit(&label, area.x() + 3, y, style);
|
|
||||||
//let style = Some(Style::default().bold().white().not_dim());
|
|
||||||
//to.blit(&self.value, area.x() + 3 + label.len() as u16 + 1, y, style);
|
|
||||||
//let style = Some(Style::default().bold().white().not_dim().reversed());
|
|
||||||
//to.blit(&"▂", area.x() + 3 + label.len() as u16 + 1 + self.cursor as u16, y, style);
|
|
||||||
//Ok(Some(area))
|
|
||||||
//Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Handle<Tui> for ArrangerRenameModal<Tui> {
|
|
||||||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
|
||||||
match from.event() {
|
|
||||||
TuiEvent::Input(Event::Key(k)) => {
|
|
||||||
match k.code {
|
|
||||||
KeyCode::Esc => {
|
|
||||||
self.exit();
|
|
||||||
},
|
|
||||||
KeyCode::Enter => {
|
|
||||||
*self.result.write().unwrap() = self.value.clone();
|
|
||||||
self.exit();
|
|
||||||
},
|
|
||||||
KeyCode::Left => {
|
|
||||||
self.cursor = self.cursor.saturating_sub(1);
|
|
||||||
},
|
|
||||||
KeyCode::Right => {
|
|
||||||
self.cursor = self.value.len().min(self.cursor + 1)
|
|
||||||
},
|
|
||||||
KeyCode::Backspace => {
|
|
||||||
let last = self.value.len().saturating_sub(1);
|
|
||||||
self.value = format!("{}{}",
|
|
||||||
&self.value[0..self.cursor.min(last)],
|
|
||||||
&self.value[self.cursor.min(last)..last]
|
|
||||||
);
|
|
||||||
self.cursor = self.cursor.saturating_sub(1)
|
|
||||||
}
|
|
||||||
KeyCode::Char(c) => {
|
|
||||||
self.value.insert(self.cursor, c);
|
|
||||||
self.cursor = self.value.len().min(self.cursor + 1)
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
Ok(Some(true))
|
|
||||||
},
|
|
||||||
_ => Ok(None),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
impl Sequencer<Tui> {
|
impl Sequencer<Tui> {
|
||||||
const H_KEYS_OFFSET: usize = 5;
|
const H_KEYS_OFFSET: usize = 5;
|
||||||
/// Select which pattern to display. This pre-renders it to the buffer at full resolution.
|
/// Select which pattern to display. This pre-renders it to the buffer at full resolution.
|
||||||
|
|
@ -954,125 +301,3 @@ impl Handle<Tui> for Sequencer<Tui> {
|
||||||
return Ok(Some(true))
|
return Ok(Some(true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
impl TransportToolbar<Tui> {
|
|
||||||
fn handle_bpm (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
|
||||||
match from.event() {
|
|
||||||
key!(KeyCode::Char(',')) => { self.bpm -= 1.0; },
|
|
||||||
key!(KeyCode::Char('.')) => { self.bpm += 1.0; },
|
|
||||||
key!(KeyCode::Char('<')) => { self.bpm -= 0.001; },
|
|
||||||
key!(KeyCode::Char('>')) => { self.bpm += 0.001; },
|
|
||||||
_ => return Ok(None)
|
|
||||||
}
|
|
||||||
Ok(Some(true))
|
|
||||||
}
|
|
||||||
fn handle_quant (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
|
||||||
match from.event() {
|
|
||||||
key!(KeyCode::Char(',')) => {
|
|
||||||
self.quant = prev_note_length(self.quant);
|
|
||||||
Ok(Some(true))
|
|
||||||
},
|
|
||||||
key!(KeyCode::Char('.')) => {
|
|
||||||
self.quant = next_note_length(self.quant);
|
|
||||||
Ok(Some(true))
|
|
||||||
},
|
|
||||||
_ => Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn handle_sync (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
|
||||||
match from.event() {
|
|
||||||
key!(KeyCode::Char(',')) => {
|
|
||||||
self.sync = prev_note_length(self.sync);
|
|
||||||
Ok(Some(true))
|
|
||||||
},
|
|
||||||
key!(KeyCode::Char('.')) => {
|
|
||||||
self.sync = next_note_length(self.sync);
|
|
||||||
Ok(Some(true))
|
|
||||||
},
|
|
||||||
_ => Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Handle<Tui> for TransportToolbar<Tui> {
|
|
||||||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
|
||||||
match from.event() {
|
|
||||||
key!(KeyCode::Left) => { self.focus.prev(); },
|
|
||||||
key!(KeyCode::Right) => { self.focus.next(); },
|
|
||||||
_ => match self.focus {
|
|
||||||
TransportToolbarFocus::PlayPause => self.toggle_play().map(|_|())?,
|
|
||||||
TransportToolbarFocus::Bpm => self.handle_bpm(from).map(|_|())?,
|
|
||||||
TransportToolbarFocus::Quant => self.handle_quant(from).map(|_|())?,
|
|
||||||
TransportToolbarFocus::Sync => self.handle_sync(from).map(|_|())?,
|
|
||||||
TransportToolbarFocus::Clock => {/*todo*/},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(Some(true))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Content for TransportToolbar<Tui> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
|
||||||
row!(
|
|
||||||
self.focus.wrap(self.focused, TransportToolbarFocus::PlayPause, &Styled(
|
|
||||||
match self.playing {
|
|
||||||
Some(TransportState::Stopped) => Some(GRAY_DIM.bold()),
|
|
||||||
Some(TransportState::Starting) => Some(GRAY_NOT_DIM_BOLD),
|
|
||||||
Some(TransportState::Rolling) => Some(WHITE_NOT_DIM_BOLD),
|
|
||||||
_ => unreachable!(),
|
|
||||||
},
|
|
||||||
match self.playing {
|
|
||||||
Some(TransportState::Rolling) => "▶ PLAYING",
|
|
||||||
Some(TransportState::Starting) => "READY ...",
|
|
||||||
Some(TransportState::Stopped) => "⏹ STOPPED",
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
).min_xy(11, 2).push_x(1)),
|
|
||||||
self.focus.wrap(self.focused, TransportToolbarFocus::Bpm, &Outset::X(1u16, col! {
|
|
||||||
"BPM", format!("{}.{:03}", self.bpm as usize, (self.bpm * 1000.0) % 1000.0)
|
|
||||||
})),
|
|
||||||
self.focus.wrap(self.focused, TransportToolbarFocus::Quant, &Outset::X(1u16, col! {
|
|
||||||
"QUANT", ppq_to_name(self.quant as usize)
|
|
||||||
})),
|
|
||||||
self.focus.wrap(self.focused, TransportToolbarFocus::Sync, &Outset::X(1u16, col! {
|
|
||||||
"SYNC", ppq_to_name(self.sync as usize)
|
|
||||||
})),
|
|
||||||
self.focus.wrap(self.focused, TransportToolbarFocus::Clock, &{
|
|
||||||
let Self { frame: _frame, pulse, ppq, usecs, .. } = self;
|
|
||||||
let (beats, pulses) = if *ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) };
|
|
||||||
let (bars, beats) = ((beats / 4) + 1, (beats % 4) + 1);
|
|
||||||
let (seconds, msecs) = (usecs / 1000000, usecs / 1000 % 1000);
|
|
||||||
let (minutes, seconds) = (seconds / 60, seconds % 60);
|
|
||||||
let time1 = format!("{bars}.{beats}.{pulses:02}");
|
|
||||||
let time2 = format!("{minutes}:{seconds:02}:{msecs:03}");
|
|
||||||
col!(time1.as_str(), time2.as_str()).outset_x(1)
|
|
||||||
}),
|
|
||||||
).fill_x().bg(Color::Rgb(40, 50, 30))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Focusable<Tui> for TransportToolbar<Tui> {
|
|
||||||
fn is_focused (&self) -> bool {
|
|
||||||
self.focused
|
|
||||||
}
|
|
||||||
fn set_focused (&mut self, focused: bool) {
|
|
||||||
self.focused = focused
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TransportToolbarFocus {
|
|
||||||
pub fn wrap <'a, W: Widget<Engine = Tui>> (
|
|
||||||
self, parent_focus: bool, focus: Self, widget: &'a W
|
|
||||||
) -> impl Widget<Engine = Tui> + 'a {
|
|
||||||
Layers::new(move |add|{
|
|
||||||
if parent_focus && focus == self {
|
|
||||||
add(&CORNERS)?;
|
|
||||||
add(&Background(Color::Rgb(60, 70, 50)))?;
|
|
||||||
}
|
|
||||||
add(widget)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
162
crates/tek_sequencer/src/transport.rs
Normal file
162
crates/tek_sequencer/src/transport.rs
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// Stores and displays time-related state.
|
||||||
|
pub struct TransportToolbar<E: Engine> {
|
||||||
|
_engine: PhantomData<E>,
|
||||||
|
/// Enable metronome?
|
||||||
|
pub metronome: bool,
|
||||||
|
/// Current sample rate, tempo, and PPQ.
|
||||||
|
pub timebase: Arc<Timebase>,
|
||||||
|
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
||||||
|
pub jack: Option<JackClient>,
|
||||||
|
/// JACK transport handle.
|
||||||
|
pub transport: Option<Transport>,
|
||||||
|
/// Quantization factor
|
||||||
|
/// Global frame and usec at which playback started
|
||||||
|
pub started: Option<(usize, usize)>,
|
||||||
|
pub focused: bool,
|
||||||
|
pub focus: TransportToolbarFocus,
|
||||||
|
pub playing: Option<TransportState>,
|
||||||
|
pub bpm: f64,
|
||||||
|
pub quant: usize,
|
||||||
|
pub sync: usize,
|
||||||
|
pub frame: usize,
|
||||||
|
pub pulse: usize,
|
||||||
|
pub ppq: usize,
|
||||||
|
pub usecs: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: Engine> TransportToolbar<E> {
|
||||||
|
pub fn standalone () -> Usually<Arc<RwLock<Self>>> where Self: 'static {
|
||||||
|
let jack = JackClient::Inactive(
|
||||||
|
Client::new("tek_transport", ClientOptions::NO_START_SERVER)?.0
|
||||||
|
);
|
||||||
|
let mut transport = Self::new(Some(jack.transport()));
|
||||||
|
transport.focused = true;
|
||||||
|
let transport = Arc::new(RwLock::new(transport));
|
||||||
|
transport.write().unwrap().jack = Some(
|
||||||
|
jack.activate(
|
||||||
|
&transport.clone(),
|
||||||
|
|state: &Arc<RwLock<TransportToolbar<E>>>, client, scope| {
|
||||||
|
state.write().unwrap().process(client, scope)
|
||||||
|
}
|
||||||
|
)?
|
||||||
|
);
|
||||||
|
Ok(transport)
|
||||||
|
}
|
||||||
|
pub fn new (transport: Option<Transport>) -> Self {
|
||||||
|
let timebase = Arc::new(Timebase::default());
|
||||||
|
Self {
|
||||||
|
_engine: Default::default(),
|
||||||
|
focused: false,
|
||||||
|
focus: TransportToolbarFocus::PlayPause,
|
||||||
|
playing: Some(TransportState::Stopped),
|
||||||
|
bpm: timebase.bpm(),
|
||||||
|
quant: 24,
|
||||||
|
sync: timebase.ppq() as usize * 4,
|
||||||
|
frame: 0,
|
||||||
|
pulse: 0,
|
||||||
|
ppq: 0,
|
||||||
|
usecs: 0,
|
||||||
|
metronome: false,
|
||||||
|
started: None,
|
||||||
|
jack: None,
|
||||||
|
transport,
|
||||||
|
timebase,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn toggle_play (&mut self) -> Usually<()> {
|
||||||
|
let transport = self.transport.as_ref().unwrap();
|
||||||
|
self.playing = match self.playing.expect("1st frame has not been processed yet") {
|
||||||
|
TransportState::Stopped => {
|
||||||
|
transport.start()?;
|
||||||
|
Some(TransportState::Starting)
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
transport.stop()?;
|
||||||
|
transport.locate(0)?;
|
||||||
|
Some(TransportState::Stopped)
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn update (&mut self, scope: &ProcessScope) -> (bool, usize, usize, usize, usize, f64) {
|
||||||
|
let times = scope.cycle_times().unwrap();
|
||||||
|
let CycleTimes { current_frames, current_usecs, next_usecs, period_usecs } = times;
|
||||||
|
let chunk_size = scope.n_frames() as usize;
|
||||||
|
let transport = self.transport.as_ref().unwrap().query().unwrap();
|
||||||
|
self.frame = transport.pos.frame() as usize;
|
||||||
|
let mut reset = false;
|
||||||
|
if self.playing != Some(transport.state) {
|
||||||
|
match transport.state {
|
||||||
|
TransportState::Rolling => {
|
||||||
|
self.started = Some((current_frames as usize, current_usecs as usize));
|
||||||
|
},
|
||||||
|
TransportState::Stopped => {
|
||||||
|
self.started = None;
|
||||||
|
reset = true;
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.playing = Some(transport.state);
|
||||||
|
(
|
||||||
|
reset,
|
||||||
|
current_frames as usize,
|
||||||
|
chunk_size as usize,
|
||||||
|
current_usecs as usize,
|
||||||
|
next_usecs as usize,
|
||||||
|
period_usecs as f64
|
||||||
|
)
|
||||||
|
}
|
||||||
|
pub fn bpm (&self) -> usize {
|
||||||
|
self.timebase.bpm() as usize
|
||||||
|
}
|
||||||
|
pub fn ppq (&self) -> usize {
|
||||||
|
self.timebase.ppq() as usize
|
||||||
|
}
|
||||||
|
pub fn pulse (&self) -> usize {
|
||||||
|
self.timebase.frame_to_pulse(self.frame as f64) as usize
|
||||||
|
}
|
||||||
|
pub fn usecs (&self) -> usize {
|
||||||
|
self.timebase.frame_to_usec(self.frame as f64) as usize
|
||||||
|
}
|
||||||
|
pub fn quant (&self) -> usize {
|
||||||
|
self.quant
|
||||||
|
}
|
||||||
|
pub fn sync (&self) -> usize {
|
||||||
|
self.sync
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: Engine> Audio for TransportToolbar<E> {
|
||||||
|
fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
|
||||||
|
self.update(&scope);
|
||||||
|
Control::Continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
|
pub enum TransportToolbarFocus { PlayPause, Bpm, Quant, Sync, Clock, }
|
||||||
|
impl TransportToolbarFocus {
|
||||||
|
pub fn next (&mut self) {
|
||||||
|
*self = match self {
|
||||||
|
Self::PlayPause => Self::Bpm,
|
||||||
|
Self::Bpm => Self::Quant,
|
||||||
|
Self::Quant => Self::Sync,
|
||||||
|
Self::Sync => Self::Clock,
|
||||||
|
Self::Clock => Self::PlayPause,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn prev (&mut self) {
|
||||||
|
*self = match self {
|
||||||
|
Self::PlayPause => Self::Clock,
|
||||||
|
Self::Bpm => Self::PlayPause,
|
||||||
|
Self::Quant => Self::Bpm,
|
||||||
|
Self::Sync => Self::Quant,
|
||||||
|
Self::Clock => Self::Sync,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
121
crates/tek_sequencer/src/transport_tui.rs
Normal file
121
crates/tek_sequencer/src/transport_tui.rs
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
impl TransportToolbar<Tui> {
|
||||||
|
fn handle_bpm (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||||
|
match from.event() {
|
||||||
|
key!(KeyCode::Char(',')) => { self.bpm -= 1.0; },
|
||||||
|
key!(KeyCode::Char('.')) => { self.bpm += 1.0; },
|
||||||
|
key!(KeyCode::Char('<')) => { self.bpm -= 0.001; },
|
||||||
|
key!(KeyCode::Char('>')) => { self.bpm += 0.001; },
|
||||||
|
_ => return Ok(None)
|
||||||
|
}
|
||||||
|
Ok(Some(true))
|
||||||
|
}
|
||||||
|
fn handle_quant (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||||
|
match from.event() {
|
||||||
|
key!(KeyCode::Char(',')) => {
|
||||||
|
self.quant = prev_note_length(self.quant);
|
||||||
|
Ok(Some(true))
|
||||||
|
},
|
||||||
|
key!(KeyCode::Char('.')) => {
|
||||||
|
self.quant = next_note_length(self.quant);
|
||||||
|
Ok(Some(true))
|
||||||
|
},
|
||||||
|
_ => Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn handle_sync (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||||
|
match from.event() {
|
||||||
|
key!(KeyCode::Char(',')) => {
|
||||||
|
self.sync = prev_note_length(self.sync);
|
||||||
|
Ok(Some(true))
|
||||||
|
},
|
||||||
|
key!(KeyCode::Char('.')) => {
|
||||||
|
self.sync = next_note_length(self.sync);
|
||||||
|
Ok(Some(true))
|
||||||
|
},
|
||||||
|
_ => Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handle<Tui> for TransportToolbar<Tui> {
|
||||||
|
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||||
|
match from.event() {
|
||||||
|
key!(KeyCode::Left) => { self.focus.prev(); },
|
||||||
|
key!(KeyCode::Right) => { self.focus.next(); },
|
||||||
|
_ => match self.focus {
|
||||||
|
TransportToolbarFocus::PlayPause => self.toggle_play().map(|_|())?,
|
||||||
|
TransportToolbarFocus::Bpm => self.handle_bpm(from).map(|_|())?,
|
||||||
|
TransportToolbarFocus::Quant => self.handle_quant(from).map(|_|())?,
|
||||||
|
TransportToolbarFocus::Sync => self.handle_sync(from).map(|_|())?,
|
||||||
|
TransportToolbarFocus::Clock => {/*todo*/},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Some(true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Content for TransportToolbar<Tui> {
|
||||||
|
type Engine = Tui;
|
||||||
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||||
|
row!(
|
||||||
|
self.focus.wrap(self.focused, TransportToolbarFocus::PlayPause, &Styled(
|
||||||
|
match self.playing {
|
||||||
|
Some(TransportState::Stopped) => Some(GRAY_DIM.bold()),
|
||||||
|
Some(TransportState::Starting) => Some(GRAY_NOT_DIM_BOLD),
|
||||||
|
Some(TransportState::Rolling) => Some(WHITE_NOT_DIM_BOLD),
|
||||||
|
_ => unreachable!(),
|
||||||
|
},
|
||||||
|
match self.playing {
|
||||||
|
Some(TransportState::Rolling) => "▶ PLAYING",
|
||||||
|
Some(TransportState::Starting) => "READY ...",
|
||||||
|
Some(TransportState::Stopped) => "⏹ STOPPED",
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
).min_xy(11, 2).push_x(1)),
|
||||||
|
self.focus.wrap(self.focused, TransportToolbarFocus::Bpm, &Outset::X(1u16, col! {
|
||||||
|
"BPM", format!("{}.{:03}", self.bpm as usize, (self.bpm * 1000.0) % 1000.0)
|
||||||
|
})),
|
||||||
|
self.focus.wrap(self.focused, TransportToolbarFocus::Quant, &Outset::X(1u16, col! {
|
||||||
|
"QUANT", ppq_to_name(self.quant as usize)
|
||||||
|
})),
|
||||||
|
self.focus.wrap(self.focused, TransportToolbarFocus::Sync, &Outset::X(1u16, col! {
|
||||||
|
"SYNC", ppq_to_name(self.sync as usize)
|
||||||
|
})),
|
||||||
|
self.focus.wrap(self.focused, TransportToolbarFocus::Clock, &{
|
||||||
|
let Self { frame: _frame, pulse, ppq, usecs, .. } = self;
|
||||||
|
let (beats, pulses) = if *ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) };
|
||||||
|
let (bars, beats) = ((beats / 4) + 1, (beats % 4) + 1);
|
||||||
|
let (seconds, msecs) = (usecs / 1000000, usecs / 1000 % 1000);
|
||||||
|
let (minutes, seconds) = (seconds / 60, seconds % 60);
|
||||||
|
let time1 = format!("{bars}.{beats}.{pulses:02}");
|
||||||
|
let time2 = format!("{minutes}:{seconds:02}:{msecs:03}");
|
||||||
|
col!(time1.as_str(), time2.as_str()).outset_x(1)
|
||||||
|
}),
|
||||||
|
).fill_x().bg(Color::Rgb(40, 50, 30))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Focusable<Tui> for TransportToolbar<Tui> {
|
||||||
|
fn is_focused (&self) -> bool {
|
||||||
|
self.focused
|
||||||
|
}
|
||||||
|
fn set_focused (&mut self, focused: bool) {
|
||||||
|
self.focused = focused
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransportToolbarFocus {
|
||||||
|
pub fn wrap <'a, W: Widget<Engine = Tui>> (
|
||||||
|
self, parent_focus: bool, focus: Self, widget: &'a W
|
||||||
|
) -> impl Widget<Engine = Tui> + 'a {
|
||||||
|
Layers::new(move |add|{
|
||||||
|
if parent_focus && focus == self {
|
||||||
|
add(&CORNERS)?;
|
||||||
|
add(&Background(Color::Rgb(60, 70, 50)))?;
|
||||||
|
}
|
||||||
|
add(widget)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue