mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-07 12:16:42 +01:00
wip: separate PhrasePlayer vs PhraseEditor
This commit is contained in:
parent
7668a6f339
commit
25e54eba4e
8 changed files with 491 additions and 636 deletions
|
|
@ -6,20 +6,17 @@ pub struct Arranger<E: Engine> {
|
||||||
pub transport: Option<Arc<RwLock<TransportToolbar<E>>>>,
|
pub transport: Option<Arc<RwLock<TransportToolbar<E>>>>,
|
||||||
/// Contains all the sequencers.
|
/// Contains all the sequencers.
|
||||||
pub arrangement: Arrangement<E>,
|
pub arrangement: Arrangement<E>,
|
||||||
|
/// Pool of all phrases in the arrangement
|
||||||
|
pub phrases: Arc<RwLock<PhrasePool<E>>>,
|
||||||
|
/// Phrase editor view
|
||||||
|
pub editor: PhraseEditor<E>,
|
||||||
/// This allows the sequencer view to be moved or hidden.
|
/// This allows the sequencer view to be moved or hidden.
|
||||||
pub show_sequencer: Option<tek_core::Direction>,
|
pub show_sequencer: Option<tek_core::Direction>,
|
||||||
/// Index of currently focused component
|
/// Index of currently focused component
|
||||||
pub focus: usize,
|
pub focus: usize,
|
||||||
/// Focus target that passes events down to sequencer
|
|
||||||
pub sequencer_proxy: SequencerProxy<E>,
|
|
||||||
/// Slot for modal dialog displayed on top of app.
|
/// Slot for modal dialog displayed on top of app.
|
||||||
pub modal: Option<Box<dyn ContentComponent<E>>>,
|
pub modal: Option<Box<dyn ContentComponent<E>>>,
|
||||||
|
|
||||||
|
|
||||||
pub phrases: Arc<RwLock<PhrasePool<E>>>,
|
|
||||||
pub editor: PhraseEditor<E>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents the tracks and scenes of the composition.
|
/// Represents the tracks and scenes of the composition.
|
||||||
pub struct Arrangement<E: Engine> {
|
pub struct Arrangement<E: Engine> {
|
||||||
/// Name of arranger
|
/// Name of arranger
|
||||||
|
|
@ -27,23 +24,77 @@ pub struct Arrangement<E: Engine> {
|
||||||
/// Collection of phrases.
|
/// Collection of phrases.
|
||||||
pub phrases: Arc<RwLock<PhrasePool<E>>>,
|
pub phrases: Arc<RwLock<PhrasePool<E>>>,
|
||||||
/// Collection of tracks.
|
/// Collection of tracks.
|
||||||
pub tracks: Vec<Sequencer<E>>,
|
pub tracks: Vec<ArrangementTrack<E>>,
|
||||||
/// Collection of scenes.
|
/// Collection of scenes.
|
||||||
pub scenes: Vec<Scene>,
|
pub scenes: Vec<Scene>,
|
||||||
/// Currently selected element.
|
/// Currently selected element.
|
||||||
pub selected: ArrangerFocus,
|
pub selected: ArrangementFocus,
|
||||||
/// Display mode of arranger
|
/// Display mode of arranger
|
||||||
pub mode: ArrangerViewMode,
|
pub mode: ArrangementViewMode,
|
||||||
/// Whether the arranger is currently focused
|
/// Whether the arranger is currently focused
|
||||||
pub focused: bool
|
pub focused: bool
|
||||||
}
|
}
|
||||||
|
/// Represents a track in the arrangement
|
||||||
|
pub struct ArrangementTrack<E: Engine> {
|
||||||
|
/// Name of track
|
||||||
|
pub name: Arc<RwLock<String>>,
|
||||||
|
/// Inputs
|
||||||
|
pub inputs: Vec<()>,
|
||||||
|
/// MIDI player/recorder
|
||||||
|
pub player: PhrasePlayer<E>,
|
||||||
|
/// Outputs
|
||||||
|
pub outputs: Vec<()>,
|
||||||
|
}
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Scene {
|
||||||
|
pub name: Arc<RwLock<String>>,
|
||||||
|
pub clips: Vec<Option<usize>>,
|
||||||
|
}
|
||||||
|
#[derive(PartialEq, Clone, Copy)]
|
||||||
|
/// Represents the current user selection in the arranger
|
||||||
|
pub enum ArrangementFocus {
|
||||||
|
/** 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),
|
||||||
|
}
|
||||||
|
/// Display mode of arranger
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub enum ArrangementViewMode {
|
||||||
|
Horizontal,
|
||||||
|
Vertical(usize),
|
||||||
|
}
|
||||||
|
/// A collection of phrases to play on each track.
|
||||||
|
pub struct VerticalArranger<'a, E: Engine>(
|
||||||
|
pub &'a Arrangement<E>, pub usize
|
||||||
|
);
|
||||||
|
pub struct VerticalArrangerGrid<'a>(
|
||||||
|
pub u16, pub &'a [(usize, usize)], pub &'a [(usize, usize)]
|
||||||
|
);
|
||||||
|
pub struct VerticalArrangerCursor<'a>(
|
||||||
|
pub bool, pub ArrangementFocus, pub u16, pub &'a [(usize, usize)], pub &'a [(usize, usize)],
|
||||||
|
);
|
||||||
|
pub struct HorizontalArranger<'a, E: Engine>(
|
||||||
|
pub &'a Arrangement<E>
|
||||||
|
);
|
||||||
|
pub struct ArrangerRenameModal<E: Engine> {
|
||||||
|
_engine: std::marker::PhantomData<E>,
|
||||||
|
pub done: bool,
|
||||||
|
pub target: ArrangementFocus,
|
||||||
|
pub value: String,
|
||||||
|
pub result: Arc<RwLock<String>>,
|
||||||
|
pub cursor: usize
|
||||||
|
}
|
||||||
impl<E: Engine> Arrangement<E> {
|
impl<E: Engine> Arrangement<E> {
|
||||||
pub fn new (name: &str, phrases: &Arc<RwLock<PhrasePool<E>>>) -> Self {
|
pub fn new (name: &str, phrases: &Arc<RwLock<PhrasePool<E>>>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: Arc::new(RwLock::new(name.into())),
|
name: Arc::new(RwLock::new(name.into())),
|
||||||
mode: ArrangerViewMode::Vertical(2),
|
mode: ArrangementViewMode::Vertical(2),
|
||||||
selected: ArrangerFocus::Clip(0, 0),
|
selected: ArrangementFocus::Clip(0, 0),
|
||||||
phrases: phrases.clone(),
|
phrases: phrases.clone(),
|
||||||
scenes: vec![],
|
scenes: vec![],
|
||||||
tracks: vec![],
|
tracks: vec![],
|
||||||
|
|
@ -52,25 +103,25 @@ impl<E: Engine> Arrangement<E> {
|
||||||
}
|
}
|
||||||
pub fn activate (&mut self) {
|
pub fn activate (&mut self) {
|
||||||
match self.selected {
|
match self.selected {
|
||||||
ArrangerFocus::Scene(s) => {
|
ArrangementFocus::Scene(s) => {
|
||||||
for (track_index, track) in self.tracks.iter_mut().enumerate() {
|
for (track_index, track) in self.tracks.iter_mut().enumerate() {
|
||||||
track.playing_phrase = self.scenes[s].clips[track_index];
|
track.player.phrase = self.scenes[s].clips[track_index];
|
||||||
track.reset = true;
|
track.player.reset = true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ArrangerFocus::Clip(t, s) => {
|
ArrangementFocus::Clip(t, s) => {
|
||||||
self.tracks[t].playing_phrase = self.scenes[s].clips[t];
|
self.tracks[t].player.phrase = self.scenes[s].clips[t];
|
||||||
self.tracks[t].reset = true;
|
self.tracks[t].player.reset = true;
|
||||||
},
|
},
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn sequencer (&self) -> Option<&Sequencer<E>> {
|
pub fn sequencer (&self) -> Option<&ArrangementTrack<E>> {
|
||||||
self.selected.track()
|
self.selected.track()
|
||||||
.map(|track|self.tracks.get(track))
|
.map(|track|self.tracks.get(track))
|
||||||
.flatten()
|
.flatten()
|
||||||
}
|
}
|
||||||
pub fn sequencer_mut (&mut self) -> Option<&mut Sequencer<E>> {
|
pub fn sequencer_mut (&mut self) -> Option<&mut ArrangementTrack<E>> {
|
||||||
self.selected.track()
|
self.selected.track()
|
||||||
.map(|track|self.tracks.get_mut(track))
|
.map(|track|self.tracks.get_mut(track))
|
||||||
.flatten()
|
.flatten()
|
||||||
|
|
@ -81,7 +132,7 @@ impl<E: Engine> Arrangement<E> {
|
||||||
let scene = self.scenes.get(scene_index);
|
let scene = self.scenes.get(scene_index);
|
||||||
let track = self.tracks.get_mut(track_index);
|
let track = self.tracks.get_mut(track_index);
|
||||||
if let (Some(scene), Some(track)) = (scene, track) {
|
if let (Some(scene), Some(track)) = (scene, track) {
|
||||||
track.viewing_phrase = scene.clips[track_index]
|
track.player.phrase = scene.clips[track_index]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -92,17 +143,17 @@ impl<E: Engine> Arrangement<E> {
|
||||||
pub fn is_last_row (&self) -> bool {
|
pub fn is_last_row (&self) -> bool {
|
||||||
let selected = self.selected;
|
let selected = self.selected;
|
||||||
(self.scenes.len() == 0 && (selected.is_mix() || selected.is_track())) || match selected {
|
(self.scenes.len() == 0 && (selected.is_mix() || selected.is_track())) || match selected {
|
||||||
ArrangerFocus::Scene(s) =>
|
ArrangementFocus::Scene(s) =>
|
||||||
s == self.scenes.len() - 1,
|
s == self.scenes.len() - 1,
|
||||||
ArrangerFocus::Clip(_, s) =>
|
ArrangementFocus::Clip(_, s) =>
|
||||||
s == self.scenes.len() - 1,
|
s == self.scenes.len() - 1,
|
||||||
_ => false
|
_ => false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn track (&self) -> Option<&Sequencer<E>> {
|
pub fn track (&self) -> Option<&PhrasePlayer<E>> {
|
||||||
self.selected.track().map(|t|self.tracks.get(t)).flatten()
|
self.selected.track().map(|t|self.tracks.get(t)).flatten()
|
||||||
}
|
}
|
||||||
pub fn track_mut (&mut self) -> Option<&mut Sequencer<E>> {
|
pub fn track_mut (&mut self) -> Option<&mut PhrasePlayer<E>> {
|
||||||
self.selected.track().map(|t|self.tracks.get_mut(t)).flatten()
|
self.selected.track().map(|t|self.tracks.get_mut(t)).flatten()
|
||||||
}
|
}
|
||||||
pub fn track_next (&mut self) {
|
pub fn track_next (&mut self) {
|
||||||
|
|
@ -111,10 +162,10 @@ impl<E: Engine> Arrangement<E> {
|
||||||
pub fn track_prev (&mut self) {
|
pub fn track_prev (&mut self) {
|
||||||
self.selected.track_prev()
|
self.selected.track_prev()
|
||||||
}
|
}
|
||||||
pub fn track_add (&mut self, name: Option<&str>) -> Usually<&mut Sequencer<E>> {
|
pub fn track_add (&mut self, name: Option<&str>) -> Usually<&mut ArrangementTrack<E>> {
|
||||||
self.tracks.push(name.map_or_else(
|
self.tracks.push(name.map_or_else(
|
||||||
|| Sequencer::new(&self.track_default_name()),
|
|| PhrasePlayer::new(&self.track_default_name()),
|
||||||
|name| Sequencer::new(name),
|
|name| PhrasePlayer::new(name),
|
||||||
));
|
));
|
||||||
let index = self.tracks.len() - 1;
|
let index = self.tracks.len() - 1;
|
||||||
Ok(&mut self.tracks[index])
|
Ok(&mut self.tracks[index])
|
||||||
|
|
@ -214,27 +265,32 @@ impl<E: Engine> Arrangement<E> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl<E: Engine> ArrangementTrack<E> {
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
pub fn longest_name (tracks: &[Self]) -> usize {
|
||||||
|
tracks.iter()
|
||||||
#[derive(PartialEq, Clone, Copy)]
|
.map(|s|s.name.read().unwrap().len())
|
||||||
/// Represents the current user selection in the arranger
|
.fold(0, usize::max)
|
||||||
pub enum ArrangerFocus {
|
}
|
||||||
/** The whole mix is selected */
|
pub fn clip_name_lengths (tracks: &[Self]) -> Vec<(usize, usize)> {
|
||||||
Mix,
|
let mut total = 0;
|
||||||
/// A track is selected.
|
let mut lengths: Vec<(usize, usize)> = tracks.iter().map(|track|{
|
||||||
Track(usize),
|
let len = 4 + track.phrases
|
||||||
/// A scene is selected.
|
.iter()
|
||||||
Scene(usize),
|
.fold(track.name.read().unwrap().len(), |len, phrase|{
|
||||||
/// A clip (track × scene) is selected.
|
len.max(phrase.read().unwrap().name.read().unwrap().len())
|
||||||
Clip(usize, usize),
|
});
|
||||||
|
total = total + len;
|
||||||
|
(len, total - len)
|
||||||
|
}).collect();
|
||||||
|
lengths.push((0, total));
|
||||||
|
lengths
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Focus identification methods
|
/// Focus identification methods
|
||||||
impl ArrangerFocus {
|
impl ArrangementFocus {
|
||||||
pub fn description <E: Engine> (
|
pub fn description <E: Engine> (
|
||||||
&self,
|
&self,
|
||||||
tracks: &Vec<Sequencer<E>>,
|
tracks: &Vec<ArrangementTrack<E>>,
|
||||||
scenes: &Vec<Scene>,
|
scenes: &Vec<Scene>,
|
||||||
) -> String {
|
) -> String {
|
||||||
format!("Selected: {}", match self {
|
format!("Selected: {}", match self {
|
||||||
|
|
@ -342,14 +398,8 @@ impl ArrangerFocus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
/// Display mode of arranger
|
|
||||||
#[derive(PartialEq)]
|
|
||||||
pub enum ArrangerViewMode { Horizontal, Vertical(usize) }
|
|
||||||
/// Arranger display mode can be cycled
|
/// Arranger display mode can be cycled
|
||||||
impl ArrangerViewMode {
|
impl ArrangementViewMode {
|
||||||
/// Cycle arranger display mode
|
/// Cycle arranger display mode
|
||||||
pub fn to_next (&mut self) {
|
pub fn to_next (&mut self) {
|
||||||
*self = match self {
|
*self = match self {
|
||||||
|
|
@ -361,39 +411,8 @@ impl ArrangerViewMode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
pub struct VerticalArranger<'a, E: Engine>(
|
|
||||||
pub &'a Arrangement<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 Arrangement<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> {
|
impl<E: Engine> ArrangerRenameModal<E> {
|
||||||
pub fn new (target: ArrangerFocus, value: &Arc<RwLock<String>>) -> Self {
|
pub fn new (target: ArrangementFocus, value: &Arc<RwLock<String>>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
_engine: Default::default(),
|
_engine: Default::default(),
|
||||||
done: false,
|
done: false,
|
||||||
|
|
@ -404,52 +423,19 @@ impl<E: Engine> ArrangerRenameModal<E> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<E: Engine + Send> Exit for ArrangerRenameModal<E> {
|
impl<E: Engine + Send> Exit for ArrangerRenameModal<E> {
|
||||||
fn exited (&self) -> bool { self.done }
|
fn exited (&self) -> bool { self.done }
|
||||||
fn exit (&mut self) { self.done = true }
|
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 {
|
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 {
|
pub fn new (name: impl AsRef<str>, clips: impl AsRef<[Option<usize>]>) -> Self {
|
||||||
let name = Arc::new(RwLock::new(name.as_ref().into()));
|
Self {
|
||||||
let clips = clips.as_ref().iter().map(|x|x.clone()).collect();
|
name: Arc::new(RwLock::new(name.as_ref().into())),
|
||||||
Self { name, clips, }
|
clips: clips.as_ref().iter().map(|x|x.clone()).collect(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/// Returns the pulse length of the longest phrase in the scene
|
/// Returns the pulse length of the longest phrase in the scene
|
||||||
pub fn pulses <E: Engine> (&self, tracks: &[Sequencer<E>]) -> usize {
|
pub fn pulses <E: Engine> (&self, tracks: &[ArrangementTrack<E>]) -> usize {
|
||||||
self.clips.iter().enumerate()
|
self.clips.iter().enumerate()
|
||||||
.filter_map(|(i, c)|c
|
.filter_map(|(i, c)|c
|
||||||
.map(|c|tracks
|
.map(|c|tracks
|
||||||
|
|
@ -462,14 +448,29 @@ impl Scene {
|
||||||
.fold(0, |a, p|a.max(p.read().unwrap().length))
|
.fold(0, |a, p|a.max(p.read().unwrap().length))
|
||||||
}
|
}
|
||||||
/// Returns true if all phrases in the scene are currently playing
|
/// Returns true if all phrases in the scene are currently playing
|
||||||
pub fn is_playing <E: Engine> (&self, tracks: &[Sequencer<E>]) -> bool {
|
pub fn is_playing <E: Engine> (&self, tracks: &[ArrangementTrack<E>]) -> bool {
|
||||||
self.clips.iter().enumerate()
|
self.clips.iter().enumerate()
|
||||||
.all(|(track_index, phrase_index)|match phrase_index {
|
.all(|(track_index, phrase_index)|match phrase_index {
|
||||||
Some(i) => tracks
|
Some(i) => tracks
|
||||||
.get(track_index)
|
.get(track_index)
|
||||||
.map(|track|track.playing_phrase == Some(*i))
|
.map(|track|track.player.phrase == Some(*i))
|
||||||
.unwrap_or(false),
|
.unwrap_or(false),
|
||||||
None => true
|
None => true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
pub fn ppqs <E: Engine> (tracks: &[ArrangementTrack<E>], scenes: &[Self]) -> Vec<(usize, usize)> {
|
||||||
|
let mut total = 0;
|
||||||
|
let mut scenes: Vec<(usize, usize)> = scenes.iter().map(|scene|{
|
||||||
|
let pulses = scene.pulses(tracks).max(PPQ);
|
||||||
|
total = total + pulses;
|
||||||
|
(pulses, total - pulses)
|
||||||
|
}).collect();
|
||||||
|
scenes.push((0, total));
|
||||||
|
scenes
|
||||||
|
}
|
||||||
|
pub fn longest_name (scenes: &[Self]) -> usize {
|
||||||
|
scenes.iter()
|
||||||
|
.map(|s|s.name.read().unwrap().len())
|
||||||
|
.fold(0, usize::max)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,6 @@ impl ArrangerCli {
|
||||||
transport: self.transport.then_some(transport),
|
transport: self.transport.then_some(transport),
|
||||||
show_sequencer: Some(tek_core::Direction::Down),
|
show_sequencer: Some(tek_core::Direction::Down),
|
||||||
focus: 0,
|
focus: 0,
|
||||||
sequencer_proxy: SequencerProxy(Default::default(), false),
|
|
||||||
modal: None,
|
modal: None,
|
||||||
editor: PhraseEditor::new(),
|
editor: PhraseEditor::new(),
|
||||||
arrangement,
|
arrangement,
|
||||||
|
|
|
||||||
27
crates/tek_sequencer/src/arranger_edn.rs
Normal file
27
crates/tek_sequencer/src/arranger_edn.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
/// The standalone arranger consists of transport, clip grid, and sequencer.
|
/// The standalone arranger consists of transport, clip grid, and sequencer.
|
||||||
impl Content for Arranger<Tui> {
|
impl Content for Arranger<Tui> {
|
||||||
type Engine = Tui;
|
type Engine = Tui;
|
||||||
|
|
@ -8,16 +7,10 @@ impl Content for Arranger<Tui> {
|
||||||
add(&Stack::down(move|add|{
|
add(&Stack::down(move|add|{
|
||||||
add(&self.transport)?;
|
add(&self.transport)?;
|
||||||
let arrangement = &self.arrangement as &dyn Widget<Engine = Tui>;
|
let arrangement = &self.arrangement as &dyn Widget<Engine = Tui>;
|
||||||
if let (Some(direction), Some(sequencer)) = (
|
if let Some(direction) = self.show_sequencer {
|
||||||
self.show_sequencer,
|
let editor = &self.editor as &dyn Widget<Engine = Tui>;
|
||||||
self.arrangement.sequencer(),
|
add(&arrangement.split(direction, 20, self.phrases.clone()
|
||||||
) {
|
.split(direction.ccw(), 20, editor)
|
||||||
let sequencer = sequencer as &dyn Widget<Engine = Tui>;
|
|
||||||
add(&arrangement.split(
|
|
||||||
direction,
|
|
||||||
20,
|
|
||||||
self.phrases.clone()
|
|
||||||
.split(direction.ccw(), 20, sequencer)
|
|
||||||
.min_y(20)).fill_y())
|
.min_y(20)).fill_y())
|
||||||
} else {
|
} else {
|
||||||
add(&self.arrangement)
|
add(&self.arrangement)
|
||||||
|
|
@ -32,7 +25,6 @@ impl Content for Arranger<Tui> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle top-level events in standalone arranger.
|
/// Handle top-level events in standalone arranger.
|
||||||
impl Handle<Tui> for Arranger<Tui> {
|
impl Handle<Tui> for Arranger<Tui> {
|
||||||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||||
|
|
@ -46,13 +38,6 @@ impl Handle<Tui> for Arranger<Tui> {
|
||||||
let focus = self.focus;
|
let focus = self.focus;
|
||||||
let is_first_row = self.arrangement.is_first_row();
|
let is_first_row = self.arrangement.is_first_row();
|
||||||
let is_last_row = self.arrangement.is_last_row();
|
let is_last_row = self.arrangement.is_last_row();
|
||||||
let mut focused_handle = || {
|
|
||||||
if focus == 2 {
|
|
||||||
self.arrangement.sequencer_mut().handle(from)
|
|
||||||
} else {
|
|
||||||
self.focused_mut().handle(from)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
match from.event() {
|
match from.event() {
|
||||||
key!(KeyCode::Char(' ')) => {
|
key!(KeyCode::Char(' ')) => {
|
||||||
if let Some(ref mut transport) = self.transport {
|
if let Some(ref mut transport) = self.transport {
|
||||||
|
|
@ -73,22 +58,23 @@ impl Handle<Tui> for Arranger<Tui> {
|
||||||
} else if focus == 1 && is_last_row {
|
} else if focus == 1 && is_last_row {
|
||||||
self.focus_next();
|
self.focus_next();
|
||||||
} else {
|
} else {
|
||||||
return focused_handle()
|
return
|
||||||
|
return self.focused_mut().handle(from)
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
key!(KeyCode::Up) => {
|
key!(KeyCode::Up) => {
|
||||||
if focus == 1 && is_first_row {
|
if focus == 1 && is_first_row {
|
||||||
self.focus_prev();
|
self.focus_prev();
|
||||||
} else {
|
} else {
|
||||||
return focused_handle()
|
return self.focused_mut().handle(from)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
_ => return focused_handle()
|
_ => return self.focused_mut().handle(from)
|
||||||
}
|
}
|
||||||
Ok(Some(true))
|
Ok(Some(true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Focusable items in standalone arranger.
|
/// Focusable items in standalone arranger.
|
||||||
impl Focus<3, Tui> for Arranger<Tui> {
|
impl Focus<3, Tui> for Arranger<Tui> {
|
||||||
fn focus (&self) -> usize {
|
fn focus (&self) -> usize {
|
||||||
|
|
@ -98,27 +84,23 @@ impl Focus<3, Tui> for Arranger<Tui> {
|
||||||
&mut self.focus
|
&mut self.focus
|
||||||
}
|
}
|
||||||
fn focusable (&self) -> [&dyn Focusable<Tui>;3] {
|
fn focusable (&self) -> [&dyn Focusable<Tui>;3] {
|
||||||
focusables!(self.transport, self.arrangement, self.sequencer_proxy)
|
focusables!(self.transport, self.arrangement, self.editor)
|
||||||
}
|
}
|
||||||
fn focusable_mut (&mut self) -> [&mut dyn Focusable<Tui>;3] {
|
fn focusable_mut (&mut self) -> [&mut dyn Focusable<Tui>;3] {
|
||||||
focusables_mut!(self.transport, self.arrangement, self.sequencer_proxy)
|
focusables_mut!(self.transport, self.arrangement, self.editor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Arranger<Tui> {
|
impl Arranger<Tui> {
|
||||||
pub fn rename_selected (&mut self) {
|
pub fn rename_selected (&mut self) {
|
||||||
self.modal = Some(Box::new(ArrangerRenameModal::new(
|
let Arrangement { selected, ref name, ref tracks, ref scenes, .. } = self.arrangement;
|
||||||
self.arrangement.selected,
|
self.modal = Some(Box::new(ArrangerRenameModal::new(selected, &match selected {
|
||||||
&match self.arrangement.selected {
|
ArrangementFocus::Mix => name.clone(),
|
||||||
ArrangerFocus::Mix => self.arrangement.name.clone(),
|
ArrangementFocus::Track(t) => tracks[t].name.clone(),
|
||||||
ArrangerFocus::Track(t) => self.arrangement.tracks[t].name.clone(),
|
ArrangementFocus::Scene(s) => scenes[s].name.clone(),
|
||||||
ArrangerFocus::Scene(s) => self.arrangement.scenes[s].name.clone(),
|
ArrangementFocus::Clip(t, s) => tracks[t].phrases[s].read().unwrap().name.clone(),
|
||||||
ArrangerFocus::Clip(t, s) => self.arrangement.tracks[t].phrases[s].read().unwrap().name.clone(),
|
})));
|
||||||
}
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Focusable<Tui> for Arrangement<Tui> {
|
impl Focusable<Tui> for Arrangement<Tui> {
|
||||||
fn is_focused (&self) -> bool {
|
fn is_focused (&self) -> bool {
|
||||||
self.focused
|
self.focused
|
||||||
|
|
@ -127,7 +109,6 @@ impl Focusable<Tui> for Arrangement<Tui> {
|
||||||
self.focused = focused
|
self.focused = focused
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Handle<Tui> for Arrangement<Tui> {
|
impl Handle<Tui> for Arrangement<Tui> {
|
||||||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||||
match from.event() {
|
match from.event() {
|
||||||
|
|
@ -138,7 +119,7 @@ impl Handle<Tui> for Arrangement<Tui> {
|
||||||
// cursor_up: move cursor up
|
// cursor_up: move cursor up
|
||||||
key!(KeyCode::Up) => {
|
key!(KeyCode::Up) => {
|
||||||
match self.mode {
|
match self.mode {
|
||||||
ArrangerViewMode::Horizontal => self.track_prev(),
|
ArrangementViewMode::Horizontal => self.track_prev(),
|
||||||
_ => self.scene_prev(),
|
_ => self.scene_prev(),
|
||||||
};
|
};
|
||||||
self.show_phrase();
|
self.show_phrase();
|
||||||
|
|
@ -146,7 +127,7 @@ impl Handle<Tui> for Arrangement<Tui> {
|
||||||
// cursor_down
|
// cursor_down
|
||||||
key!(KeyCode::Down) => {
|
key!(KeyCode::Down) => {
|
||||||
match self.mode {
|
match self.mode {
|
||||||
ArrangerViewMode::Horizontal => self.track_next(),
|
ArrangementViewMode::Horizontal => self.track_next(),
|
||||||
_ => self.scene_next(),
|
_ => self.scene_next(),
|
||||||
};
|
};
|
||||||
self.show_phrase();
|
self.show_phrase();
|
||||||
|
|
@ -154,7 +135,7 @@ impl Handle<Tui> for Arrangement<Tui> {
|
||||||
// cursor left
|
// cursor left
|
||||||
key!(KeyCode::Left) => {
|
key!(KeyCode::Left) => {
|
||||||
match self.mode {
|
match self.mode {
|
||||||
ArrangerViewMode::Horizontal => self.scene_prev(),
|
ArrangementViewMode::Horizontal => self.scene_prev(),
|
||||||
_ => self.track_prev(),
|
_ => self.track_prev(),
|
||||||
};
|
};
|
||||||
self.show_phrase();
|
self.show_phrase();
|
||||||
|
|
@ -162,7 +143,7 @@ impl Handle<Tui> for Arrangement<Tui> {
|
||||||
// cursor right
|
// cursor right
|
||||||
key!(KeyCode::Right) => {
|
key!(KeyCode::Right) => {
|
||||||
match self.mode {
|
match self.mode {
|
||||||
ArrangerViewMode::Horizontal => self.scene_next(),
|
ArrangementViewMode::Horizontal => self.scene_next(),
|
||||||
_ => self.track_next(),
|
_ => self.track_next(),
|
||||||
};
|
};
|
||||||
self.show_phrase();
|
self.show_phrase();
|
||||||
|
|
@ -211,14 +192,13 @@ impl Handle<Tui> for Arrangement<Tui> {
|
||||||
Ok(Some(true))
|
Ok(Some(true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Content for Arrangement<Tui> {
|
impl Content for Arrangement<Tui> {
|
||||||
type Engine = Tui;
|
type Engine = Tui;
|
||||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||||
Layers::new(move |add|{
|
Layers::new(move |add|{
|
||||||
match self.mode {
|
match self.mode {
|
||||||
ArrangerViewMode::Horizontal => add(&HorizontalArranger(&self)),
|
ArrangementViewMode::Horizontal => add(&HorizontalArranger(&self)),
|
||||||
ArrangerViewMode::Vertical(factor) => add(&VerticalArranger(&self, factor))
|
ArrangementViewMode::Vertical(factor) => add(&VerticalArranger(&self, factor))
|
||||||
}?;
|
}?;
|
||||||
add(&Align::SE(self.selected.description(
|
add(&Align::SE(self.selected.description(
|
||||||
&self.tracks,
|
&self.tracks,
|
||||||
|
|
@ -227,24 +207,21 @@ impl Content for Arrangement<Tui> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
impl<'a> Content for VerticalArranger<'a, Tui> {
|
impl<'a> Content for VerticalArranger<'a, Tui> {
|
||||||
type Engine = Tui;
|
type Engine = Tui;
|
||||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||||
let Self(state, factor) = self;
|
let Self(state, factor) = self;
|
||||||
let (cols, rows) = if *factor == 0 {(
|
let (cols, rows) = if *factor == 0 {(
|
||||||
track_clip_name_lengths(state.tracks.as_slice()),
|
ArrangementTrack::clip_name_lengths(state.tracks.as_slice()),
|
||||||
scene_ppqs(state.tracks.as_slice(), state.scenes.as_slice()),
|
Scene::ppqs(state.tracks.as_slice(), state.scenes.as_slice()),
|
||||||
)} else {(
|
)} else {(
|
||||||
track_clip_name_lengths(state.tracks.as_slice()),
|
ArrangementTrack::clip_name_lengths(state.tracks.as_slice()),
|
||||||
(0..=state.scenes.len()).map(|i|(factor*PPQ, factor*PPQ*i)).collect::<Vec<_>>(),
|
(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 height = rows.last().map(|(w,y)|(y+w)/PPQ).unwrap_or(16);
|
||||||
let tracks: &[Sequencer<Tui>] = state.tracks.as_ref();
|
let tracks: &[ArrangementTrack<Tui>] = state.tracks.as_ref();
|
||||||
let scenes: &[Scene] = state.scenes.as_ref();
|
let scenes: &[Scene] = state.scenes.as_ref();
|
||||||
let offset = 4 + scene_name_max_len(scenes) as u16;
|
let offset = 4 + Scene::longest_name(scenes) as u16;
|
||||||
Layers::new(move |add|{
|
Layers::new(move |add|{
|
||||||
let rows: &[(usize, usize)] = rows.as_ref();
|
let rows: &[(usize, usize)] = rows.as_ref();
|
||||||
let cols: &[(usize, usize)] = cols.as_ref();
|
let cols: &[(usize, usize)] = cols.as_ref();
|
||||||
|
|
@ -267,7 +244,7 @@ impl<'a> Content for VerticalArranger<'a, Tui> {
|
||||||
let name = name.read().unwrap();
|
let name = name.read().unwrap();
|
||||||
let name = format!("{clip:02} {}", name);
|
let name = format!("{clip:02} {}", name);
|
||||||
add(&name.as_str().push_x(1).fixed_x(w))?;
|
add(&name.as_str().push_x(1).fixed_x(w))?;
|
||||||
if (track as &Sequencer<_>).playing_phrase == Some(*clip) {
|
if (track as &PhrasePlayer<_>).phrase == Some(*clip) {
|
||||||
color = COLOR_PLAYING
|
color = COLOR_PLAYING
|
||||||
} else {
|
} else {
|
||||||
color = COLOR_BG1
|
color = COLOR_BG1
|
||||||
|
|
@ -303,39 +280,6 @@ impl<'a> Content for VerticalArranger<'a, Tui> {
|
||||||
.fg(Color::Rgb(70, 80, 50))))
|
.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(PPQ);
|
|
||||||
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> {
|
impl<'a> Widget for VerticalArrangerGrid<'a> {
|
||||||
type Engine = Tui;
|
type Engine = Tui;
|
||||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||||
|
|
@ -364,7 +308,6 @@ impl<'a> Widget for VerticalArrangerGrid<'a> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Widget for VerticalArrangerCursor<'a> {
|
impl<'a> Widget for VerticalArrangerCursor<'a> {
|
||||||
type Engine = Tui;
|
type Engine = Tui;
|
||||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||||
|
|
@ -392,21 +335,21 @@ impl<'a> Widget for VerticalArrangerCursor<'a> {
|
||||||
let mut scene_area: Option<[u16;4]> = None;
|
let mut scene_area: Option<[u16;4]> = None;
|
||||||
let mut clip_area: Option<[u16;4]> = None;
|
let mut clip_area: Option<[u16;4]> = None;
|
||||||
let area = match selected {
|
let area = match selected {
|
||||||
ArrangerFocus::Mix => {
|
ArrangementFocus::Mix => {
|
||||||
if focused {
|
if focused {
|
||||||
to.fill_bg(area, Color::Rgb(40, 50, 30));
|
to.fill_bg(area, Color::Rgb(40, 50, 30));
|
||||||
}
|
}
|
||||||
area
|
area
|
||||||
},
|
},
|
||||||
ArrangerFocus::Track(t) => {
|
ArrangementFocus::Track(t) => {
|
||||||
track_area = Some(get_track_area(t));
|
track_area = Some(get_track_area(t));
|
||||||
area
|
area
|
||||||
},
|
},
|
||||||
ArrangerFocus::Scene(s) => {
|
ArrangementFocus::Scene(s) => {
|
||||||
scene_area = Some(get_scene_area(s));
|
scene_area = Some(get_scene_area(s));
|
||||||
area
|
area
|
||||||
},
|
},
|
||||||
ArrangerFocus::Clip(t, s) => {
|
ArrangementFocus::Clip(t, s) => {
|
||||||
track_area = Some(get_track_area(t));
|
track_area = Some(get_track_area(t));
|
||||||
scene_area = Some(get_scene_area(s));
|
scene_area = Some(get_scene_area(s));
|
||||||
clip_area = Some(get_clip_area(t, s));
|
clip_area = Some(get_clip_area(t, s));
|
||||||
|
|
@ -436,9 +379,6 @@ impl<'a> Widget for VerticalArrangerCursor<'a> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
impl<'a> Content for HorizontalArranger<'a, Tui> {
|
impl<'a> Content for HorizontalArranger<'a, Tui> {
|
||||||
type Engine = Tui;
|
type Engine = Tui;
|
||||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||||
|
|
@ -653,15 +593,6 @@ impl<'a> Content for HorizontalArranger<'a, Tui> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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> {
|
impl Content for ArrangerRenameModal<Tui> {
|
||||||
type Engine = Tui;
|
type Engine = Tui;
|
||||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||||
|
|
@ -673,10 +604,10 @@ impl Content for ArrangerRenameModal<Tui> {
|
||||||
//to.fill_bg(bg_area, COLOR_BG1);
|
//to.fill_bg(bg_area, COLOR_BG1);
|
||||||
//Lozenge(Style::default().bold().white().dim()).draw(to.with_rect(bg_area));
|
//Lozenge(Style::default().bold().white().dim()).draw(to.with_rect(bg_area));
|
||||||
//let label = match self.target {
|
//let label = match self.target {
|
||||||
//ArrangerFocus::Mix => "Rename project:",
|
//ArrangementFocus::Mix => "Rename project:",
|
||||||
//ArrangerFocus::Track(_) => "Rename track:",
|
//ArrangementFocus::Track(_) => "Rename track:",
|
||||||
//ArrangerFocus::Scene(_) => "Rename scene:",
|
//ArrangementFocus::Scene(_) => "Rename scene:",
|
||||||
//ArrangerFocus::Clip(_, _) => "Rename clip:",
|
//ArrangementFocus::Clip(_, _) => "Rename clip:",
|
||||||
//};
|
//};
|
||||||
//let style = Some(Style::default().not_bold().white().not_dim());
|
//let style = Some(Style::default().not_bold().white().not_dim());
|
||||||
//to.blit(&label, area.x() + 3, y, style);
|
//to.blit(&label, area.x() + 3, y, style);
|
||||||
|
|
@ -688,7 +619,6 @@ impl Content for ArrangerRenameModal<Tui> {
|
||||||
//Ok(())
|
//Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Handle<Tui> for ArrangerRenameModal<Tui> {
|
impl Handle<Tui> for ArrangerRenameModal<Tui> {
|
||||||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||||
match from.event() {
|
match from.event() {
|
||||||
|
|
|
||||||
|
|
@ -1,53 +1,33 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
/// MIDI message structural
|
||||||
/// A collection of MIDI messages.
|
|
||||||
pub type PhraseData = Vec<Vec<MidiMessage>>;
|
pub type PhraseData = Vec<Vec<MidiMessage>>;
|
||||||
|
/// MIDI message serialized
|
||||||
/// MIDI message serialized to bytes
|
pub type PhraseMessage = Vec<u8>;
|
||||||
pub type MIDIMessage = Vec<u8>;
|
|
||||||
|
|
||||||
/// Collection of serialized MIDI messages
|
/// Collection of serialized MIDI messages
|
||||||
pub type MIDIChunk = [Vec<MIDIMessage>];
|
pub type PhraseChunk = [Vec<PhraseMessage>];
|
||||||
|
/// Root level object for standalone `tek_sequencer`
|
||||||
/// Contains all phrases in the project
|
pub struct Sequencer<E: Engine> {
|
||||||
|
/// Controls the JACK transport.
|
||||||
|
pub transport: Option<Arc<RwLock<TransportToolbar<E>>>>,
|
||||||
|
/// Pool of all phrases available to the sequencer
|
||||||
|
pub phrases: Arc<RwLock<PhrasePool<E>>>,
|
||||||
|
/// Phrase editor view
|
||||||
|
pub editor: PhraseEditor<E>,
|
||||||
|
}
|
||||||
|
/// Contains all phrases in a project
|
||||||
pub struct PhrasePool<E: Engine> {
|
pub struct PhrasePool<E: Engine> {
|
||||||
_engine: PhantomData<E>,
|
_engine: PhantomData<E>,
|
||||||
|
/// Phrases in the pool
|
||||||
pub phrases: Vec<Arc<RwLock<Option<Phrase>>>>,
|
pub phrases: Vec<Arc<RwLock<Option<Phrase>>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<E: Engine> PhrasePool<E> {
|
|
||||||
pub fn new () -> Self {
|
|
||||||
Self {
|
|
||||||
_engine: Default::default(),
|
|
||||||
phrases: vec![Arc::new(RwLock::new(Some(Phrase::default())))]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Contains state for viewing and editing a phrase
|
|
||||||
pub struct PhraseEditor<E: Engine> {
|
|
||||||
_engine: PhantomData<E>,
|
|
||||||
pub phrase: Arc<RwLock<Option<Phrase>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E: Engine> PhraseEditor<E> {
|
|
||||||
pub fn new () -> Self {
|
|
||||||
Self {
|
|
||||||
_engine: Default::default(),
|
|
||||||
phrase: Arc::new(RwLock::new(None)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn show (&mut self, phrase: &Arc<RwLock<Option<Phrase>>>) {
|
|
||||||
self.phrase = phrase.clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A MIDI sequence.
|
/// A MIDI sequence.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Phrase {
|
pub struct Phrase {
|
||||||
/// Name of phrase
|
/// Name of phrase
|
||||||
pub name: Arc<RwLock<String>>,
|
pub name: Arc<RwLock<String>>,
|
||||||
/// Length of phrase
|
/// Temporal resolution in pulses per quarter note
|
||||||
|
pub ppq: usize,
|
||||||
|
/// Length of phrase in pulses
|
||||||
pub length: usize,
|
pub length: usize,
|
||||||
/// Notes in phrase
|
/// Notes in phrase
|
||||||
pub notes: PhraseData,
|
pub notes: PhraseData,
|
||||||
|
|
@ -60,15 +40,89 @@ pub struct Phrase {
|
||||||
/// All notes are displayed with minimum length
|
/// All notes are displayed with minimum length
|
||||||
pub percussive: bool,
|
pub percussive: bool,
|
||||||
}
|
}
|
||||||
|
/// Contains state for viewing and editing a phrase
|
||||||
|
pub struct PhraseEditor<E: Engine> {
|
||||||
|
_engine: PhantomData<E>,
|
||||||
|
/// Phrase being played
|
||||||
|
pub phrase: Option<Arc<RwLock<Phrase>>>,
|
||||||
|
/// The full piano keys are rendered to this buffer
|
||||||
|
pub keys: Buffer,
|
||||||
|
/// The full piano roll is rendered to this buffer
|
||||||
|
pub buffer: BigBuffer,
|
||||||
|
/// Cursor/scroll/zoom in pitch axis
|
||||||
|
pub note_axis: FixedAxis<usize>,
|
||||||
|
/// Cursor/scroll/zoom in time axis
|
||||||
|
pub time_axis: ScaledAxis<usize>,
|
||||||
|
/// Whether this widget is focused
|
||||||
|
pub focused: bool,
|
||||||
|
/// Whether note enter mode is enabled
|
||||||
|
pub entered: bool,
|
||||||
|
/// Display mode
|
||||||
|
pub mode: bool,
|
||||||
|
/// Notes currently held at input
|
||||||
|
pub notes_in: Arc<RwLock<[bool; 128]>>,
|
||||||
|
/// Notes currently held at output
|
||||||
|
pub notes_out: Arc<RwLock<[bool; 128]>>,
|
||||||
|
}
|
||||||
|
/// Phrase player.
|
||||||
|
pub struct PhrasePlayer<E: Engine> {
|
||||||
|
_engine: PhantomData<E>,
|
||||||
|
/// Phrase being played
|
||||||
|
pub phrase: Option<Arc<RwLock<Phrase>>>,
|
||||||
|
/// Notes currently held at input
|
||||||
|
pub notes_in: Arc<RwLock<[bool; 128]>>,
|
||||||
|
/// Notes currently held at output
|
||||||
|
pub notes_out: Arc<RwLock<[bool; 128]>>,
|
||||||
|
/// Current point in playing phrase
|
||||||
|
pub now: usize,
|
||||||
|
/// Play input through output.
|
||||||
|
pub monitoring: bool,
|
||||||
|
/// Write input to sequence.
|
||||||
|
pub recording: bool,
|
||||||
|
/// Overdub input to sequence.
|
||||||
|
pub overdub: bool,
|
||||||
|
/// Output from current sequence.
|
||||||
|
pub midi_out: Option<Port<MidiOut>>,
|
||||||
|
/// MIDI output buffer
|
||||||
|
midi_out_buf: Vec<Vec<Vec<u8>>>,
|
||||||
|
/// Send all notes off
|
||||||
|
pub reset: bool, // TODO?: after Some(nframes)
|
||||||
|
}
|
||||||
|
impl<E: Engine> PhrasePool<E> {
|
||||||
|
pub fn new () -> Self {
|
||||||
|
Self {
|
||||||
|
_engine: Default::default(),
|
||||||
|
phrases: vec![Arc::new(RwLock::new(Some(Phrase::default())))]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<E: Engine> PhraseEditor<E> {
|
||||||
|
pub fn new () -> Self {
|
||||||
|
Self {
|
||||||
|
_engine: Default::default(),
|
||||||
|
phrase: None,
|
||||||
|
notes_in: Arc::new(RwLock::new([false;128])),
|
||||||
|
notes_out: Arc::new(RwLock::new([false;128])),
|
||||||
|
keys: keys_vert(),
|
||||||
|
buffer: Default::default(),
|
||||||
|
note_axis: FixedAxis { start: 12, point: Some(36) },
|
||||||
|
time_axis: ScaledAxis { start: 0, scale: 24, point: Some(0) },
|
||||||
|
focused: false,
|
||||||
|
entered: false,
|
||||||
|
mode: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
impl Default for Phrase {
|
impl Default for Phrase {
|
||||||
fn default () -> Self { Self::new("", false, 0, None) }
|
fn default () -> Self { Self::new("", false, 0, None) }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Phrase {
|
impl Phrase {
|
||||||
pub fn new (name: &str, loop_on: bool, length: usize, notes: Option<PhraseData>) -> Self {
|
pub fn new (
|
||||||
|
name: &str, loop_on: bool, length: usize, notes: Option<PhraseData>
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: Arc::new(RwLock::new(name.into())),
|
name: Arc::new(RwLock::new(name.into())),
|
||||||
|
ppq: PPQ,
|
||||||
length,
|
length,
|
||||||
notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]),
|
notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]),
|
||||||
loop_on,
|
loop_on,
|
||||||
|
|
@ -106,7 +160,7 @@ impl Phrase {
|
||||||
/// Write a chunk of MIDI events to an output port.
|
/// Write a chunk of MIDI events to an output port.
|
||||||
pub fn process_out (
|
pub fn process_out (
|
||||||
&self,
|
&self,
|
||||||
output: &mut MIDIChunk,
|
output: &mut PhraseChunk,
|
||||||
notes_on: &mut [bool;128],
|
notes_on: &mut [bool;128],
|
||||||
timebase: &Arc<Timebase>,
|
timebase: &Arc<Timebase>,
|
||||||
(frame0, frames, _): (usize, usize, f64),
|
(frame0, frames, _): (usize, usize, f64),
|
||||||
|
|
@ -130,136 +184,21 @@ impl Phrase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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")) {
|
impl<E: Engine> PhrasePlayer<E> {
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Phrase player.
|
|
||||||
pub struct Sequencer<E: Engine> {
|
|
||||||
pub name: Arc<RwLock<String>>,
|
|
||||||
pub mode: bool,
|
|
||||||
pub focused: bool,
|
|
||||||
pub entered: bool,
|
|
||||||
pub transport: Option<Arc<RwLock<TransportToolbar<E>>>>,
|
|
||||||
/// The full piano roll is rendered to this buffer
|
|
||||||
pub buffer: BigBuffer,
|
|
||||||
/// The full piano keys is rendered to this buffer
|
|
||||||
pub keys: Buffer,
|
|
||||||
/// Highlight input keys
|
|
||||||
pub keys_in: [bool; 128],
|
|
||||||
/// Highlight output keys
|
|
||||||
pub keys_out: [bool; 128],
|
|
||||||
/// Current point in playing phrase
|
|
||||||
pub now: usize,
|
|
||||||
/// Temporal resolution (default 96)
|
|
||||||
pub ppq: usize,
|
|
||||||
/// Scroll/room in pitch axis
|
|
||||||
pub note_axis: FixedAxis<usize>,
|
|
||||||
/// Scroll/room in time axis
|
|
||||||
pub time_axis: ScaledAxis<usize>,
|
|
||||||
/// Play input through output.
|
|
||||||
pub monitoring: bool,
|
|
||||||
/// Write input to sequence.
|
|
||||||
pub recording: bool,
|
|
||||||
/// Overdub input to sequence.
|
|
||||||
pub overdub: bool,
|
|
||||||
/// Map: tick -> MIDI events at tick
|
|
||||||
pub phrases: Vec<Arc<RwLock<Phrase>>>,
|
|
||||||
/// Phrase currently being played
|
|
||||||
pub playing_phrase: Option<usize>,
|
|
||||||
/// Phrase currently being viewed
|
|
||||||
pub viewing_phrase: Option<usize>,
|
|
||||||
/// Output from current sequence.
|
|
||||||
pub midi_out: Option<Port<MidiOut>>,
|
|
||||||
/// MIDI output buffer
|
|
||||||
midi_out_buf: Vec<Vec<Vec<u8>>>,
|
|
||||||
/// Send all notes off
|
|
||||||
pub reset: bool, // TODO?: after Some(nframes)
|
|
||||||
/// Highlight keys on piano roll.
|
|
||||||
pub notes_in: [bool;128],
|
|
||||||
/// Highlight keys on piano roll.
|
|
||||||
pub notes_out: [bool;128],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E: Engine> Sequencer<E> {
|
|
||||||
pub fn new (name: &str) -> Self {
|
pub fn new (name: &str) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: Arc::new(RwLock::new(name.into())),
|
_engine: Default::default(),
|
||||||
|
phrase: None,
|
||||||
|
notes_in: Arc::new(RwLock::new([false;128])),
|
||||||
|
notes_out: Arc::new(RwLock::new([false;128])),
|
||||||
monitoring: false,
|
monitoring: false,
|
||||||
recording: false,
|
recording: false,
|
||||||
overdub: true,
|
overdub: true,
|
||||||
phrases: vec![],
|
|
||||||
viewing_phrase: None,
|
|
||||||
playing_phrase: None,
|
|
||||||
midi_out: None,
|
midi_out: None,
|
||||||
midi_out_buf: vec![Vec::with_capacity(16);16384],
|
midi_out_buf: vec![Vec::with_capacity(16);16384],
|
||||||
reset: true,
|
reset: true,
|
||||||
notes_in: [false;128],
|
|
||||||
notes_out: [false;128],
|
|
||||||
buffer: Default::default(),
|
|
||||||
keys: keys_vert(),
|
|
||||||
entered: false,
|
|
||||||
focused: false,
|
|
||||||
mode: false,
|
|
||||||
keys_in: [false;128],
|
|
||||||
keys_out: [false;128],
|
|
||||||
now: 0,
|
now: 0,
|
||||||
ppq: PPQ,
|
|
||||||
transport: None,
|
|
||||||
note_axis: FixedAxis { start: 12, point: Some(36) },
|
|
||||||
time_axis: ScaledAxis { start: 0, scale: 24, point: Some(0) },
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn toggle_monitor (&mut self) {
|
pub fn toggle_monitor (&mut self) {
|
||||||
|
|
@ -298,15 +237,15 @@ impl<E: Engine> Sequencer<E> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let (
|
if let (
|
||||||
Some(TransportState::Rolling), Some((start_frame, _)), Some(phrase)
|
Some(TransportState::Rolling),
|
||||||
) = (
|
Some((start_frame, _)),
|
||||||
playing, started, self.playing_phrase.and_then(|id|self.phrases.get_mut(id))
|
Some(ref phrase)
|
||||||
) {
|
) = (playing, started, &self.phrase) {
|
||||||
phrase.read().map(|phrase|{
|
phrase.read().map(|phrase|{
|
||||||
if self.midi_out.is_some() {
|
if self.midi_out.is_some() {
|
||||||
phrase.process_out(
|
phrase.process_out(
|
||||||
&mut self.midi_out_buf,
|
&mut self.midi_out_buf,
|
||||||
&mut self.notes_out,
|
&mut self.notes_out.write().unwrap(),
|
||||||
timebase,
|
timebase,
|
||||||
(frame0.saturating_sub(start_frame), frames, period)
|
(frame0.saturating_sub(start_frame), frames, period)
|
||||||
);
|
);
|
||||||
|
|
@ -317,6 +256,7 @@ impl<E: Engine> Sequencer<E> {
|
||||||
// Monitor and record input
|
// Monitor and record input
|
||||||
if input.is_some() && (self.recording || self.monitoring) {
|
if input.is_some() && (self.recording || self.monitoring) {
|
||||||
// For highlighting keys and note repeat
|
// For highlighting keys and note repeat
|
||||||
|
let mut notes_in = self.notes_in.write().unwrap();
|
||||||
for (frame, event, bytes) in parse_midi_input(input.unwrap()) {
|
for (frame, event, bytes) in parse_midi_input(input.unwrap()) {
|
||||||
match event {
|
match event {
|
||||||
LiveEvent::Midi { message, .. } => {
|
LiveEvent::Midi { message, .. } => {
|
||||||
|
|
@ -337,10 +277,10 @@ impl<E: Engine> Sequencer<E> {
|
||||||
}
|
}
|
||||||
match message {
|
match message {
|
||||||
MidiMessage::NoteOn { key, .. } => {
|
MidiMessage::NoteOn { key, .. } => {
|
||||||
self.notes_in[key.as_int() as usize] = true;
|
notes_in[key.as_int() as usize] = true;
|
||||||
}
|
}
|
||||||
MidiMessage::NoteOff { key, .. } => {
|
MidiMessage::NoteOff { key, .. } => {
|
||||||
self.notes_in[key.as_int() as usize] = false;
|
notes_in[key.as_int() as usize] = false;
|
||||||
},
|
},
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
@ -350,50 +290,45 @@ impl<E: Engine> Sequencer<E> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if input.is_some() && self.midi_out.is_some() && self.monitoring {
|
} else if input.is_some() && self.midi_out.is_some() && self.monitoring {
|
||||||
|
let mut notes_in = self.notes_in.write().unwrap();
|
||||||
for (frame, event, bytes) in parse_midi_input(input.unwrap()) {
|
for (frame, event, bytes) in parse_midi_input(input.unwrap()) {
|
||||||
self.process_monitor_event(frame, &event, bytes)
|
match event {
|
||||||
|
LiveEvent::Midi { message, .. } => {
|
||||||
|
self.midi_out_buf[frame].push(bytes.to_vec());
|
||||||
|
match message {
|
||||||
|
MidiMessage::NoteOn { key, .. } => {
|
||||||
|
notes_in[key.as_int() as usize] = true;
|
||||||
|
}
|
||||||
|
MidiMessage::NoteOff { key, .. } => {
|
||||||
|
notes_in[key.as_int() as usize] = false;
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(out) = &mut self.midi_out {
|
if let Some(out) = &mut self.midi_out {
|
||||||
write_midi_output(&mut out.writer(scope), &self.midi_out_buf, frames);
|
let writer = &mut out.writer(scope);
|
||||||
}
|
let output = &self.midi_out_buf;
|
||||||
}
|
for time in 0..frames {
|
||||||
#[inline]
|
for event in output[time].iter() {
|
||||||
fn process_monitor_event (&mut self, frame: usize, event: &LiveEvent, bytes: &[u8]) {
|
writer.write(&RawMidi { time: time as u32, bytes: &event })
|
||||||
match event {
|
.expect(&format!("{event:?}"));
|
||||||
LiveEvent::Midi { message, .. } => {
|
}
|
||||||
self.write_to_output_buffer(frame, bytes);
|
}
|
||||||
self.process_monitor_message(&message);
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[inline] fn write_to_output_buffer (&mut self, frame: usize, bytes: &[u8]) {
|
|
||||||
self.midi_out_buf[frame].push(bytes.to_vec());
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
fn process_monitor_message (&mut self, message: &MidiMessage) {
|
|
||||||
match message {
|
|
||||||
MidiMessage::NoteOn { key, .. } => {
|
|
||||||
self.notes_in[key.as_int() as usize] = true;
|
|
||||||
}
|
|
||||||
MidiMessage::NoteOff { key, .. } => {
|
|
||||||
self.notes_in[key.as_int() as usize] = false;
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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 PhraseChunk) {
|
||||||
let mut buf = vec![];
|
let mut buf = vec![];
|
||||||
let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() };
|
let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() };
|
||||||
let evt = LiveEvent::Midi { channel: 0.into(), message: msg };
|
let evt = LiveEvent::Midi { channel: 0.into(), message: msg };
|
||||||
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 }|(
|
||||||
|
|
@ -402,30 +337,3 @@ 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)
|
|
||||||
pub fn write_midi_output (writer: &mut MidiWriter, output: &MIDIChunk, frames: usize) {
|
|
||||||
for time in 0..frames {
|
|
||||||
for event in output[time].iter() {
|
|
||||||
writer.write(&RawMidi { time: time as u32, bytes: &event })
|
|
||||||
.expect(&format!("{event:?}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct SequencerProxy<E: Engine>(pub PhantomData<E>, pub bool);
|
|
||||||
|
|
||||||
impl Handle<Tui> for SequencerProxy<Tui> {
|
|
||||||
fn handle (&mut self, _: &TuiInput) -> Perhaps<bool> { unreachable!() }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Content for SequencerProxy<Tui> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn content (&self) -> impl Widget<Engine = Tui> { "" }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Focusable<Tui> for SequencerProxy<Tui> {
|
|
||||||
fn is_focused (&self) -> bool { self.1 }
|
|
||||||
fn set_focused (&mut self, focus: bool) { self.1 = focus }
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -18,22 +18,24 @@ pub struct SequencerCli {
|
||||||
|
|
||||||
impl SequencerCli {
|
impl SequencerCli {
|
||||||
fn run (&self) -> Usually<()> {
|
fn run (&self) -> Usually<()> {
|
||||||
let mut seq = Sequencer::new("");
|
let seq = Sequencer {
|
||||||
|
transport: self.transport.unwrap_or(false)
|
||||||
|
.then_some(Arc::new(RwLock::new(TransportToolbar::new(None)))),
|
||||||
|
};
|
||||||
if let Some(name) = self.name.as_ref() {
|
if let Some(name) = self.name.as_ref() {
|
||||||
seq.name = Arc::new(RwLock::new(name.clone()));
|
// TODO
|
||||||
|
//seq.name = Arc::new(RwLock::new(name.clone()));
|
||||||
}
|
}
|
||||||
if let Some(ppq) = self.ppq {
|
if let Some(ppq) = self.ppq {
|
||||||
seq.ppq = ppq;
|
// TODO
|
||||||
|
//seq.ppq = ppq;
|
||||||
}
|
}
|
||||||
if let Some(length) = self.length {
|
if let Some(length) = self.length {
|
||||||
// TODO FIXME WTF
|
// TODO
|
||||||
//if let Some(phrase) = seq.phrase.as_mut() {
|
//if let Some(phrase) = seq.phrase.as_mut() {
|
||||||
//phrase.write().unwrap().length = length;
|
//phrase.write().unwrap().length = length;
|
||||||
//}
|
//}
|
||||||
}
|
}
|
||||||
if self.transport == Some(true) {
|
|
||||||
seq.transport = Some(Arc::new(RwLock::new(TransportToolbar::new(None))));
|
|
||||||
}
|
|
||||||
Tui::run(Arc::new(RwLock::new(seq))).map(|_|())
|
Tui::run(Arc::new(RwLock::new(seq))).map(|_|())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
0
crates/tek_sequencer/src/sequencer_edn.rs
Normal file
0
crates/tek_sequencer/src/sequencer_edn.rs
Normal file
|
|
@ -1,5 +1,4 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
impl Content for PhrasePool<Tui> {
|
impl Content for PhrasePool<Tui> {
|
||||||
type Engine = Tui;
|
type Engine = Tui;
|
||||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||||
|
|
@ -14,21 +13,154 @@ impl Content for PhrasePool<Tui> {
|
||||||
.fg(Color::Rgb(70, 80, 50))))
|
.fg(Color::Rgb(70, 80, 50))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl Content for PhraseEditor<Tui> {
|
||||||
impl Sequencer<Tui> {
|
type Engine = Tui;
|
||||||
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||||
|
let field_bg = Color::Rgb(28, 35, 25);
|
||||||
|
let toolbar = Stack::down(move|add|{
|
||||||
|
//let name = format!("{:>9}", self.name.read().unwrap().as_str());
|
||||||
|
//add(&col!("Track:", TuiStyle::bg(name.as_str(), field_bg)))?;
|
||||||
|
if let Some(phrase) = self.phrase {
|
||||||
|
let phrase = phrase.read().unwrap();
|
||||||
|
let length = format!("{}q{}p", phrase.length / PPQ, phrase.length % PPQ);
|
||||||
|
let length = format!("{:>9}", &length);
|
||||||
|
let loop_on = format!("{:>9}", if phrase.loop_on { "on" } else { "off" });
|
||||||
|
let loop_start = format!("{:>9}", phrase.loop_start);
|
||||||
|
let loop_end = format!("{:>9}", phrase.loop_length);
|
||||||
|
add(&"")?;
|
||||||
|
add(&col!("Length:", TuiStyle::bg(length.as_str(), field_bg)))?;
|
||||||
|
add(&col!("Loop:", TuiStyle::bg(loop_on.as_str(), field_bg)))?;
|
||||||
|
add(&col!("L. start:", TuiStyle::bg(loop_start.as_str(), field_bg)))?;
|
||||||
|
add(&col!("L. length:", TuiStyle::bg(loop_end.as_str(), field_bg)))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}).min_x(10);
|
||||||
|
let content = lay!(
|
||||||
|
// keys
|
||||||
|
CustomWidget::new(|_|Ok(Some([32,4])), |to: &mut TuiOutput|{
|
||||||
|
if to.area().h() < 2 { return Ok(()) }
|
||||||
|
Ok(to.buffer_update(to.area().set_w(5).shrink_y(2), &|cell, x, y|{
|
||||||
|
let y = y + self.note_axis.start as u16;
|
||||||
|
if x < self.keys.area.width && y < self.keys.area.height {
|
||||||
|
*cell = self.keys.get(x, y).clone()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}).fill_y(),
|
||||||
|
// playhead
|
||||||
|
CustomWidget::new(|_|Ok(Some([32,2])), |to: &mut TuiOutput|{
|
||||||
|
if let Some(phrase) = self.phrase {
|
||||||
|
let time_0 = self.time_axis.start;
|
||||||
|
let time_z = self.time_axis.scale;
|
||||||
|
let now = self.now % phrase.read().unwrap().length;
|
||||||
|
let [x, y, width, _] = to.area();
|
||||||
|
let x2 = x as usize + Self::H_KEYS_OFFSET;
|
||||||
|
let x3 = x as usize + width as usize;
|
||||||
|
for x in x2..x3 {
|
||||||
|
let step = (time_0 + x2) * time_z;
|
||||||
|
let next_step = (time_0 + x2 + 1) * time_z;
|
||||||
|
to.blit(&"-", x as u16, y, Some(PhraseEditor::<Tui>::style_timer_step(
|
||||||
|
now, step as usize, next_step as usize
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}).fill_x(),
|
||||||
|
// notes
|
||||||
|
CustomWidget::new(|_|Ok(Some([32,4])), |to: &mut TuiOutput|{
|
||||||
|
let offset = Self::H_KEYS_OFFSET as u16;
|
||||||
|
if to.area().h() < 2 || to.area().w() < offset { return Ok(()) }
|
||||||
|
let area = to.area().push_x(offset).shrink_x(offset).shrink_y(2);
|
||||||
|
Ok(to.buffer_update(area, &move |cell, x, y|{
|
||||||
|
cell.set_bg(Color::Rgb(20, 20, 20));
|
||||||
|
let src_x = ((x as usize + self.time_axis.start) * self.time_axis.scale) as usize;
|
||||||
|
let src_y = (y as usize + self.note_axis.start) as usize;
|
||||||
|
if src_x < self.buffer.width && src_y < self.buffer.height - 1 {
|
||||||
|
let src = self.buffer.get(src_x, self.buffer.height - src_y);
|
||||||
|
src.map(|src|{ cell.set_symbol(src.symbol()); cell.set_fg(src.fg); });
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}).fill_x(),
|
||||||
|
// note cursor
|
||||||
|
CustomWidget::new(|_|Ok(Some([1,1])), |to: &mut TuiOutput|{
|
||||||
|
let area = to.area();
|
||||||
|
if let (Some(time), Some(note)) = (self.time_axis.point, self.note_axis.point) {
|
||||||
|
let x = area.x() + Self::H_KEYS_OFFSET as u16 + time as u16;
|
||||||
|
let y = area.y() + 1 + note as u16 / 2;
|
||||||
|
let c = if note % 2 == 0 { "▀" } else { "▄" };
|
||||||
|
to.blit(&c, x, y, self.style_focus());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}),
|
||||||
|
// zoom
|
||||||
|
CustomWidget::new(|_|Ok(Some([10,1])), |to: &mut TuiOutput|{
|
||||||
|
let [x, y, w, h] = to.area.xywh();
|
||||||
|
let quant = ppq_to_name(self.time_axis.scale);
|
||||||
|
Ok(to.blit(
|
||||||
|
&quant,
|
||||||
|
x + w - 1 - quant.len() as u16,
|
||||||
|
y + h - 2,
|
||||||
|
self.style_focus()
|
||||||
|
))
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
row!(toolbar, content).fill_x()
|
||||||
|
.bg(Color::Rgb(40, 50, 30))
|
||||||
|
.border(Lozenge(Style::default()
|
||||||
|
.bg(Color::Rgb(40, 50, 30))
|
||||||
|
.fg(Color::Rgb(70, 80, 50))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Handle<Tui> for PhraseEditor<Tui> {
|
||||||
|
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||||
|
match from.event() {
|
||||||
|
key!(KeyCode::Char('`')) => {
|
||||||
|
self.mode = !self.mode;
|
||||||
|
},
|
||||||
|
key!(KeyCode::Up) => match self.entered {
|
||||||
|
true => { self.note_axis.point_dec(); },
|
||||||
|
false => { self.note_axis.start_dec(); },
|
||||||
|
},
|
||||||
|
key!(KeyCode::Down) => match self.entered {
|
||||||
|
true => { self.note_axis.point_inc(); },
|
||||||
|
false => { self.note_axis.start_inc(); },
|
||||||
|
},
|
||||||
|
key!(KeyCode::Left) => match self.entered {
|
||||||
|
true => { self.time_axis.point_dec(); },
|
||||||
|
false => { self.time_axis.start_dec(); },
|
||||||
|
},
|
||||||
|
key!(KeyCode::Right) => match self.entered {
|
||||||
|
true => { self.time_axis.point_inc(); },
|
||||||
|
false => { self.time_axis.start_inc(); },
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
return Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Ok(Some(true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Focusable<Tui> for PhraseEditor<Tui> {
|
||||||
|
fn is_focused (&self) -> bool {
|
||||||
|
self.focused
|
||||||
|
}
|
||||||
|
fn set_focused (&mut self, focused: bool) {
|
||||||
|
self.focused = focused
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl PhraseEditor<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.
|
||||||
pub fn show (&mut self, index: Option<usize>) {
|
pub fn show (&mut self, phrase: Option<&Arc<RwLock<Phrase>>>) {
|
||||||
self.viewing_phrase = index;
|
if let Some(phrase) = phrase {
|
||||||
if let Some(phrase) = index.map(|index|self.phrases.get(index)).flatten() {
|
self.phrase = Some(phrase.clone());
|
||||||
let width = usize::MAX.min(phrase.read().unwrap().length);
|
let width = usize::MAX.min(phrase.read().unwrap().length);
|
||||||
let mut buffer = BigBuffer::new(width, 64);
|
let mut buffer = BigBuffer::new(width, 64);
|
||||||
let phrase = phrase.read().unwrap();
|
let phrase = phrase.read().unwrap();
|
||||||
Self::fill_seq_bg(&mut buffer, phrase.length, self.ppq);
|
Self::fill_seq_bg(&mut buffer, phrase.length, phrase.ppq);
|
||||||
Self::fill_seq_fg(&mut buffer, &phrase);
|
Self::fill_seq_fg(&mut buffer, &phrase);
|
||||||
self.buffer = buffer;
|
self.buffer = buffer;
|
||||||
} else {
|
} else {
|
||||||
self.viewing_phrase = None;
|
self.phrase = None;
|
||||||
self.buffer = Default::default();
|
self.buffer = Default::default();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -121,130 +253,18 @@ impl Sequencer<Tui> {
|
||||||
}
|
}
|
||||||
pub fn index_to_color (&self, index: u16, default: Color) -> Color {
|
pub fn index_to_color (&self, index: u16, default: Color) -> Color {
|
||||||
let index = index as usize;
|
let index = index as usize;
|
||||||
if self.keys_in[index] && self.keys_out[index] {
|
let (notes_in, notes_out) = (self.notes_in.read().unwrap(), self.notes_out.read().unwrap());
|
||||||
|
if notes_in[index] && notes_out[index] {
|
||||||
Color::Yellow
|
Color::Yellow
|
||||||
} else if self.keys_in[index] {
|
} else if notes_in[index] {
|
||||||
Color::Red
|
Color::Red
|
||||||
} else if self.keys_out[index] {
|
} else if notes_out[index] {
|
||||||
Color::Green
|
Color::Green
|
||||||
} else {
|
} else {
|
||||||
default
|
default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Content for Sequencer<Tui> {
|
|
||||||
type Engine = Tui;
|
|
||||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
|
||||||
let field_bg = Color::Rgb(28, 35, 25);
|
|
||||||
let toolbar = Stack::down(move|add|{
|
|
||||||
let name = format!("{:>9}", self.name.read().unwrap().as_str());
|
|
||||||
add(&col!("Track:", TuiStyle::bg(name.as_str(), field_bg)))?;
|
|
||||||
if let Some(phrase) = self.viewing_phrase
|
|
||||||
.map(|index|self.phrases.get(index))
|
|
||||||
.flatten()
|
|
||||||
{
|
|
||||||
let phrase = phrase.read().unwrap();
|
|
||||||
let length = format!("{}q{}p", phrase.length / PPQ, phrase.length % PPQ);
|
|
||||||
let length = format!("{:>9}", &length);
|
|
||||||
let loop_on = format!("{:>9}", if phrase.loop_on { "on" } else { "off" });
|
|
||||||
let loop_start = format!("{:>9}", phrase.loop_start);
|
|
||||||
let loop_end = format!("{:>9}", phrase.loop_length);
|
|
||||||
add(&"")?;
|
|
||||||
add(&col!("Length:", TuiStyle::bg(length.as_str(), field_bg)))?;
|
|
||||||
add(&col!("Loop:", TuiStyle::bg(loop_on.as_str(), field_bg)))?;
|
|
||||||
add(&col!("L. start:", TuiStyle::bg(loop_start.as_str(), field_bg)))?;
|
|
||||||
add(&col!("L. length:", TuiStyle::bg(loop_end.as_str(), field_bg)))?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}).min_x(10);
|
|
||||||
let content = lay!(
|
|
||||||
// keys
|
|
||||||
CustomWidget::new(|_|Ok(Some([32,4])), |to: &mut TuiOutput|{
|
|
||||||
if to.area().h() < 2 { return Ok(()) }
|
|
||||||
Ok(to.buffer_update(to.area().set_w(5).shrink_y(2), &|cell, x, y|{
|
|
||||||
let y = y + self.note_axis.start as u16;
|
|
||||||
if x < self.keys.area.width && y < self.keys.area.height {
|
|
||||||
*cell = self.keys.get(x, y).clone()
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}).fill_y(),
|
|
||||||
// playhead
|
|
||||||
CustomWidget::new(|_|Ok(Some([32,2])), |to: &mut TuiOutput|{
|
|
||||||
if let Some(phrase) = self.viewing_phrase
|
|
||||||
.map(|index|self.phrases.get(index))
|
|
||||||
.flatten()
|
|
||||||
{
|
|
||||||
let time_0 = self.time_axis.start;
|
|
||||||
let time_z = self.time_axis.scale;
|
|
||||||
let now = self.now % phrase.read().unwrap().length;
|
|
||||||
let [x, y, width, _] = to.area();
|
|
||||||
let x2 = x as usize + Sequencer::H_KEYS_OFFSET;
|
|
||||||
let x3 = x as usize + width as usize;
|
|
||||||
for x in x2..x3 {
|
|
||||||
let step = (time_0 + x2) * time_z;
|
|
||||||
let next_step = (time_0 + x2 + 1) * time_z;
|
|
||||||
to.blit(&"-", x as u16, y, Some(Sequencer::<Tui>::style_timer_step(
|
|
||||||
now, step as usize, next_step as usize
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}).fill_x(),
|
|
||||||
// notes
|
|
||||||
CustomWidget::new(|_|Ok(Some([32,4])), |to: &mut TuiOutput|{
|
|
||||||
let offset = Sequencer::H_KEYS_OFFSET as u16;
|
|
||||||
if to.area().h() < 2 || to.area().w() < offset { return Ok(()) }
|
|
||||||
let area = to.area().push_x(offset).shrink_x(offset).shrink_y(2);
|
|
||||||
Ok(to.buffer_update(area, &move |cell, x, y|{
|
|
||||||
cell.set_bg(Color::Rgb(20, 20, 20));
|
|
||||||
let src_x = ((x as usize + self.time_axis.start) * self.time_axis.scale) as usize;
|
|
||||||
let src_y = (y as usize + self.note_axis.start) as usize;
|
|
||||||
if src_x < self.buffer.width && src_y < self.buffer.height - 1 {
|
|
||||||
let src = self.buffer.get(src_x, self.buffer.height - src_y);
|
|
||||||
src.map(|src|{ cell.set_symbol(src.symbol()); cell.set_fg(src.fg); });
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}).fill_x(),
|
|
||||||
// note cursor
|
|
||||||
CustomWidget::new(|_|Ok(Some([1,1])), |to: &mut TuiOutput|{
|
|
||||||
let area = to.area();
|
|
||||||
if let (Some(time), Some(note)) = (self.time_axis.point, self.note_axis.point) {
|
|
||||||
let x = area.x() + Sequencer::H_KEYS_OFFSET as u16 + time as u16;
|
|
||||||
let y = area.y() + 1 + note as u16 / 2;
|
|
||||||
let c = if note % 2 == 0 { "▀" } else { "▄" };
|
|
||||||
to.blit(&c, x, y, self.style_focus());
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}),
|
|
||||||
// zoom
|
|
||||||
CustomWidget::new(|_|Ok(Some([10,1])), |to: &mut TuiOutput|{
|
|
||||||
let [x, y, w, h] = to.area.xywh();
|
|
||||||
let quant = ppq_to_name(self.time_axis.scale);
|
|
||||||
Ok(to.blit(
|
|
||||||
&quant,
|
|
||||||
x + w - 1 - quant.len() as u16,
|
|
||||||
y + h - 2,
|
|
||||||
self.style_focus()
|
|
||||||
))
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
row!(toolbar, content).fill_x()
|
|
||||||
.bg(Color::Rgb(40, 50, 30))
|
|
||||||
.border(Lozenge(Style::default()
|
|
||||||
.bg(Color::Rgb(40, 50, 30))
|
|
||||||
.fg(Color::Rgb(70, 80, 50))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Focusable<Tui> for Sequencer<Tui> {
|
|
||||||
fn is_focused (&self) -> bool {
|
|
||||||
self.focused
|
|
||||||
}
|
|
||||||
fn set_focused (&mut self, focused: bool) {
|
|
||||||
self.focused = focused
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Colors of piano keys
|
/// Colors of piano keys
|
||||||
const KEY_COLORS: [(Color, Color);6] = [
|
const KEY_COLORS: [(Color, Color);6] = [
|
||||||
(Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)),
|
(Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)),
|
||||||
|
|
@ -254,7 +274,6 @@ const KEY_COLORS: [(Color, Color);6] = [
|
||||||
(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
|
(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
|
||||||
(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
|
(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
|
||||||
];
|
];
|
||||||
|
|
||||||
pub(crate) fn keys_vert () -> Buffer {
|
pub(crate) fn keys_vert () -> Buffer {
|
||||||
let area = [0, 0, 5, 64];
|
let area = [0, 0, 5, 64];
|
||||||
let mut buffer = Buffer::empty(Rect {
|
let mut buffer = Buffer::empty(Rect {
|
||||||
|
|
@ -281,38 +300,7 @@ pub(crate) fn keys_vert () -> Buffer {
|
||||||
});
|
});
|
||||||
buffer
|
buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Octave number (from -1 to 9)
|
/// Octave number (from -1 to 9)
|
||||||
const NTH_OCTAVE: [&'static str;11] = [
|
const NTH_OCTAVE: [&'static str;11] = [
|
||||||
"-1", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"
|
"-1", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"
|
||||||
];
|
];
|
||||||
|
|
||||||
impl Handle<Tui> for Sequencer<Tui> {
|
|
||||||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
|
||||||
match from.event() {
|
|
||||||
key!(KeyCode::Char('`')) => {
|
|
||||||
self.mode = !self.mode;
|
|
||||||
},
|
|
||||||
key!(KeyCode::Up) => match self.entered {
|
|
||||||
true => { self.note_axis.point_dec(); },
|
|
||||||
false => { self.note_axis.start_dec(); },
|
|
||||||
},
|
|
||||||
key!(KeyCode::Down) => match self.entered {
|
|
||||||
true => { self.note_axis.point_inc(); },
|
|
||||||
false => { self.note_axis.start_inc(); },
|
|
||||||
},
|
|
||||||
key!(KeyCode::Left) => match self.entered {
|
|
||||||
true => { self.time_axis.point_dec(); },
|
|
||||||
false => { self.time_axis.start_dec(); },
|
|
||||||
},
|
|
||||||
key!(KeyCode::Right) => match self.entered {
|
|
||||||
true => { self.time_axis.point_inc(); },
|
|
||||||
false => { self.time_axis.start_inc(); },
|
|
||||||
},
|
|
||||||
_ => {
|
|
||||||
return Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Ok(Some(true))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue