start porting some sequencer keybinds

This commit is contained in:
🪞👃🪞 2024-12-18 18:36:20 +01:00
parent 8472805142
commit de77daf565
6 changed files with 248 additions and 223 deletions

View file

@ -7,8 +7,6 @@ pub(crate) mod midi_out; pub(crate) use midi_out::*;
pub(crate) mod midi_phrase; pub(crate) use midi_phrase::*;
pub(crate) mod midi_play; pub(crate) use midi_play::*;
pub(crate) mod midi_rec; pub(crate) use midi_rec::*;
pub(crate) mod midi_scene; pub(crate) use midi_scene::*;
pub(crate) mod midi_track; pub(crate) use midi_track::*;
/// Add "all notes off" to the start of a buffer.
pub fn all_notes_off (output: &mut [Vec<Vec<u8>>]) {

View file

@ -1,96 +0,0 @@
use crate::*;
pub trait HasScenes<S: ArrangerSceneApi> {
fn scenes (&self) -> &Vec<S>;
fn scenes_mut (&mut self) -> &mut Vec<S>;
fn scene_add (&mut self, name: Option<&str>, color: Option<ItemPalette>) -> Usually<&mut S>;
fn scene_del (&mut self, index: usize) {
self.scenes_mut().remove(index);
}
fn scene_default_name (&self) -> String {
format!("Scene {}", self.scenes().len() + 1)
}
fn selected_scene (&self) -> Option<&S> {
None
}
fn selected_scene_mut (&mut self) -> Option<&mut S> {
None
}
}
#[derive(Clone, Debug)]
pub enum ArrangerSceneCommand {
Add,
Delete(usize),
RandomColor,
Play(usize),
Swap(usize, usize),
SetSize(usize),
SetZoom(usize),
}
//impl<T: ArrangerApi> Command<T> for ArrangerSceneCommand {
//fn execute (self, state: &mut T) -> Perhaps<Self> {
//match self {
//Self::Delete(index) => { state.scene_del(index); },
//_ => todo!()
//}
//Ok(None)
//}
//}
pub trait ArrangerSceneApi: Sized {
fn name (&self) -> &Arc<RwLock<String>>;
fn clips (&self) -> &Vec<Option<Arc<RwLock<Phrase>>>>;
fn color (&self) -> ItemPalette;
fn ppqs (scenes: &[Self], factor: usize) -> Vec<(usize, usize)> {
let mut total = 0;
if factor == 0 {
scenes.iter().map(|scene|{
let pulses = scene.pulses().max(PPQ);
total = total + pulses;
(pulses, total - pulses)
}).collect()
} else {
(0..=scenes.len()).map(|i|{
(factor*PPQ, factor*PPQ*i)
}).collect()
}
}
fn longest_name (scenes: &[Self]) -> usize {
scenes.iter().map(|s|s.name().read().unwrap().len()).fold(0, usize::max)
}
/// Returns the pulse length of the longest phrase in the scene
fn pulses (&self) -> usize {
self.clips().iter().fold(0, |a, p|{
a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0))
})
}
/// Returns true if all phrases in the scene are
/// currently playing on the given collection of tracks.
fn is_playing <T: ArrangerTrackApi> (&self, tracks: &[T]) -> bool {
self.clips().iter().any(|clip|clip.is_some()) && self.clips().iter().enumerate()
.all(|(track_index, clip)|match clip {
Some(clip) => tracks
.get(track_index)
.map(|track|{
if let Some((_, Some(phrase))) = track.player().play_phrase() {
*phrase.read().unwrap() == *clip.read().unwrap()
} else {
false
}
})
.unwrap_or(false),
None => true
})
}
fn clip (&self, index: usize) -> Option<&Arc<RwLock<Phrase>>> {
match self.clips().get(index) { Some(Some(clip)) => Some(clip), _ => None }
}
}

View file

@ -1,87 +0,0 @@
use crate::*;
pub trait HasTracks<T: ArrangerTrackApi>: Send + Sync {
fn tracks (&self) -> &Vec<T>;
fn tracks_mut (&mut self) -> &mut Vec<T>;
}
impl<T: ArrangerTrackApi> HasTracks<T> for Vec<T> {
fn tracks (&self) -> &Vec<T> {
self
}
fn tracks_mut (&mut self) -> &mut Vec<T> {
self
}
}
pub trait ArrangerTracksApi<T: ArrangerTrackApi>: HasTracks<T> {
fn track_add (&mut self, name: Option<&str>, color: Option<ItemPalette>)-> Usually<&mut T>;
fn track_del (&mut self, index: usize);
fn track_default_name (&self) -> String {
format!("Track {}", self.tracks().len() + 1)
}
}
#[derive(Clone, Debug)]
pub enum ArrangerTrackCommand {
Add,
Delete(usize),
RandomColor,
Stop,
Swap(usize, usize),
SetSize(usize),
SetZoom(usize),
}
pub trait ArrangerTrackApi: HasPlayer + Send + Sync + Sized {
/// Name of track
fn name (&self) -> &Arc<RwLock<String>>;
/// Preferred width of track column
fn width (&self) -> usize;
/// Preferred width of track column
fn width_mut (&mut self) -> &mut usize;
/// Identifying color of track
fn color (&self) -> ItemPalette;
fn longest_name (tracks: &[Self]) -> usize {
tracks.iter().map(|s|s.name().read().unwrap().len()).fold(0, usize::max)
}
const MIN_WIDTH: usize = 3;
fn width_inc (&mut self) {
*self.width_mut() += 1;
}
fn width_dec (&mut self) {
if self.width() > Self::MIN_WIDTH {
*self.width_mut() -= 1;
}
}
}
/// Hosts the JACK callback for a collection of tracks
pub struct TracksAudio<'a, T: ArrangerTrackApi, H: HasTracks<T>>(
// Track collection
pub &'a mut H,
/// Note buffer
pub &'a mut Vec<u8>,
/// Note chunk buffer
pub &'a mut Vec<Vec<Vec<u8>>>,
/// Marker
pub PhantomData<T>,
);
impl<'a, T: ArrangerTrackApi, H: HasTracks<T>> Audio for TracksAudio<'a, T, H> {
#[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
let model = &mut self.0;
let note_buffer = &mut self.1;
let output_buffer = &mut self.2;
for track in model.tracks_mut().iter_mut() {
if PlayerAudio(track.player_mut(), note_buffer, output_buffer).process(client, scope) == Control::Quit {
return Control::Quit
}
}
Control::Continue
}
}

View file

@ -1,4 +1,6 @@
use crate::*;
use ClockCommand::{Play, Pause};
use KeyCode::{Char, Down, Right, Delete};
/// Root view for standalone `tek_arranger`
pub struct ArrangerTui {
pub jack: Arc<RwLock<JackClient>>,
@ -42,10 +44,6 @@ from_jack!(|jack| ArrangerTui Self {
note_buf: vec![],
perf: PerfModel::default(),
});
has_clock!(|self: ArrangerTui|&self.clock);
has_phrases!(|self: ArrangerTui|self.phrases.phrases);
has_editor!(|self: ArrangerTui|self.editor);
handle!(<Tui>|self: ArrangerTui, input|ArrangerCommand::execute_with_state(self, input));
render!(<Tui>|self: ArrangerTui|{
let arranger = ||lay!(|add|{
let color = self.color;
@ -112,10 +110,15 @@ audio!(|self: ArrangerTui, client, scope|{
self.perf.update(t0, scope);
return Control::Continue
});
has_clock!(|self: ArrangerTui|&self.clock);
has_phrases!(|self: ArrangerTui|self.phrases.phrases);
has_editor!(|self: ArrangerTui|self.editor);
handle!(<Tui>|self: ArrangerTui, input|ArrangerCommand::execute_with_state(self, input));
#[derive(Clone, Debug)] pub enum ArrangerCommand {
Undo,
Redo,
Clear,
StopAll,
Color(ItemPalette),
Clock(ClockCommand),
Scene(ArrangerSceneCommand),
@ -126,35 +129,60 @@ audio!(|self: ArrangerTui, client, scope|{
Phrases(PhrasesCommand),
Editor(PhraseCommand),
}
input_to_command!(ArrangerCommand: <Tui>|state:ArrangerTui,input|to_arranger_command(state, input)?);
fn to_arranger_command (state: &ArrangerTui, input: &TuiInput) -> Option<ArrangerCommand> {
use ArrangerSelection::*;
use ArrangerCommand as Cmd;
use KeyCode::Char;
input_to_command!(ArrangerCommand: <Tui>|state:ArrangerTui,input|{
use ArrangerSelection as Selected;
use ArrangerCommand::*;
// WSAD navigation, Q launches, E edits, PgUp/Down pool, Arrows editor
Some(match input.event() {
key_pat!(Char('e')) => Cmd::Editor(PhraseCommand::Show(Some(state.phrases.phrase().clone()))),
key_pat!(Char('l')) => Cmd::Clip(ArrangerClipCommand::SetLoop(false)),
key_pat!(Char('+')) => Cmd::Zoom(0), // TODO
key_pat!(Char('=')) => Cmd::Zoom(0), // TODO
key_pat!(Char('_')) => Cmd::Zoom(0), // TODO
key_pat!(Char('-')) => Cmd::Zoom(0), // TODO
key_pat!(Char('`')) => { todo!("toggle state mode") },
key_pat!(Ctrl-Char('a')) => Cmd::Scene(ArrangerSceneCommand::Add),
key_pat!(Ctrl-Char('t')) => Cmd::Track(ArrangerTrackCommand::Add),
match input.event() {
// Transport: Play/pause
key_pat!(Char(' ')) =>
Clock(if state.clock().is_stopped() { Play(None) } else { Pause(None) }),
// Transport: Play from start or rewind to start
key_pat!(Shift-Char(' ')) =>
Clock(if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }),
// TODO: u: undo
key_pat!(Char('u')) =>
{ todo!("undo") },
// TODO: Shift-U: redo
key_pat!(Char('U')) =>
{ todo!("redo") },
// TODO: k: toggle on-screen keyboard
key_pat!(Ctrl-Char('k')) =>
{ todo!("keyboard") },
key_pat!(Char('e')) =>
Editor(PhraseCommand::Show(Some(state.phrases.phrase().clone()))),
key_pat!(Char('l')) =>
Clip(ArrangerClipCommand::SetLoop(false)),
key_pat!(Ctrl-Char('a')) =>
Scene(ArrangerSceneCommand::Add),
key_pat!(Ctrl-Char('t')) =>
Track(ArrangerTrackCommand::Add),
key_pat!(Char('0')) => match state.selected() {
Selected::Mix => StopAll,
Selected::Track(t) => return None,
Selected::Scene(s) => return None,
Selected::Clip(t, s) => return None,
},
key_pat!(Char('q')) => match state.selected() {
Selected::Mix => return None,
Selected::Track(t) => return None,
Selected::Scene(s) => return None,
Selected::Clip(t, s) => return None,
},
_ => match state.selected() {
Mix => to_arranger_mix_command(input)?,
Track(t) => to_arranger_track_command(input, t)?,
Scene(s) => to_arranger_scene_command(input, s)?,
Clip(t, s) => to_arranger_clip_command(input, t, s)?,
Selected::Mix => to_arranger_mix_command(input)?,
Selected::Track(t) => to_arranger_track_command(input, t)?,
Selected::Scene(s) => to_arranger_scene_command(input, s)?,
Selected::Clip(t, s) => to_arranger_clip_command(input, t, s)?,
}
})
}
}
});
fn to_arranger_mix_command (input: &TuiInput) -> Option<ArrangerCommand> {
use KeyCode::{Char, Down, Right, Delete};
use ArrangerCommand as Cmd;
use ArrangerSelection as Select;
Some(match input.event() {
// 0: Enqueue phrase 0 (stop all)
key_pat!(Char('0')) => Cmd::StopAll,
key_pat!(Char('s')) => Cmd::Select(Select::Scene(0)),
key_pat!(Char('d')) => Cmd::Select(Select::Track(0)),
key_pat!(Char(',')) => Cmd::Zoom(0),

View file

@ -1,4 +1,99 @@
use crate::*;
pub trait HasScenes<S: ArrangerSceneApi> {
fn scenes (&self) -> &Vec<S>;
fn scenes_mut (&mut self) -> &mut Vec<S>;
fn scene_add (&mut self, name: Option<&str>, color: Option<ItemPalette>) -> Usually<&mut S>;
fn scene_del (&mut self, index: usize) {
self.scenes_mut().remove(index);
}
fn scene_default_name (&self) -> String {
format!("Scene {}", self.scenes().len() + 1)
}
fn selected_scene (&self) -> Option<&S> {
None
}
fn selected_scene_mut (&mut self) -> Option<&mut S> {
None
}
}
#[derive(Clone, Debug)]
pub enum ArrangerSceneCommand {
Add,
Delete(usize),
RandomColor,
Play(usize),
Swap(usize, usize),
SetSize(usize),
SetZoom(usize),
}
//impl<T: ArrangerApi> Command<T> for ArrangerSceneCommand {
//fn execute (self, state: &mut T) -> Perhaps<Self> {
//match self {
//Self::Delete(index) => { state.scene_del(index); },
//_ => todo!()
//}
//Ok(None)
//}
//}
pub trait ArrangerSceneApi: Sized {
fn name (&self) -> &Arc<RwLock<String>>;
fn clips (&self) -> &Vec<Option<Arc<RwLock<Phrase>>>>;
fn color (&self) -> ItemPalette;
fn ppqs (scenes: &[Self], factor: usize) -> Vec<(usize, usize)> {
let mut total = 0;
if factor == 0 {
scenes.iter().map(|scene|{
let pulses = scene.pulses().max(PPQ);
total = total + pulses;
(pulses, total - pulses)
}).collect()
} else {
(0..=scenes.len()).map(|i|{
(factor*PPQ, factor*PPQ*i)
}).collect()
}
}
fn longest_name (scenes: &[Self]) -> usize {
scenes.iter().map(|s|s.name().read().unwrap().len()).fold(0, usize::max)
}
/// Returns the pulse length of the longest phrase in the scene
fn pulses (&self) -> usize {
self.clips().iter().fold(0, |a, p|{
a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0))
})
}
/// Returns true if all phrases in the scene are
/// currently playing on the given collection of tracks.
fn is_playing <T: ArrangerTrackApi> (&self, tracks: &[T]) -> bool {
self.clips().iter().any(|clip|clip.is_some()) && self.clips().iter().enumerate()
.all(|(track_index, clip)|match clip {
Some(clip) => tracks
.get(track_index)
.map(|track|{
if let Some((_, Some(phrase))) = track.player().play_phrase() {
*phrase.read().unwrap() == *clip.read().unwrap()
} else {
false
}
})
.unwrap_or(false),
None => true
})
}
fn clip (&self, index: usize) -> Option<&Arc<RwLock<Phrase>>> {
match self.clips().get(index) { Some(Some(clip)) => Some(clip), _ => None }
}
}
pub fn to_arranger_scene_command (input: &TuiInput, s: usize) -> Option<ArrangerCommand> {
use KeyCode::{Char, Up, Down, Right, Enter, Delete};
use ArrangerCommand as Cmd;
@ -18,6 +113,7 @@ pub fn to_arranger_scene_command (input: &TuiInput, s: usize) -> Option<Arranger
_ => return None
})
}
impl HasScenes<ArrangerScene> for ArrangerTui {
fn scenes (&self) -> &Vec<ArrangerScene> {
&self.scenes

View file

@ -1,18 +1,104 @@
use crate::*;
use KeyCode::{Char, Down, Left, Right, Delete};
pub trait HasTracks<T: ArrangerTrackApi>: Send + Sync {
fn tracks (&self) -> &Vec<T>;
fn tracks_mut (&mut self) -> &mut Vec<T>;
}
impl<T: ArrangerTrackApi> HasTracks<T> for Vec<T> {
fn tracks (&self) -> &Vec<T> {
self
}
fn tracks_mut (&mut self) -> &mut Vec<T> {
self
}
}
pub trait ArrangerTracksApi<T: ArrangerTrackApi>: HasTracks<T> {
fn track_add (&mut self, name: Option<&str>, color: Option<ItemPalette>)-> Usually<&mut T>;
fn track_del (&mut self, index: usize);
fn track_default_name (&self) -> String {
format!("Track {}", self.tracks().len() + 1)
}
}
#[derive(Clone, Debug)]
pub enum ArrangerTrackCommand {
Add,
Delete(usize),
RandomColor,
Stop,
Swap(usize, usize),
SetSize(usize),
SetZoom(usize),
}
pub trait ArrangerTrackApi: HasPlayer + Send + Sync + Sized {
/// Name of track
fn name (&self) -> &Arc<RwLock<String>>;
/// Preferred width of track column
fn width (&self) -> usize;
/// Preferred width of track column
fn width_mut (&mut self) -> &mut usize;
/// Identifying color of track
fn color (&self) -> ItemPalette;
fn longest_name (tracks: &[Self]) -> usize {
tracks.iter().map(|s|s.name().read().unwrap().len()).fold(0, usize::max)
}
const MIN_WIDTH: usize = 3;
fn width_inc (&mut self) {
*self.width_mut() += 1;
}
fn width_dec (&mut self) {
if self.width() > Self::MIN_WIDTH {
*self.width_mut() -= 1;
}
}
}
/// Hosts the JACK callback for a collection of tracks
pub struct TracksAudio<'a, T: ArrangerTrackApi, H: HasTracks<T>>(
// Track collection
pub &'a mut H,
/// Note buffer
pub &'a mut Vec<u8>,
/// Note chunk buffer
pub &'a mut Vec<Vec<Vec<u8>>>,
/// Marker
pub PhantomData<T>,
);
impl<'a, T: ArrangerTrackApi, H: HasTracks<T>> Audio for TracksAudio<'a, T, H> {
#[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
let model = &mut self.0;
let note_buffer = &mut self.1;
let output_buffer = &mut self.2;
for track in model.tracks_mut().iter_mut() {
if PlayerAudio(track.player_mut(), note_buffer, output_buffer).process(client, scope) == Control::Quit {
return Control::Quit
}
}
Control::Continue
}
}
pub fn to_arranger_track_command (input: &TuiInput, t: usize) -> Option<ArrangerCommand> {
use KeyCode::{Char, Down, Left, Right, Delete};
use ArrangerCommand as Cmd;
use ArrangerSelection as Select;
use ArrangerTrackCommand as Track;
use ArrangerCommand::*;
use ArrangerSelection as Selected;
use ArrangerTrackCommand as Tracks;
Some(match input.event() {
key_pat!(Char('s')) => Cmd::Select(Select::Clip(t, 0)),
key_pat!(Char('a')) => Cmd::Select(if t > 0 { Select::Track(t - 1) } else { Select::Mix }),
key_pat!(Char('d')) => Cmd::Select(Select::Track(t + 1)),
key_pat!(Char(',')) => Cmd::Track(Track::Swap(t, t - 1)),
key_pat!(Char('.')) => Cmd::Track(Track::Swap(t, t + 1)),
key_pat!(Char('<')) => Cmd::Track(Track::Swap(t, t - 1)),
key_pat!(Char('>')) => Cmd::Track(Track::Swap(t, t + 1)),
key_pat!(Delete) => Cmd::Track(Track::Delete(t)),
key_pat!(Char('s')) => Select(Selected::Clip(t, 0)),
key_pat!(Char('a')) => Select(if t > 0 { Selected::Track(t - 1) } else { Selected::Mix }),
key_pat!(Char('d')) => Select(Selected::Track(t + 1)),
key_pat!(Char(',')) => Track(Tracks::Swap(t, t - 1)),
key_pat!(Char('.')) => Track(Tracks::Swap(t, t + 1)),
key_pat!(Char('<')) => Track(Tracks::Swap(t, t - 1)),
key_pat!(Char('>')) => Track(Tracks::Swap(t, t + 1)),
key_pat!(Delete) => Track(Tracks::Delete(t)),
//key_pat!(Char('c')) => Cmd::Track(Track::Color(t, ItemPalette::random())),
_ => return None
})