mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 19:56:42 +01:00
midi: modularize
This commit is contained in:
parent
39dc6b803e
commit
13444dc59a
29 changed files with 872 additions and 750 deletions
|
|
@ -36,6 +36,9 @@ pub fn handle_arranger (app: &mut Tek, input: &TuiIn) -> Perhaps<bool> {
|
|||
}
|
||||
|
||||
pub fn handle_sequencer (app: &mut Tek, input: &TuiIn) -> Perhaps<bool> {
|
||||
if app.editor.handle(input)? == Some(true) {
|
||||
return Ok(Some(true))
|
||||
}
|
||||
if let Some(command) = SourceIter(include_str!("../edn/sequencer_keys.edn"))
|
||||
.command::<_, TekCommand, _>(app, input)
|
||||
{
|
||||
|
|
@ -48,6 +51,9 @@ pub fn handle_sequencer (app: &mut Tek, input: &TuiIn) -> Perhaps<bool> {
|
|||
}
|
||||
|
||||
pub fn handle_groovebox (app: &mut Tek, input: &TuiIn) -> Perhaps<bool> {
|
||||
if app.editor.handle(input)? == Some(true) {
|
||||
return Ok(Some(true))
|
||||
}
|
||||
if let Some(command) = SourceIter(include_str!("../edn/groovebox_keys.edn"))
|
||||
.command::<_, TekCommand, _>(app, input)
|
||||
{
|
||||
|
|
|
|||
86
crates/midi/midi.scratch.rs
Normal file
86
crates/midi/midi.scratch.rs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//fn to_clips_command (state: &MidiPool, input: &Event) -> Option<PoolCommand> {
|
||||
//use KeyCode::{Up, Down, Delete, Char};
|
||||
//use PoolCommand as Cmd;
|
||||
//let index = state.clip_index();
|
||||
//let count = state.clips().len();
|
||||
//Some(match input {
|
||||
//kpat!(Char('n')) => Cmd::Rename(ClipRenameCommand::Begin),
|
||||
//kpat!(Char('t')) => Cmd::Length(ClipLengthCommand::Begin),
|
||||
//kpat!(Char('m')) => Cmd::Import(FileBrowserCommand::Begin),
|
||||
//kpat!(Char('x')) => Cmd::Export(FileBrowserCommand::Begin),
|
||||
//kpat!(Char('c')) => Cmd::Clip(PoolClipCommand::SetColor(index, ItemColor::random())),
|
||||
//kpat!(Char('[')) | kpat!(Up) => Cmd::Select(
|
||||
//index.overflowing_sub(1).0.min(state.clips().len() - 1)
|
||||
//),
|
||||
//kpat!(Char(']')) | kpat!(Down) => Cmd::Select(
|
||||
//index.saturating_add(1) % state.clips().len()
|
||||
//),
|
||||
//kpat!(Char('<')) => if index > 1 {
|
||||
//state.set_clip_index(state.clip_index().saturating_sub(1));
|
||||
//Cmd::Clip(PoolClipCommand::Swap(index - 1, index))
|
||||
//} else {
|
||||
//return None
|
||||
//},
|
||||
//kpat!(Char('>')) => if index < count.saturating_sub(1) {
|
||||
//state.set_clip_index(state.clip_index() + 1);
|
||||
//Cmd::Clip(PoolClipCommand::Swap(index + 1, index))
|
||||
//} else {
|
||||
//return None
|
||||
//},
|
||||
//kpat!(Delete) => if index > 0 {
|
||||
//state.set_clip_index(index.min(count.saturating_sub(1)));
|
||||
//Cmd::Clip(PoolClipCommand::Delete(index))
|
||||
//} else {
|
||||
//return None
|
||||
//},
|
||||
//kpat!(Char('a')) | kpat!(Shift-Char('A')) => Cmd::Clip(PoolClipCommand::Add(count, MidiClip::new(
|
||||
//"Clip", true, 4 * PPQ, None, Some(ItemPalette::random())
|
||||
//))),
|
||||
//kpat!(Char('i')) => Cmd::Clip(PoolClipCommand::Add(index + 1, MidiClip::new(
|
||||
//"Clip", true, 4 * PPQ, None, Some(ItemPalette::random())
|
||||
//))),
|
||||
//kpat!(Char('d')) | kpat!(Shift-Char('D')) => {
|
||||
//let mut clip = state.clips()[index].read().unwrap().duplicate();
|
||||
//clip.color = ItemPalette::random_near(clip.color, 0.25);
|
||||
//Cmd::Clip(PoolClipCommand::Add(index + 1, clip))
|
||||
//},
|
||||
//_ => return None
|
||||
//})
|
||||
//}
|
||||
//keymap!(KEYS_MIDI_EDITOR = |s: MidiEditor, _input: Event| MidiEditCommand {
|
||||
//key(Up) => SetNoteCursor(s.note_pos() + 1),
|
||||
//key(Char('w')) => SetNoteCursor(s.note_pos() + 1),
|
||||
//key(Down) => SetNoteCursor(s.note_pos().saturating_sub(1)),
|
||||
//key(Char('s')) => SetNoteCursor(s.note_pos().saturating_sub(1)),
|
||||
//key(Left) => SetTimeCursor(s.time_pos().saturating_sub(s.note_len())),
|
||||
//key(Char('a')) => SetTimeCursor(s.time_pos().saturating_sub(s.note_len())),
|
||||
//key(Right) => SetTimeCursor((s.time_pos() + s.note_len()) % s.clip_length()),
|
||||
//ctrl(alt(key(Up))) => SetNoteScroll(s.note_pos() + 3),
|
||||
//ctrl(alt(key(Down))) => SetNoteScroll(s.note_pos().saturating_sub(3)),
|
||||
//ctrl(alt(key(Left))) => SetTimeScroll(s.time_pos().saturating_sub(s.time_zoom().get())),
|
||||
//ctrl(alt(key(Right))) => SetTimeScroll((s.time_pos() + s.time_zoom().get()) % s.clip_length()),
|
||||
//ctrl(key(Up)) => SetNoteScroll(s.note_lo().get() + 1),
|
||||
//ctrl(key(Down)) => SetNoteScroll(s.note_lo().get().saturating_sub(1)),
|
||||
//ctrl(key(Left)) => SetTimeScroll(s.time_start().get().saturating_sub(s.note_len())),
|
||||
//ctrl(key(Right)) => SetTimeScroll(s.time_start().get() + s.note_len()),
|
||||
//alt(key(Up)) => SetNoteCursor(s.note_pos() + 3),
|
||||
//alt(key(Down)) => SetNoteCursor(s.note_pos().saturating_sub(3)),
|
||||
//alt(key(Left)) => SetTimeCursor(s.time_pos().saturating_sub(s.time_zoom().get())),
|
||||
//alt(key(Right)) => SetTimeCursor((s.time_pos() + s.time_zoom().get()) % s.clip_length()),
|
||||
//key(Char('d')) => SetTimeCursor((s.time_pos() + s.note_len()) % s.clip_length()),
|
||||
//key(Char('z')) => SetTimeLock(!s.time_lock().get()),
|
||||
//key(Char('-')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::next(s.time_zoom().get()) }),
|
||||
//key(Char('_')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::next(s.time_zoom().get()) }),
|
||||
//key(Char('=')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::prev(s.time_zoom().get()) }),
|
||||
//key(Char('+')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::prev(s.time_zoom().get()) }),
|
||||
//key(Enter) => PutNote,
|
||||
//ctrl(key(Enter)) => AppendNote,
|
||||
//key(Char(',')) => SetNoteLength(NoteDuration::prev(s.note_len())),
|
||||
//key(Char('.')) => SetNoteLength(NoteDuration::next(s.note_len())),
|
||||
//key(Char('<')) => SetNoteLength(NoteDuration::prev(s.note_len())),
|
||||
//key(Char('>')) => SetNoteLength(NoteDuration::next(s.note_len())),
|
||||
////// TODO: kpat!(Char('/')) => // toggle 3plet
|
||||
////// TODO: kpat!(Char('?')) => // toggle dotted
|
||||
//});
|
||||
|
||||
52
crates/midi/src/clip.rs
Normal file
52
crates/midi/src/clip.rs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
mod clip_editor; pub use self::clip_editor::*;
|
||||
mod clip_launch; pub use self::clip_launch::*;
|
||||
mod clip_model; pub use self::clip_model::*;
|
||||
mod clip_play; pub use self::clip_play::*;
|
||||
mod clip_view; pub use self::clip_view::*;
|
||||
|
||||
#[cfg(test)] #[test] pub fn test_midi_clip () {
|
||||
let clip = MidiClip::stop_all();
|
||||
println!("{clip:?}");
|
||||
|
||||
let clip = MidiClip::default();
|
||||
let mut clip = MidiClip::new("clip", true, 1, None, None);
|
||||
clip.set_length(96);
|
||||
clip.toggle_loop();
|
||||
clip.record_event(12, midly::MidiMessage::NoteOn { key: 36.into(), vel: 100.into() });
|
||||
assert!(clip.contains_note_on(36.into(), 6, 18));
|
||||
assert_eq!(&clip.notes, &clip.duplicate().notes);
|
||||
|
||||
let clip = std::sync::Arc::new(clip);
|
||||
assert_eq!(clip.clone(), clip);
|
||||
}
|
||||
|
||||
#[cfg(test)] #[test] fn test_midi_play () {
|
||||
let player = MidiPlayer::default();
|
||||
println!("{player:?}");
|
||||
}
|
||||
|
||||
#[cfg(test)] #[test] fn test_midi_edit () {
|
||||
let editor = MidiEditor::default();
|
||||
let mut editor = MidiEditor {
|
||||
mode: PianoHorizontal::new(Some(&Arc::new(RwLock::new(MidiClip::stop_all())))),
|
||||
size: Default::default(),
|
||||
keys: Default::default(),
|
||||
};
|
||||
let _ = editor.put_note(true);
|
||||
let _ = editor.put_note(false);
|
||||
let _ = editor.clip_status();
|
||||
let _ = editor.edit_status();
|
||||
struct TestEditorHost(Option<MidiEditor>);
|
||||
has_editor!(|self: TestEditorHost|{
|
||||
editor = self.0;
|
||||
editor_w = 0;
|
||||
editor_h = 0;
|
||||
is_editing = false;
|
||||
});
|
||||
let mut host = TestEditorHost(Some(editor));
|
||||
let _ = host.editor();
|
||||
let _ = host.editor_mut();
|
||||
let _ = host.is_editing();
|
||||
let _ = host.editor_w();
|
||||
let _ = host.editor_h();
|
||||
}
|
||||
|
|
@ -1,62 +1,60 @@
|
|||
//! MIDI editor.
|
||||
use crate::*;
|
||||
pub trait HasEditor {
|
||||
fn editor (&self) -> &Option<MidiEditor>;
|
||||
fn editor_mut (&mut self) -> &Option<MidiEditor>;
|
||||
fn is_editing (&self) -> bool { true }
|
||||
fn editor_w (&self) -> usize { 0 }
|
||||
fn editor_h (&self) -> usize { 0 }
|
||||
}
|
||||
#[macro_export] macro_rules! has_editor {
|
||||
(|$self:ident: $Struct:ident|{
|
||||
editor = $e0:expr;
|
||||
editor_w = $e1:expr;
|
||||
editor_h = $e2:expr;
|
||||
is_editing = $e3:expr;
|
||||
}) => {
|
||||
impl HasEditor for $Struct {
|
||||
fn editor (&$self) -> &Option<MidiEditor> { &$e0 }
|
||||
fn editor_mut (&mut $self) -> &Option<MidiEditor> { &mut $e0 }
|
||||
fn editor_w (&$self) -> usize { $e1 }
|
||||
fn editor_h (&$self) -> usize { $e2 }
|
||||
fn is_editing (&$self) -> bool { $e3 }
|
||||
}
|
||||
};
|
||||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||||
impl $(<$($L),*$($T $(: $U)?),*>)? HasEditor for $Struct $(<$($L),*$($T),*>)? {
|
||||
fn editor (&$self) -> &MidiEditor { &$cb }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Contains state for viewing and editing a clip
|
||||
pub struct MidiEditor {
|
||||
pub mode: PianoHorizontal,
|
||||
pub size: Measure<TuiOut>,
|
||||
keys: SourceIter<'static>
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for MidiEditor {
|
||||
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||
f.debug_struct("MidiEditor")
|
||||
.field("mode", &self.mode)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MidiEditor {
|
||||
fn default () -> Self {
|
||||
Self {
|
||||
mode: PianoHorizontal::new(None),
|
||||
size: Measure::new(),
|
||||
keys: SourceIter(include_str!("../../edn/keys_edit.edn")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
has_size!(<TuiOut>|self: MidiEditor|&self.size);
|
||||
|
||||
content!(TuiOut: |self: MidiEditor| {
|
||||
self.autoscroll();
|
||||
//self.autozoom();
|
||||
self.size.of(&self.mode)
|
||||
});
|
||||
|
||||
from!(|clip: &Arc<RwLock<MidiClip>>|MidiEditor = {
|
||||
let model = Self::from(Some(clip.clone()));
|
||||
model.redraw();
|
||||
model
|
||||
});
|
||||
|
||||
from!(|clip: Option<Arc<RwLock<MidiClip>>>|MidiEditor = {
|
||||
let mut model = Self::default();
|
||||
*model.clip_mut() = clip;
|
||||
model.redraw();
|
||||
model
|
||||
});
|
||||
|
||||
provide!(bool: |self: MidiEditor| {
|
||||
":true" => true,
|
||||
":false" => false,
|
||||
":time-lock" => self.time_lock().get(),
|
||||
":time-lock-toggle" => !self.time_lock().get(),
|
||||
});
|
||||
|
||||
provide!(usize: |self: MidiEditor| {
|
||||
":note-length" => self.note_len(),
|
||||
|
||||
|
|
@ -80,22 +78,7 @@ provide!(usize: |self: MidiEditor| {
|
|||
":time-zoom-next" => self.time_zoom().get() + 1,
|
||||
":time-zoom-prev" => self.time_zoom().get().saturating_sub(1).max(1),
|
||||
});
|
||||
impl std::fmt::Debug for MidiEditor {
|
||||
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||
f.debug_struct("MidiEditor")
|
||||
.field("mode", &self.mode)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
impl Default for MidiEditor {
|
||||
fn default () -> Self {
|
||||
Self {
|
||||
mode: PianoHorizontal::new(None),
|
||||
size: Measure::new(),
|
||||
keys: SourceIter(KEYS_EDIT),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MidiEditor {
|
||||
//fn clip_length (&self) -> usize {
|
||||
//self.clip().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1)
|
||||
|
|
@ -155,6 +138,7 @@ impl MidiEditor {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl TimeRange for MidiEditor {
|
||||
fn time_len (&self) -> &AtomicUsize { self.mode.time_len() }
|
||||
fn time_zoom (&self) -> &AtomicUsize { self.mode.time_zoom() }
|
||||
|
|
@ -162,20 +146,24 @@ impl TimeRange for MidiEditor {
|
|||
fn time_start (&self) -> &AtomicUsize { self.mode.time_start() }
|
||||
fn time_axis (&self) -> &AtomicUsize { self.mode.time_axis() }
|
||||
}
|
||||
|
||||
impl NoteRange for MidiEditor {
|
||||
fn note_lo (&self) -> &AtomicUsize { self.mode.note_lo() }
|
||||
fn note_axis (&self) -> &AtomicUsize { self.mode.note_axis() }
|
||||
}
|
||||
|
||||
impl NotePoint for MidiEditor {
|
||||
fn note_len (&self) -> usize { self.mode.note_len() }
|
||||
fn set_note_len (&self, x: usize) { self.mode.set_note_len(x) }
|
||||
fn note_pos (&self) -> usize { self.mode.note_pos() }
|
||||
fn set_note_pos (&self, x: usize) { self.mode.set_note_pos(x) }
|
||||
}
|
||||
|
||||
impl TimePoint for MidiEditor {
|
||||
fn time_pos (&self) -> usize { self.mode.time_pos() }
|
||||
fn set_time_pos (&self, x: usize) { self.mode.set_time_pos(x) }
|
||||
}
|
||||
|
||||
impl MidiViewer for MidiEditor {
|
||||
fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { self.mode.buffer_size(clip) }
|
||||
fn redraw (&self) { self.mode.redraw() }
|
||||
|
|
@ -183,6 +171,7 @@ impl MidiViewer for MidiEditor {
|
|||
fn clip_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>> { self.mode.clip_mut() }
|
||||
fn set_clip (&mut self, p: Option<&Arc<RwLock<MidiClip>>>) { self.mode.set_clip(p) }
|
||||
}
|
||||
|
||||
atom_command!(MidiEditCommand: |state: MidiEditor| {
|
||||
("note/append" [] Some(Self::AppendNote))
|
||||
("note/put" [] Some(Self::PutNote))
|
||||
|
|
@ -194,6 +183,7 @@ atom_command!(MidiEditCommand: |state: MidiEditor| {
|
|||
("time/lock" [a: bool] Some(Self::SetTimeLock(a.expect("no time lock"))))
|
||||
("time/lock" [] Some(Self::SetTimeLock(!state.time_lock().get())))
|
||||
});
|
||||
|
||||
#[derive(Clone, Debug)] pub enum MidiEditCommand {
|
||||
// TODO: 1-9 seek markers that by default start every 8th of the clip
|
||||
AppendNote,
|
||||
|
|
@ -208,6 +198,7 @@ atom_command!(MidiEditCommand: |state: MidiEditor| {
|
|||
SetTimeLock(bool),
|
||||
Show(Option<Arc<RwLock<MidiClip>>>),
|
||||
}
|
||||
|
||||
handle!(TuiIn: |self: MidiEditor, input|{
|
||||
Ok(if let Some(command) = self.keys.command::<_, MidiEditCommand, _>(self, input) {
|
||||
let _undo = command.execute(self)?;
|
||||
|
|
@ -216,6 +207,7 @@ handle!(TuiIn: |self: MidiEditor, input|{
|
|||
None
|
||||
})
|
||||
});
|
||||
|
||||
impl Command<MidiEditor> for MidiEditCommand {
|
||||
fn execute (self, state: &mut MidiEditor) -> Perhaps<Self> {
|
||||
use MidiEditCommand::*;
|
||||
|
|
@ -244,63 +236,32 @@ impl Command<MidiEditor> for MidiEditCommand {
|
|||
}
|
||||
}
|
||||
|
||||
//keymap!(KEYS_MIDI_EDITOR = |s: MidiEditor, _input: Event| MidiEditCommand {
|
||||
//key(Up) => SetNoteCursor(s.note_pos() + 1),
|
||||
//key(Char('w')) => SetNoteCursor(s.note_pos() + 1),
|
||||
//key(Down) => SetNoteCursor(s.note_pos().saturating_sub(1)),
|
||||
//key(Char('s')) => SetNoteCursor(s.note_pos().saturating_sub(1)),
|
||||
//key(Left) => SetTimeCursor(s.time_pos().saturating_sub(s.note_len())),
|
||||
//key(Char('a')) => SetTimeCursor(s.time_pos().saturating_sub(s.note_len())),
|
||||
//key(Right) => SetTimeCursor((s.time_pos() + s.note_len()) % s.clip_length()),
|
||||
//ctrl(alt(key(Up))) => SetNoteScroll(s.note_pos() + 3),
|
||||
//ctrl(alt(key(Down))) => SetNoteScroll(s.note_pos().saturating_sub(3)),
|
||||
//ctrl(alt(key(Left))) => SetTimeScroll(s.time_pos().saturating_sub(s.time_zoom().get())),
|
||||
//ctrl(alt(key(Right))) => SetTimeScroll((s.time_pos() + s.time_zoom().get()) % s.clip_length()),
|
||||
//ctrl(key(Up)) => SetNoteScroll(s.note_lo().get() + 1),
|
||||
//ctrl(key(Down)) => SetNoteScroll(s.note_lo().get().saturating_sub(1)),
|
||||
//ctrl(key(Left)) => SetTimeScroll(s.time_start().get().saturating_sub(s.note_len())),
|
||||
//ctrl(key(Right)) => SetTimeScroll(s.time_start().get() + s.note_len()),
|
||||
//alt(key(Up)) => SetNoteCursor(s.note_pos() + 3),
|
||||
//alt(key(Down)) => SetNoteCursor(s.note_pos().saturating_sub(3)),
|
||||
//alt(key(Left)) => SetTimeCursor(s.time_pos().saturating_sub(s.time_zoom().get())),
|
||||
//alt(key(Right)) => SetTimeCursor((s.time_pos() + s.time_zoom().get()) % s.clip_length()),
|
||||
//key(Char('d')) => SetTimeCursor((s.time_pos() + s.note_len()) % s.clip_length()),
|
||||
//key(Char('z')) => SetTimeLock(!s.time_lock().get()),
|
||||
//key(Char('-')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::next(s.time_zoom().get()) }),
|
||||
//key(Char('_')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::next(s.time_zoom().get()) }),
|
||||
//key(Char('=')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::prev(s.time_zoom().get()) }),
|
||||
//key(Char('+')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::prev(s.time_zoom().get()) }),
|
||||
//key(Enter) => PutNote,
|
||||
//ctrl(key(Enter)) => AppendNote,
|
||||
//key(Char(',')) => SetNoteLength(NoteDuration::prev(s.note_len())),
|
||||
//key(Char('.')) => SetNoteLength(NoteDuration::next(s.note_len())),
|
||||
//key(Char('<')) => SetNoteLength(NoteDuration::prev(s.note_len())),
|
||||
//key(Char('>')) => SetNoteLength(NoteDuration::next(s.note_len())),
|
||||
////// TODO: kpat!(Char('/')) => // toggle 3plet
|
||||
////// TODO: kpat!(Char('?')) => // toggle dotted
|
||||
//});
|
||||
|
||||
#[cfg(test)] #[test] fn test_midi_edit () {
|
||||
let mut editor = MidiEditor {
|
||||
mode: PianoHorizontal::new(Some(&Arc::new(RwLock::new(MidiClip::stop_all())))),
|
||||
size: Default::default(),
|
||||
keys: Default::default(),
|
||||
};
|
||||
let _ = editor.put_note(true);
|
||||
let _ = editor.put_note(false);
|
||||
let _ = editor.clip_status();
|
||||
let _ = editor.edit_status();
|
||||
struct TestEditorHost(Option<MidiEditor>);
|
||||
has_editor!(|self: TestEditorHost|{
|
||||
editor = self.0;
|
||||
editor_w = 0;
|
||||
editor_h = 0;
|
||||
is_editing = false;
|
||||
});
|
||||
let mut host = TestEditorHost(Some(editor));
|
||||
let _ = host.editor();
|
||||
let _ = host.editor_mut();
|
||||
let _ = host.is_editing();
|
||||
let _ = host.editor_w();
|
||||
let _ = host.editor_h();
|
||||
pub trait HasEditor {
|
||||
fn editor (&self) -> &Option<MidiEditor>;
|
||||
fn editor_mut (&mut self) -> &Option<MidiEditor>;
|
||||
fn is_editing (&self) -> bool { true }
|
||||
fn editor_w (&self) -> usize { 0 }
|
||||
fn editor_h (&self) -> usize { 0 }
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! has_editor {
|
||||
(|$self:ident: $Struct:ident|{
|
||||
editor = $e0:expr;
|
||||
editor_w = $e1:expr;
|
||||
editor_h = $e2:expr;
|
||||
is_editing = $e3:expr;
|
||||
}) => {
|
||||
impl HasEditor for $Struct {
|
||||
fn editor (&$self) -> &Option<MidiEditor> { &$e0 }
|
||||
fn editor_mut (&mut $self) -> &Option<MidiEditor> { &mut $e0 }
|
||||
fn editor_w (&$self) -> usize { $e1 }
|
||||
fn editor_h (&$self) -> usize { $e2 }
|
||||
fn is_editing (&$self) -> bool { $e3 }
|
||||
}
|
||||
};
|
||||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||||
impl $(<$($L),*$($T $(: $U)?),*>)? HasEditor for $Struct $(<$($L),*$($T),*>)? {
|
||||
fn editor (&$self) -> &MidiEditor { &$cb }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -1,12 +1,19 @@
|
|||
use crate::*;
|
||||
|
||||
pub trait HasPlayClip: HasClock {
|
||||
|
||||
fn reset (&self) -> bool;
|
||||
|
||||
fn reset_mut (&mut self) -> &mut bool;
|
||||
|
||||
fn play_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
||||
|
||||
fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
||||
|
||||
fn next_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
||||
|
||||
fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
||||
|
||||
fn pulses_since_start (&self) -> Option<f64> {
|
||||
if let Some((started, Some(_))) = self.play_clip().as_ref() {
|
||||
let elapsed = self.clock().playhead.pulse.get() - started.pulse.get();
|
||||
|
|
@ -15,6 +22,7 @@ pub trait HasPlayClip: HasClock {
|
|||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn pulses_since_start_looped (&self) -> Option<(f64, f64)> {
|
||||
if let Some((started, Some(clip))) = self.play_clip().as_ref() {
|
||||
let elapsed = self.clock().playhead.pulse.get() - started.pulse.get();
|
||||
|
|
@ -26,12 +34,14 @@ pub trait HasPlayClip: HasClock {
|
|||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn enqueue_next (&mut self, clip: Option<&Arc<RwLock<MidiClip>>>) {
|
||||
let start = self.clock().next_launch_pulse() as f64;
|
||||
let instant = Moment::from_pulse(self.clock().timebase(), start);
|
||||
*self.next_clip_mut() = Some((instant, clip.cloned()));
|
||||
*self.reset_mut() = true;
|
||||
}
|
||||
|
||||
fn play_status (&self) -> impl Content<TuiOut> {
|
||||
let (name, color): (Arc<str>, ItemPalette) = if let Some((_, Some(clip))) = self.play_clip() {
|
||||
let MidiClip { ref name, color, .. } = *clip.read().unwrap();
|
||||
|
|
@ -44,6 +54,7 @@ pub trait HasPlayClip: HasClock {
|
|||
.unwrap_or_else(||String::from(" ")).into();
|
||||
FieldV(color, "Now:", format!("{} {}", time, name))
|
||||
}
|
||||
|
||||
fn next_status (&self) -> impl Content<TuiOut> {
|
||||
let mut time: Arc<str> = String::from("--.-.--").into();
|
||||
let mut name: Arc<str> = String::from("").into();
|
||||
|
|
@ -81,4 +92,5 @@ pub trait HasPlayClip: HasClock {
|
|||
};
|
||||
FieldV(color, "Next:", format!("{} {}", time, name))
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,86 +1,21 @@
|
|||
mod midi_clip; pub use midi_clip::*;
|
||||
mod midi_edit; pub use midi_edit::*;
|
||||
mod midi_in; pub use midi_in::*;
|
||||
mod midi_launch; pub use midi_launch::*;
|
||||
mod midi_out; pub use midi_out::*;
|
||||
mod midi_pitch; pub use midi_pitch::*;
|
||||
mod midi_player; pub use midi_player::*;
|
||||
mod midi_point; pub use midi_point::*;
|
||||
mod midi_pool; pub use midi_pool::*;
|
||||
mod midi_range; pub use midi_range::*;
|
||||
mod midi_view; pub use midi_view::*;
|
||||
mod piano_h; pub use self::piano_h::*;
|
||||
mod piano_v; pub use self::piano_v::*;
|
||||
|
||||
pub(crate) use ::tek_time::*;
|
||||
pub(crate) use ::tek_jack::{*, jack::*};
|
||||
pub(crate) use ::tengri::{
|
||||
input::*,
|
||||
output::*,
|
||||
dsl::*,
|
||||
tui::{
|
||||
*,
|
||||
ratatui::style::{Style, Stylize, Color}
|
||||
}
|
||||
};
|
||||
|
||||
pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicUsize, AtomicBool, Ordering::Relaxed}};
|
||||
pub(crate) use std::path::PathBuf;
|
||||
pub(crate) use std::fmt::Debug;
|
||||
|
||||
pub use ::midly; pub(crate) use ::midly::{*, num::*, live::*};
|
||||
pub use ::midly;
|
||||
pub(crate) use ::midly::{*, num::*, live::*};
|
||||
|
||||
pub(crate) const KEYS_EDIT: &str = include_str!("../edn/keys_edit.edn");
|
||||
pub(crate) const KEYS_POOL: &str = include_str!("../edn/keys_pool.edn");
|
||||
pub(crate) const KEYS_FILE: &str = include_str!("../edn/keys_pool_file.edn");
|
||||
pub(crate) const KEYS_LENGTH: &str = include_str!("../edn/keys_clip_length.edn");
|
||||
pub(crate) const KEYS_RENAME: &str = include_str!("../edn/keys_clip_rename.edn");
|
||||
pub(crate) use ::tek_time::*;
|
||||
pub(crate) use ::tek_jack::{*, jack::*};
|
||||
pub(crate) use ::tengri::input::*;
|
||||
pub(crate) use ::tengri::output::*;
|
||||
pub(crate) use ::tengri::dsl::*;
|
||||
pub(crate) use ::tengri::tui::*;
|
||||
pub(crate) use ::tengri::tui::ratatui::style::{Style, Stylize, Color};
|
||||
|
||||
/// Add "all notes off" to the start of a buffer.
|
||||
pub fn all_notes_off (output: &mut [Vec<Vec<u8>>]) {
|
||||
let mut buf = vec![];
|
||||
let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() };
|
||||
let evt = LiveEvent::Midi { channel: 0.into(), message: msg };
|
||||
evt.write(&mut buf).unwrap();
|
||||
output[0].push(buf);
|
||||
}
|
||||
|
||||
/// Return boxed iterator of MIDI events
|
||||
pub fn parse_midi_input <'a> (input: MidiIter<'a>) -> Box<dyn Iterator<Item=(usize, LiveEvent<'a>, &'a [u8])> + 'a> {
|
||||
Box::new(input.map(|RawMidi { time, bytes }|(
|
||||
time as usize,
|
||||
LiveEvent::parse(bytes).unwrap(),
|
||||
bytes
|
||||
)))
|
||||
}
|
||||
|
||||
/// Update notes_in array
|
||||
pub fn update_keys (keys: &mut[bool;128], message: &MidiMessage) {
|
||||
match message {
|
||||
MidiMessage::NoteOn { key, .. } => { keys[key.as_int() as usize] = true; }
|
||||
MidiMessage::NoteOff { key, .. } => { keys[key.as_int() as usize] = false; },
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)] #[test] pub fn test_midi_clip () {
|
||||
let clip = MidiClip::stop_all();
|
||||
println!("{clip:?}");
|
||||
let clip = MidiClip::default();
|
||||
let mut clip = MidiClip::new("clip", true, 1, None, None);
|
||||
clip.set_length(96);
|
||||
clip.toggle_loop();
|
||||
clip.record_event(12, midly::MidiMessage::NoteOn { key: 36.into(), vel: 100.into() });
|
||||
assert!(clip.contains_note_on(36.into(), 6, 18));
|
||||
assert_eq!(&clip.notes, &clip.duplicate().notes);
|
||||
let clip = std::sync::Arc::new(clip);
|
||||
assert_eq!(clip.clone(), clip);
|
||||
}
|
||||
#[cfg(test)] #[test] pub fn test_midi_edit () {
|
||||
let editor = MidiEditor::default();
|
||||
println!("{editor:?}");
|
||||
}
|
||||
#[cfg(test)] #[test] pub fn test_midi_player () {
|
||||
let player = MidiPlayer::default();
|
||||
println!("{player:?}");
|
||||
}
|
||||
mod clip; pub use self::clip::*;
|
||||
mod mode; pub use self::mode::*;
|
||||
mod note; pub use self::note::*;
|
||||
mod piano; pub use self::piano::*;
|
||||
mod pool; pub use self::pool::*;
|
||||
mod port; pub use self::port::*;
|
||||
|
|
|
|||
|
|
@ -1,561 +0,0 @@
|
|||
use crate::*;
|
||||
pub type ClipPool = Vec<Arc<RwLock<MidiClip>>>;
|
||||
pub trait HasClips {
|
||||
fn clips <'a> (&'a self) -> std::sync::RwLockReadGuard<'a, ClipPool>;
|
||||
fn clips_mut <'a> (&'a self) -> std::sync::RwLockWriteGuard<'a, ClipPool>;
|
||||
fn add_clip (&self) -> (usize, Arc<RwLock<MidiClip>>) {
|
||||
let clip = Arc::new(RwLock::new(MidiClip::new("Clip", true, 384, None, None)));
|
||||
self.clips_mut().push(clip.clone());
|
||||
(self.clips().len() - 1, clip)
|
||||
}
|
||||
}
|
||||
#[macro_export] macro_rules! has_clips {
|
||||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||||
impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? {
|
||||
fn clips <'a> (&'a $self) -> std::sync::RwLockReadGuard<'a, ClipPool> {
|
||||
$cb.read().unwrap()
|
||||
}
|
||||
fn clips_mut <'a> (&'a $self) -> std::sync::RwLockWriteGuard<'a, ClipPool> {
|
||||
$cb.write().unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub struct MidiPool {
|
||||
pub visible: bool,
|
||||
/// Collection of clips
|
||||
pub clips: Arc<RwLock<Vec<Arc<RwLock<MidiClip>>>>>,
|
||||
/// Selected clip
|
||||
pub clip: AtomicUsize,
|
||||
/// Mode switch
|
||||
pub mode: Option<PoolMode>,
|
||||
|
||||
keys: SourceIter<'static>,
|
||||
keys_rename: SourceIter<'static>,
|
||||
keys_length: SourceIter<'static>,
|
||||
keys_file: SourceIter<'static>,
|
||||
}
|
||||
/// Modes for clip pool
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PoolMode {
|
||||
/// Renaming a pattern
|
||||
Rename(usize, Arc<str>),
|
||||
/// Editing the length of a pattern
|
||||
Length(usize, usize, ClipLengthFocus),
|
||||
/// Load clip from disk
|
||||
Import(usize, FileBrowser),
|
||||
/// Save clip to disk
|
||||
Export(usize, FileBrowser),
|
||||
}
|
||||
impl Default for MidiPool {
|
||||
fn default () -> Self {
|
||||
Self {
|
||||
visible: true,
|
||||
clips: Arc::from(RwLock::from(vec![])),
|
||||
clip: 0.into(),
|
||||
mode: None,
|
||||
keys: SourceIter(KEYS_POOL),
|
||||
keys_rename: SourceIter(KEYS_RENAME),
|
||||
keys_length: SourceIter(KEYS_LENGTH),
|
||||
keys_file: SourceIter(KEYS_FILE),
|
||||
}
|
||||
}
|
||||
}
|
||||
from!(|clip:&Arc<RwLock<MidiClip>>|MidiPool = {
|
||||
let model = Self::default();
|
||||
model.clips.write().unwrap().push(clip.clone());
|
||||
model.clip.store(1, Relaxed);
|
||||
model
|
||||
});
|
||||
has_clips!(|self: MidiPool|self.clips);
|
||||
has_clip!(|self: MidiPool|self.clips().get(self.clip_index()).map(|c|c.clone()));
|
||||
impl MidiPool {
|
||||
fn clip_index (&self) -> usize { self.clip.load(Relaxed) }
|
||||
fn set_clip_index (&self, value: usize) { self.clip.store(value, Relaxed); }
|
||||
fn mode (&self) -> &Option<PoolMode> { &self.mode }
|
||||
fn mode_mut (&mut self) -> &mut Option<PoolMode> { &mut self.mode }
|
||||
fn begin_clip_length (&mut self) {
|
||||
let length = self.clips()[self.clip_index()].read().unwrap().length;
|
||||
*self.mode_mut() = Some(PoolMode::Length(
|
||||
self.clip_index(),
|
||||
length,
|
||||
ClipLengthFocus::Bar
|
||||
));
|
||||
}
|
||||
fn begin_clip_rename (&mut self) {
|
||||
let name = self.clips()[self.clip_index()].read().unwrap().name.clone();
|
||||
*self.mode_mut() = Some(PoolMode::Rename(
|
||||
self.clip_index(),
|
||||
name
|
||||
));
|
||||
}
|
||||
fn begin_import (&mut self) -> Usually<()> {
|
||||
*self.mode_mut() = Some(PoolMode::Import(
|
||||
self.clip_index(),
|
||||
FileBrowser::new(None)?
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
fn begin_export (&mut self) -> Usually<()> {
|
||||
*self.mode_mut() = Some(PoolMode::Export(
|
||||
self.clip_index(),
|
||||
FileBrowser::new(None)?
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
/// Displays and edits clip length.
|
||||
#[derive(Clone)]
|
||||
pub struct ClipLength {
|
||||
/// Pulses per beat (quaver)
|
||||
ppq: usize,
|
||||
/// Beats per bar
|
||||
bpb: usize,
|
||||
/// Length of clip in pulses
|
||||
pulses: usize,
|
||||
/// Selected subdivision
|
||||
focus: Option<ClipLengthFocus>,
|
||||
}
|
||||
impl ClipLength {
|
||||
fn _new (pulses: usize, focus: Option<ClipLengthFocus>) -> Self {
|
||||
Self { ppq: PPQ, bpb: 4, pulses, focus }
|
||||
}
|
||||
fn bars (&self) -> usize {
|
||||
self.pulses / (self.bpb * self.ppq)
|
||||
}
|
||||
fn beats (&self) -> usize {
|
||||
(self.pulses % (self.bpb * self.ppq)) / self.ppq
|
||||
}
|
||||
fn ticks (&self) -> usize {
|
||||
self.pulses % self.ppq
|
||||
}
|
||||
fn bars_string (&self) -> Arc<str> {
|
||||
format!("{}", self.bars()).into()
|
||||
}
|
||||
fn beats_string (&self) -> Arc<str> {
|
||||
format!("{}", self.beats()).into()
|
||||
}
|
||||
fn ticks_string (&self) -> Arc<str> {
|
||||
format!("{:>02}", self.ticks()).into()
|
||||
}
|
||||
}
|
||||
/// Focused field of `ClipLength`
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum ClipLengthFocus {
|
||||
/// Editing the number of bars
|
||||
Bar,
|
||||
/// Editing the number of beats
|
||||
Beat,
|
||||
/// Editing the number of ticks
|
||||
Tick,
|
||||
}
|
||||
impl ClipLengthFocus {
|
||||
fn next (&mut self) {
|
||||
*self = match self { Self::Bar => Self::Beat, Self::Beat => Self::Tick, Self::Tick => Self::Bar, }
|
||||
}
|
||||
fn prev (&mut self) {
|
||||
*self = match self { Self::Bar => Self::Tick, Self::Beat => Self::Bar, Self::Tick => Self::Beat, }
|
||||
}
|
||||
}
|
||||
pub struct PoolView<'a>(pub bool, pub &'a MidiPool);
|
||||
content!(TuiOut: |self: PoolView<'a>| {
|
||||
let Self(compact, model) = self;
|
||||
let MidiPool { clips, .. } = self.1;
|
||||
//let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||Tui::g(32).into());
|
||||
let on_bg = |x|x;//Bsp::b(Repeat(" "), Tui::bg(color.darkest.rgb, x));
|
||||
let border = |x|x;//Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)).enclose(x);
|
||||
let iter = | |model.clips().clone().into_iter();
|
||||
let height = clips.read().unwrap().len() as u16;
|
||||
Tui::bg(Color::Reset, Fixed::y(height, on_bg(border(Map::new(iter, move|clip: Arc<RwLock<MidiClip>>, i|{
|
||||
let item_height = 1;
|
||||
let item_offset = i as u16 * item_height;
|
||||
let selected = i == model.clip_index();
|
||||
let MidiClip { ref name, color, length, .. } = *clip.read().unwrap();
|
||||
let bg = if selected { color.light.rgb } else { color.base.rgb };
|
||||
let fg = color.lightest.rgb;
|
||||
let name = if *compact { format!(" {i:>3}") } else { format!(" {i:>3} {name}") };
|
||||
let length = if *compact { String::default() } else { format!("{length} ") };
|
||||
Fixed::y(1, map_south(item_offset, item_height, Tui::bg(bg, lay!(
|
||||
Fill::x(Align::w(Tui::fg(fg, Tui::bold(selected, name)))),
|
||||
Fill::x(Align::e(Tui::fg(fg, Tui::bold(selected, length)))),
|
||||
Fill::x(Align::w(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "▶"))))),
|
||||
Fill::x(Align::e(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "◀"))))),
|
||||
))))
|
||||
})))))
|
||||
});
|
||||
content!(TuiOut: |self: ClipLength| {
|
||||
let bars = ||self.bars_string();
|
||||
let beats = ||self.beats_string();
|
||||
let ticks = ||self.ticks_string();
|
||||
match self.focus {
|
||||
None =>
|
||||
row!(" ", bars(), ".", beats(), ".", ticks()),
|
||||
Some(ClipLengthFocus::Bar) =>
|
||||
row!("[", bars(), "]", beats(), ".", ticks()),
|
||||
Some(ClipLengthFocus::Beat) =>
|
||||
row!(" ", bars(), "[", beats(), "]", ticks()),
|
||||
Some(ClipLengthFocus::Tick) =>
|
||||
row!(" ", bars(), ".", beats(), "[", ticks()),
|
||||
}
|
||||
});
|
||||
handle!(TuiIn: |self: MidiPool, input|{
|
||||
Ok(if let Some(command) = match self.mode() {
|
||||
Some(PoolMode::Rename(..)) => self.keys_rename,
|
||||
Some(PoolMode::Length(..)) => self.keys_length,
|
||||
Some(PoolMode::Import(..)) | Some(PoolMode::Export(..)) => self.keys_file,
|
||||
_ => self.keys
|
||||
}.command::<Self, PoolCommand, TuiIn>(self, input) {
|
||||
let _undo = command.execute(self)?;
|
||||
Some(true)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
});
|
||||
provide!(bool: |self: MidiPool| {});
|
||||
impl MidiPool {
|
||||
pub fn new_clip (&self) -> MidiClip {
|
||||
MidiClip::new("Clip", true, 4 * PPQ, None, Some(ItemPalette::random()))
|
||||
}
|
||||
pub fn cloned_clip (&self) -> MidiClip {
|
||||
let index = self.clip_index();
|
||||
let mut clip = self.clips()[index].read().unwrap().duplicate();
|
||||
clip.color = ItemPalette::random_near(clip.color, 0.25);
|
||||
clip
|
||||
}
|
||||
pub fn add_new_clip (&self) -> (usize, Arc<RwLock<MidiClip>>) {
|
||||
let clip = Arc::new(RwLock::new(self.new_clip()));
|
||||
let index = {
|
||||
let mut clips = self.clips.write().unwrap();
|
||||
clips.push(clip.clone());
|
||||
clips.len().saturating_sub(1)
|
||||
};
|
||||
self.clip.store(index, Relaxed);
|
||||
(index, clip)
|
||||
}
|
||||
}
|
||||
provide!(MidiClip: |self: MidiPool| {
|
||||
":new-clip" => self.new_clip(),
|
||||
":cloned-clip" => self.cloned_clip(),
|
||||
});
|
||||
provide!(PathBuf: |self: MidiPool| {});
|
||||
provide!(Arc<str>: |self: MidiPool| {});
|
||||
provide!(usize: |self: MidiPool| {
|
||||
":current" => 0,
|
||||
":after" => 0,
|
||||
":previous" => 0,
|
||||
":next" => 0
|
||||
});
|
||||
provide!(ItemColor: |self: MidiPool| {
|
||||
":random-color" => ItemColor::random()
|
||||
});
|
||||
#[derive(Clone, PartialEq, Debug)] pub enum PoolCommand {
|
||||
/// Toggle visibility of pool
|
||||
Show(bool),
|
||||
/// Select a clip from the clip pool
|
||||
Select(usize),
|
||||
/// Rename a clip
|
||||
Rename(ClipRenameCommand),
|
||||
/// Change the length of a clip
|
||||
Length(ClipLengthCommand),
|
||||
/// Import from file
|
||||
Import(FileBrowserCommand),
|
||||
/// Export to file
|
||||
Export(FileBrowserCommand),
|
||||
/// Update the contents of the clip pool
|
||||
Clip(PoolClipCommand),
|
||||
}
|
||||
atom_command!(PoolCommand: |state: MidiPool| {
|
||||
("show" [a: bool] Some(Self::Show(a.expect("no flag"))))
|
||||
("select" [i: usize] Some(Self::Select(i.expect("no index"))))
|
||||
("rename" [,..a] ClipRenameCommand::try_from_expr(state, a).map(Self::Rename))
|
||||
("length" [,..a] ClipLengthCommand::try_from_expr(state, a).map(Self::Length))
|
||||
("import" [,..a] FileBrowserCommand::try_from_expr(state, a).map(Self::Import))
|
||||
("export" [,..a] FileBrowserCommand::try_from_expr(state, a).map(Self::Export))
|
||||
("clip" [,..a] PoolClipCommand::try_from_expr(state, a).map(Self::Clip))
|
||||
});
|
||||
command!(|self: PoolCommand, state: MidiPool|{
|
||||
use PoolCommand::*;
|
||||
match self {
|
||||
Rename(ClipRenameCommand::Begin) => { state.begin_clip_rename(); None }
|
||||
Rename(command) => command.delegate(state, Rename)?,
|
||||
Length(ClipLengthCommand::Begin) => { state.begin_clip_length(); None },
|
||||
Length(command) => command.delegate(state, Length)?,
|
||||
Import(FileBrowserCommand::Begin) => { state.begin_import()?; None },
|
||||
Import(command) => command.delegate(state, Import)?,
|
||||
Export(FileBrowserCommand::Begin) => { state.begin_export()?; None },
|
||||
Export(command) => command.delegate(state, Export)?,
|
||||
Clip(command) => command.execute(state)?.map(Clip),
|
||||
Show(visible) => { state.visible = visible; Some(Self::Show(!visible)) },
|
||||
Select(clip) => { state.set_clip_index(clip); None },
|
||||
}
|
||||
});
|
||||
#[derive(Clone, Debug, PartialEq)] pub enum PoolClipCommand {
|
||||
Add(usize, MidiClip),
|
||||
Delete(usize),
|
||||
Swap(usize, usize),
|
||||
Import(usize, PathBuf),
|
||||
Export(usize, PathBuf),
|
||||
SetName(usize, Arc<str>),
|
||||
SetLength(usize, usize),
|
||||
SetColor(usize, ItemColor),
|
||||
}
|
||||
atom_command!(PoolClipCommand: |state: MidiPool| {
|
||||
("add" [i: usize, c: MidiClip]
|
||||
Some(Self::Add(i.expect("no index"), c.expect("no clip"))))
|
||||
("delete" [i: usize]
|
||||
Some(Self::Delete(i.expect("no index"))))
|
||||
("swap" [a: usize, b: usize]
|
||||
Some(Self::Swap(a.expect("no index"), b.expect("no index"))))
|
||||
("import" [i: usize, p: PathBuf]
|
||||
Some(Self::Import(i.expect("no index"), p.expect("no path"))))
|
||||
("export" [i: usize, p: PathBuf]
|
||||
Some(Self::Export(i.expect("no index"), p.expect("no path"))))
|
||||
("set-name" [i: usize, n: Arc<str>]
|
||||
Some(Self::SetName(i.expect("no index"), n.expect("no name"))))
|
||||
("set-length" [i: usize, l: usize]
|
||||
Some(Self::SetLength(i.expect("no index"), l.expect("no length"))))
|
||||
("set-color" [i: usize, c: ItemColor]
|
||||
Some(Self::SetColor(i.expect("no index"), c.expect("no color"))))
|
||||
});
|
||||
impl<T: HasClips> Command<T> for PoolClipCommand {
|
||||
fn execute (self, model: &mut T) -> Perhaps<Self> {
|
||||
use PoolClipCommand::*;
|
||||
Ok(match self {
|
||||
Add(mut index, clip) => {
|
||||
let clip = Arc::new(RwLock::new(clip));
|
||||
let mut clips = model.clips_mut();
|
||||
if index >= clips.len() {
|
||||
index = clips.len();
|
||||
clips.push(clip)
|
||||
} else {
|
||||
clips.insert(index, clip);
|
||||
}
|
||||
Some(Self::Delete(index))
|
||||
},
|
||||
Delete(index) => {
|
||||
let clip = model.clips_mut().remove(index).read().unwrap().clone();
|
||||
Some(Self::Add(index, clip))
|
||||
},
|
||||
Swap(index, other) => {
|
||||
model.clips_mut().swap(index, other);
|
||||
Some(Self::Swap(index, other))
|
||||
},
|
||||
Import(index, path) => {
|
||||
let bytes = std::fs::read(&path)?;
|
||||
let smf = Smf::parse(bytes.as_slice())?;
|
||||
let mut t = 0u32;
|
||||
let mut events = vec![];
|
||||
for track in smf.tracks.iter() {
|
||||
for event in track.iter() {
|
||||
t += event.delta.as_int();
|
||||
if let TrackEventKind::Midi { channel, message } = event.kind {
|
||||
events.push((t, channel.as_int(), message));
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut clip = MidiClip::new("imported", true, t as usize + 1, None, None);
|
||||
for event in events.iter() {
|
||||
clip.notes[event.0 as usize].push(event.2);
|
||||
}
|
||||
Self::Add(index, clip).execute(model)?
|
||||
},
|
||||
Export(_index, _path) => {
|
||||
todo!("export clip to midi file");
|
||||
},
|
||||
SetName(index, name) => {
|
||||
let clip = &mut model.clips_mut()[index];
|
||||
let old_name = clip.read().unwrap().name.clone();
|
||||
clip.write().unwrap().name = name;
|
||||
Some(Self::SetName(index, old_name))
|
||||
},
|
||||
SetLength(index, length) => {
|
||||
let clip = &mut model.clips_mut()[index];
|
||||
let old_len = clip.read().unwrap().length;
|
||||
clip.write().unwrap().length = length;
|
||||
Some(Self::SetLength(index, old_len))
|
||||
},
|
||||
SetColor(index, color) => {
|
||||
let mut color = ItemPalette::from(color);
|
||||
std::mem::swap(&mut color, &mut model.clips()[index].write().unwrap().color);
|
||||
Some(Self::SetColor(index, color.base))
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug, PartialEq)] pub enum ClipRenameCommand {
|
||||
Begin,
|
||||
Cancel,
|
||||
Confirm,
|
||||
Set(Arc<str>),
|
||||
}
|
||||
atom_command!(ClipRenameCommand: |state: MidiPool| {
|
||||
("begin" [] Some(Self::Begin))
|
||||
("cancel" [] Some(Self::Cancel))
|
||||
("confirm" [] Some(Self::Confirm))
|
||||
("set" [n: Arc<str>] Some(Self::Set(n.expect("no name"))))
|
||||
});
|
||||
command!(|self: ClipRenameCommand, state: MidiPool|if let Some(
|
||||
PoolMode::Rename(clip, ref mut old_name)
|
||||
) = state.mode_mut().clone() {
|
||||
match self {
|
||||
Self::Set(s) => {
|
||||
state.clips()[clip].write().unwrap().name = s;
|
||||
return Ok(Some(Self::Set(old_name.clone().into())))
|
||||
},
|
||||
Self::Confirm => {
|
||||
let old_name = old_name.clone();
|
||||
*state.mode_mut() = None;
|
||||
return Ok(Some(Self::Set(old_name)))
|
||||
},
|
||||
Self::Cancel => {
|
||||
state.clips()[clip].write().unwrap().name = old_name.clone().into();
|
||||
return Ok(None)
|
||||
},
|
||||
_ => unreachable!()
|
||||
}
|
||||
} else {
|
||||
unreachable!()
|
||||
});
|
||||
#[derive(Copy, Clone, Debug, PartialEq)] pub enum ClipLengthCommand {
|
||||
Begin,
|
||||
Cancel,
|
||||
Set(usize),
|
||||
Next,
|
||||
Prev,
|
||||
Inc,
|
||||
Dec,
|
||||
}
|
||||
atom_command!(ClipLengthCommand: |state: MidiPool| {
|
||||
("begin" [] Some(Self::Begin))
|
||||
("cancel" [] Some(Self::Cancel))
|
||||
("next" [] Some(Self::Next))
|
||||
("prev" [] Some(Self::Prev))
|
||||
("inc" [] Some(Self::Inc))
|
||||
("dec" [] Some(Self::Dec))
|
||||
("set" [l: usize] Some(Self::Set(l.expect("no length"))))
|
||||
});
|
||||
command!(|self: ClipLengthCommand, state: MidiPool|{
|
||||
use ClipLengthCommand::*;
|
||||
use ClipLengthFocus::*;
|
||||
if let Some(
|
||||
PoolMode::Length(clip, ref mut length, ref mut focus)
|
||||
) = state.mode_mut().clone() {
|
||||
match self {
|
||||
Cancel => { *state.mode_mut() = None; },
|
||||
Prev => { focus.prev() },
|
||||
Next => { focus.next() },
|
||||
Inc => match focus {
|
||||
Bar => { *length += 4 * PPQ },
|
||||
Beat => { *length += PPQ },
|
||||
Tick => { *length += 1 },
|
||||
},
|
||||
Dec => match focus {
|
||||
Bar => { *length = length.saturating_sub(4 * PPQ) },
|
||||
Beat => { *length = length.saturating_sub(PPQ) },
|
||||
Tick => { *length = length.saturating_sub(1) },
|
||||
},
|
||||
Set(length) => {
|
||||
let old_length;
|
||||
{
|
||||
let clip = state.clips()[clip].clone();//.write().unwrap();
|
||||
old_length = Some(clip.read().unwrap().length);
|
||||
clip.write().unwrap().length = length;
|
||||
}
|
||||
*state.mode_mut() = None;
|
||||
return Ok(old_length.map(Self::Set))
|
||||
},
|
||||
_ => unreachable!()
|
||||
}
|
||||
} else {
|
||||
unreachable!();
|
||||
}
|
||||
None
|
||||
});
|
||||
atom_command!(FileBrowserCommand: |state: MidiPool| {
|
||||
("begin" [] Some(Self::Begin))
|
||||
("cancel" [] Some(Self::Cancel))
|
||||
("confirm" [] Some(Self::Confirm))
|
||||
("select" [i: usize] Some(Self::Select(i.expect("no index"))))
|
||||
("chdir" [p: PathBuf] Some(Self::Chdir(p.expect("no path"))))
|
||||
("filter" [f: Arc<str>] Some(Self::Filter(f.expect("no filter"))))
|
||||
});
|
||||
command!(|self: FileBrowserCommand, state: MidiPool|{
|
||||
use PoolMode::*;
|
||||
use FileBrowserCommand::*;
|
||||
let mode = &mut state.mode;
|
||||
match mode {
|
||||
Some(Import(index, ref mut browser)) => match self {
|
||||
Cancel => { *mode = None; },
|
||||
Chdir(cwd) => { *mode = Some(Import(*index, FileBrowser::new(Some(cwd))?)); },
|
||||
Select(index) => { browser.index = index; },
|
||||
Confirm => if browser.is_file() {
|
||||
let index = *index;
|
||||
let path = browser.path();
|
||||
*mode = None;
|
||||
PoolClipCommand::Import(index, path).execute(state)?;
|
||||
} else if browser.is_dir() {
|
||||
*mode = Some(Import(*index, browser.chdir()?));
|
||||
},
|
||||
_ => todo!(),
|
||||
},
|
||||
Some(Export(index, ref mut browser)) => match self {
|
||||
Cancel => { *mode = None; },
|
||||
Chdir(cwd) => { *mode = Some(Export(*index, FileBrowser::new(Some(cwd))?)); },
|
||||
Select(index) => { browser.index = index; },
|
||||
_ => unreachable!()
|
||||
},
|
||||
_ => unreachable!(),
|
||||
};
|
||||
None
|
||||
});
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//fn to_clips_command (state: &MidiPool, input: &Event) -> Option<PoolCommand> {
|
||||
//use KeyCode::{Up, Down, Delete, Char};
|
||||
//use PoolCommand as Cmd;
|
||||
//let index = state.clip_index();
|
||||
//let count = state.clips().len();
|
||||
//Some(match input {
|
||||
//kpat!(Char('n')) => Cmd::Rename(ClipRenameCommand::Begin),
|
||||
//kpat!(Char('t')) => Cmd::Length(ClipLengthCommand::Begin),
|
||||
//kpat!(Char('m')) => Cmd::Import(FileBrowserCommand::Begin),
|
||||
//kpat!(Char('x')) => Cmd::Export(FileBrowserCommand::Begin),
|
||||
//kpat!(Char('c')) => Cmd::Clip(PoolClipCommand::SetColor(index, ItemColor::random())),
|
||||
//kpat!(Char('[')) | kpat!(Up) => Cmd::Select(
|
||||
//index.overflowing_sub(1).0.min(state.clips().len() - 1)
|
||||
//),
|
||||
//kpat!(Char(']')) | kpat!(Down) => Cmd::Select(
|
||||
//index.saturating_add(1) % state.clips().len()
|
||||
//),
|
||||
//kpat!(Char('<')) => if index > 1 {
|
||||
//state.set_clip_index(state.clip_index().saturating_sub(1));
|
||||
//Cmd::Clip(PoolClipCommand::Swap(index - 1, index))
|
||||
//} else {
|
||||
//return None
|
||||
//},
|
||||
//kpat!(Char('>')) => if index < count.saturating_sub(1) {
|
||||
//state.set_clip_index(state.clip_index() + 1);
|
||||
//Cmd::Clip(PoolClipCommand::Swap(index + 1, index))
|
||||
//} else {
|
||||
//return None
|
||||
//},
|
||||
//kpat!(Delete) => if index > 0 {
|
||||
//state.set_clip_index(index.min(count.saturating_sub(1)));
|
||||
//Cmd::Clip(PoolClipCommand::Delete(index))
|
||||
//} else {
|
||||
//return None
|
||||
//},
|
||||
//kpat!(Char('a')) | kpat!(Shift-Char('A')) => Cmd::Clip(PoolClipCommand::Add(count, MidiClip::new(
|
||||
//"Clip", true, 4 * PPQ, None, Some(ItemPalette::random())
|
||||
//))),
|
||||
//kpat!(Char('i')) => Cmd::Clip(PoolClipCommand::Add(index + 1, MidiClip::new(
|
||||
//"Clip", true, 4 * PPQ, None, Some(ItemPalette::random())
|
||||
//))),
|
||||
//kpat!(Char('d')) | kpat!(Shift-Char('D')) => {
|
||||
//let mut clip = state.clips()[index].read().unwrap().duplicate();
|
||||
//clip.color = ItemPalette::random_near(clip.color, 0.25);
|
||||
//Cmd::Clip(PoolClipCommand::Add(index + 1, clip))
|
||||
//},
|
||||
//_ => return None
|
||||
//})
|
||||
//}
|
||||
18
crates/midi/src/mode.rs
Normal file
18
crates/midi/src/mode.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
use crate::*;
|
||||
|
||||
mod mode_browse; pub use self::mode_browse::*;
|
||||
mod mode_length; pub use self::mode_length::*;
|
||||
mod mode_rename; pub use self::mode_rename::*;
|
||||
|
||||
/// Modes for clip pool
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PoolMode {
|
||||
/// Renaming a pattern
|
||||
Rename(usize, Arc<str>),
|
||||
/// Editing the length of a pattern
|
||||
Length(usize, usize, ClipLengthFocus),
|
||||
/// Load clip from disk
|
||||
Import(usize, FileBrowser),
|
||||
/// Save clip to disk
|
||||
Export(usize, FileBrowser),
|
||||
}
|
||||
40
crates/midi/src/mode/mode_browse.rs
Normal file
40
crates/midi/src/mode/mode_browse.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
use crate::*;
|
||||
|
||||
atom_command!(FileBrowserCommand: |state: MidiPool| {
|
||||
("begin" [] Some(Self::Begin))
|
||||
("cancel" [] Some(Self::Cancel))
|
||||
("confirm" [] Some(Self::Confirm))
|
||||
("select" [i: usize] Some(Self::Select(i.expect("no index"))))
|
||||
("chdir" [p: PathBuf] Some(Self::Chdir(p.expect("no path"))))
|
||||
("filter" [f: Arc<str>] Some(Self::Filter(f.expect("no filter"))))
|
||||
});
|
||||
|
||||
command!(|self: FileBrowserCommand, state: MidiPool|{
|
||||
use PoolMode::*;
|
||||
use FileBrowserCommand::*;
|
||||
let mode = &mut state.mode;
|
||||
match mode {
|
||||
Some(Import(index, ref mut browser)) => match self {
|
||||
Cancel => { *mode = None; },
|
||||
Chdir(cwd) => { *mode = Some(Import(*index, FileBrowser::new(Some(cwd))?)); },
|
||||
Select(index) => { browser.index = index; },
|
||||
Confirm => if browser.is_file() {
|
||||
let index = *index;
|
||||
let path = browser.path();
|
||||
*mode = None;
|
||||
PoolClipCommand::Import(index, path).execute(state)?;
|
||||
} else if browser.is_dir() {
|
||||
*mode = Some(Import(*index, browser.chdir()?));
|
||||
},
|
||||
_ => todo!(),
|
||||
},
|
||||
Some(Export(index, ref mut browser)) => match self {
|
||||
Cancel => { *mode = None; },
|
||||
Chdir(cwd) => { *mode = Some(Export(*index, FileBrowser::new(Some(cwd))?)); },
|
||||
Select(index) => { browser.index = index; },
|
||||
_ => unreachable!()
|
||||
},
|
||||
_ => unreachable!(),
|
||||
};
|
||||
None
|
||||
});
|
||||
133
crates/midi/src/mode/mode_length.rs
Normal file
133
crates/midi/src/mode/mode_length.rs
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
use crate::*;
|
||||
|
||||
/// Displays and edits clip length.
|
||||
#[derive(Clone)]
|
||||
pub struct ClipLength {
|
||||
/// Pulses per beat (quaver)
|
||||
ppq: usize,
|
||||
/// Beats per bar
|
||||
bpb: usize,
|
||||
/// Length of clip in pulses
|
||||
pulses: usize,
|
||||
/// Selected subdivision
|
||||
focus: Option<ClipLengthFocus>,
|
||||
}
|
||||
|
||||
impl ClipLength {
|
||||
fn _new (pulses: usize, focus: Option<ClipLengthFocus>) -> Self {
|
||||
Self { ppq: PPQ, bpb: 4, pulses, focus }
|
||||
}
|
||||
fn bars (&self) -> usize {
|
||||
self.pulses / (self.bpb * self.ppq)
|
||||
}
|
||||
fn beats (&self) -> usize {
|
||||
(self.pulses % (self.bpb * self.ppq)) / self.ppq
|
||||
}
|
||||
fn ticks (&self) -> usize {
|
||||
self.pulses % self.ppq
|
||||
}
|
||||
fn bars_string (&self) -> Arc<str> {
|
||||
format!("{}", self.bars()).into()
|
||||
}
|
||||
fn beats_string (&self) -> Arc<str> {
|
||||
format!("{}", self.beats()).into()
|
||||
}
|
||||
fn ticks_string (&self) -> Arc<str> {
|
||||
format!("{:>02}", self.ticks()).into()
|
||||
}
|
||||
}
|
||||
|
||||
content!(TuiOut: |self: ClipLength| {
|
||||
let bars = ||self.bars_string();
|
||||
let beats = ||self.beats_string();
|
||||
let ticks = ||self.ticks_string();
|
||||
match self.focus {
|
||||
None =>
|
||||
row!(" ", bars(), ".", beats(), ".", ticks()),
|
||||
Some(ClipLengthFocus::Bar) =>
|
||||
row!("[", bars(), "]", beats(), ".", ticks()),
|
||||
Some(ClipLengthFocus::Beat) =>
|
||||
row!(" ", bars(), "[", beats(), "]", ticks()),
|
||||
Some(ClipLengthFocus::Tick) =>
|
||||
row!(" ", bars(), ".", beats(), "[", ticks()),
|
||||
}
|
||||
});
|
||||
|
||||
/// Focused field of `ClipLength`
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum ClipLengthFocus {
|
||||
/// Editing the number of bars
|
||||
Bar,
|
||||
/// Editing the number of beats
|
||||
Beat,
|
||||
/// Editing the number of ticks
|
||||
Tick,
|
||||
}
|
||||
|
||||
impl ClipLengthFocus {
|
||||
fn next (&mut self) {
|
||||
*self = match self { Self::Bar => Self::Beat, Self::Beat => Self::Tick, Self::Tick => Self::Bar, }
|
||||
}
|
||||
fn prev (&mut self) {
|
||||
*self = match self { Self::Bar => Self::Tick, Self::Beat => Self::Bar, Self::Tick => Self::Beat, }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub enum ClipLengthCommand {
|
||||
Begin,
|
||||
Cancel,
|
||||
Set(usize),
|
||||
Next,
|
||||
Prev,
|
||||
Inc,
|
||||
Dec,
|
||||
}
|
||||
|
||||
atom_command!(ClipLengthCommand: |state: MidiPool| {
|
||||
("begin" [] Some(Self::Begin))
|
||||
("cancel" [] Some(Self::Cancel))
|
||||
("next" [] Some(Self::Next))
|
||||
("prev" [] Some(Self::Prev))
|
||||
("inc" [] Some(Self::Inc))
|
||||
("dec" [] Some(Self::Dec))
|
||||
("set" [l: usize] Some(Self::Set(l.expect("no length"))))
|
||||
});
|
||||
|
||||
command!(|self: ClipLengthCommand, state: MidiPool|{
|
||||
use ClipLengthCommand::*;
|
||||
use ClipLengthFocus::*;
|
||||
if let Some(
|
||||
PoolMode::Length(clip, ref mut length, ref mut focus)
|
||||
) = state.mode_mut().clone() {
|
||||
match self {
|
||||
Cancel => { *state.mode_mut() = None; },
|
||||
Prev => { focus.prev() },
|
||||
Next => { focus.next() },
|
||||
Inc => match focus {
|
||||
Bar => { *length += 4 * PPQ },
|
||||
Beat => { *length += PPQ },
|
||||
Tick => { *length += 1 },
|
||||
},
|
||||
Dec => match focus {
|
||||
Bar => { *length = length.saturating_sub(4 * PPQ) },
|
||||
Beat => { *length = length.saturating_sub(PPQ) },
|
||||
Tick => { *length = length.saturating_sub(1) },
|
||||
},
|
||||
Set(length) => {
|
||||
let old_length;
|
||||
{
|
||||
let clip = state.clips()[clip].clone();//.write().unwrap();
|
||||
old_length = Some(clip.read().unwrap().length);
|
||||
clip.write().unwrap().length = length;
|
||||
}
|
||||
*state.mode_mut() = None;
|
||||
return Ok(old_length.map(Self::Set))
|
||||
},
|
||||
_ => unreachable!()
|
||||
}
|
||||
} else {
|
||||
unreachable!();
|
||||
}
|
||||
None
|
||||
});
|
||||
38
crates/midi/src/mode/mode_rename.rs
Normal file
38
crates/midi/src/mode/mode_rename.rs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)] pub enum ClipRenameCommand {
|
||||
Begin,
|
||||
Cancel,
|
||||
Confirm,
|
||||
Set(Arc<str>),
|
||||
}
|
||||
|
||||
atom_command!(ClipRenameCommand: |state: MidiPool| {
|
||||
("begin" [] Some(Self::Begin))
|
||||
("cancel" [] Some(Self::Cancel))
|
||||
("confirm" [] Some(Self::Confirm))
|
||||
("set" [n: Arc<str>] Some(Self::Set(n.expect("no name"))))
|
||||
});
|
||||
|
||||
command!(|self: ClipRenameCommand, state: MidiPool|if let Some(
|
||||
PoolMode::Rename(clip, ref mut old_name)
|
||||
) = state.mode_mut().clone() {
|
||||
match self {
|
||||
Self::Set(s) => {
|
||||
state.clips()[clip].write().unwrap().name = s;
|
||||
return Ok(Some(Self::Set(old_name.clone().into())))
|
||||
},
|
||||
Self::Confirm => {
|
||||
let old_name = old_name.clone();
|
||||
*state.mode_mut() = None;
|
||||
return Ok(Some(Self::Set(old_name)))
|
||||
},
|
||||
Self::Cancel => {
|
||||
state.clips()[clip].write().unwrap().name = old_name.clone().into();
|
||||
return Ok(None)
|
||||
},
|
||||
_ => unreachable!()
|
||||
}
|
||||
} else {
|
||||
unreachable!()
|
||||
});
|
||||
3
crates/midi/src/note.rs
Normal file
3
crates/midi/src/note.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
mod note_pitch; pub use self::note_pitch::*;
|
||||
mod note_point; pub use self::note_point::*;
|
||||
mod note_range; pub use self::note_range::*;
|
||||
2
crates/midi/src/piano.rs
Normal file
2
crates/midi/src/piano.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
mod piano_h; pub use self::piano_h::*;
|
||||
mod piano_v; pub use self::piano_v::*;
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
use crate::*;
|
||||
use Color::*;
|
||||
|
||||
/// A clip, rendered as a horizontal piano roll.
|
||||
pub struct PianoHorizontal {
|
||||
pub clip: Option<Arc<RwLock<MidiClip>>>,
|
||||
|
|
@ -16,6 +17,7 @@ pub struct PianoHorizontal {
|
|||
/// Width of the keyboard
|
||||
pub keys_width: u16,
|
||||
}
|
||||
|
||||
impl PianoHorizontal {
|
||||
pub fn new (clip: Option<&Arc<RwLock<MidiClip>>>) -> Self {
|
||||
let size = Measure::new();
|
||||
|
|
@ -35,9 +37,11 @@ impl PianoHorizontal {
|
|||
piano
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn note_y_iter (note_lo: usize, note_hi: usize, y0: u16) -> impl Iterator<Item=(usize, u16, usize)> {
|
||||
(note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n))
|
||||
}
|
||||
|
||||
content!(TuiOut:|self: PianoHorizontal| Tui::bg(Tui::g(40), Bsp::s(
|
||||
Bsp::e(
|
||||
Fixed::x(5, format!("{}x{}", self.size.w(), self.size.h())),
|
||||
|
|
@ -230,20 +234,24 @@ impl TimeRange for PianoHorizontal {
|
|||
fn time_start (&self) -> &AtomicUsize { self.range.time_start() }
|
||||
fn time_axis (&self) -> &AtomicUsize { self.range.time_axis() }
|
||||
}
|
||||
|
||||
impl NoteRange for PianoHorizontal {
|
||||
fn note_lo (&self) -> &AtomicUsize { self.range.note_lo() }
|
||||
fn note_axis (&self) -> &AtomicUsize { self.range.note_axis() }
|
||||
}
|
||||
|
||||
impl NotePoint for PianoHorizontal {
|
||||
fn note_len (&self) -> usize { self.point.note_len() }
|
||||
fn set_note_len (&self, x: usize) { self.point.set_note_len(x) }
|
||||
fn note_pos (&self) -> usize { self.point.note_pos() }
|
||||
fn set_note_pos (&self, x: usize) { self.point.set_note_pos(x) }
|
||||
}
|
||||
|
||||
impl TimePoint for PianoHorizontal {
|
||||
fn time_pos (&self) -> usize { self.point.time_pos() }
|
||||
fn set_time_pos (&self, x: usize) { self.point.set_time_pos(x) }
|
||||
}
|
||||
|
||||
impl MidiViewer for PianoHorizontal {
|
||||
fn clip (&self) -> &Option<Arc<RwLock<MidiClip>>> {
|
||||
&self.clip
|
||||
4
crates/midi/src/pool.rs
Normal file
4
crates/midi/src/pool.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
mod pool_api; pub use self::pool_api::*;
|
||||
mod pool_clips; pub use self::pool_clips::*;
|
||||
mod pool_model; pub use self::pool_model::*;
|
||||
mod pool_view; pub use self::pool_view::*;
|
||||
177
crates/midi/src/pool/pool_api.rs
Normal file
177
crates/midi/src/pool/pool_api.rs
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
use crate::*;
|
||||
|
||||
handle!(TuiIn: |self: MidiPool, input|{
|
||||
Ok(if let Some(command) = match self.mode() {
|
||||
Some(PoolMode::Rename(..)) => self.keys_rename,
|
||||
Some(PoolMode::Length(..)) => self.keys_length,
|
||||
Some(PoolMode::Import(..)) | Some(PoolMode::Export(..)) => self.keys_file,
|
||||
_ => self.keys
|
||||
}.command::<Self, PoolCommand, TuiIn>(self, input) {
|
||||
let _undo = command.execute(self)?;
|
||||
Some(true)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
});
|
||||
|
||||
provide!(bool: |self: MidiPool| {});
|
||||
|
||||
provide!(MidiClip: |self: MidiPool| {
|
||||
":new-clip" => self.new_clip(),
|
||||
":cloned-clip" => self.cloned_clip(),
|
||||
});
|
||||
|
||||
provide!(PathBuf: |self: MidiPool| {});
|
||||
|
||||
provide!(Arc<str>: |self: MidiPool| {});
|
||||
|
||||
provide!(usize: |self: MidiPool| {
|
||||
":current" => 0,
|
||||
":after" => 0,
|
||||
":previous" => 0,
|
||||
":next" => 0
|
||||
});
|
||||
|
||||
provide!(ItemColor: |self: MidiPool| {
|
||||
":random-color" => ItemColor::random()
|
||||
});
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)] pub enum PoolCommand {
|
||||
/// Toggle visibility of pool
|
||||
Show(bool),
|
||||
/// Select a clip from the clip pool
|
||||
Select(usize),
|
||||
/// Rename a clip
|
||||
Rename(ClipRenameCommand),
|
||||
/// Change the length of a clip
|
||||
Length(ClipLengthCommand),
|
||||
/// Import from file
|
||||
Import(FileBrowserCommand),
|
||||
/// Export to file
|
||||
Export(FileBrowserCommand),
|
||||
/// Update the contents of the clip pool
|
||||
Clip(PoolClipCommand),
|
||||
}
|
||||
|
||||
atom_command!(PoolCommand: |state: MidiPool| {
|
||||
("show" [a: bool] Some(Self::Show(a.expect("no flag"))))
|
||||
("select" [i: usize] Some(Self::Select(i.expect("no index"))))
|
||||
("rename" [,..a] ClipRenameCommand::try_from_expr(state, a).map(Self::Rename))
|
||||
("length" [,..a] ClipLengthCommand::try_from_expr(state, a).map(Self::Length))
|
||||
("import" [,..a] FileBrowserCommand::try_from_expr(state, a).map(Self::Import))
|
||||
("export" [,..a] FileBrowserCommand::try_from_expr(state, a).map(Self::Export))
|
||||
("clip" [,..a] PoolClipCommand::try_from_expr(state, a).map(Self::Clip))
|
||||
});
|
||||
|
||||
command!(|self: PoolCommand, state: MidiPool|{
|
||||
use PoolCommand::*;
|
||||
match self {
|
||||
Rename(ClipRenameCommand::Begin) => { state.begin_clip_rename(); None }
|
||||
Rename(command) => command.delegate(state, Rename)?,
|
||||
Length(ClipLengthCommand::Begin) => { state.begin_clip_length(); None },
|
||||
Length(command) => command.delegate(state, Length)?,
|
||||
Import(FileBrowserCommand::Begin) => { state.begin_import()?; None },
|
||||
Import(command) => command.delegate(state, Import)?,
|
||||
Export(FileBrowserCommand::Begin) => { state.begin_export()?; None },
|
||||
Export(command) => command.delegate(state, Export)?,
|
||||
Clip(command) => command.execute(state)?.map(Clip),
|
||||
Show(visible) => { state.visible = visible; Some(Self::Show(!visible)) },
|
||||
Select(clip) => { state.set_clip_index(clip); None },
|
||||
}
|
||||
});
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)] pub enum PoolClipCommand {
|
||||
Add(usize, MidiClip),
|
||||
Delete(usize),
|
||||
Swap(usize, usize),
|
||||
Import(usize, PathBuf),
|
||||
Export(usize, PathBuf),
|
||||
SetName(usize, Arc<str>),
|
||||
SetLength(usize, usize),
|
||||
SetColor(usize, ItemColor),
|
||||
}
|
||||
|
||||
atom_command!(PoolClipCommand: |state: MidiPool| {
|
||||
("add" [i: usize, c: MidiClip]
|
||||
Some(Self::Add(i.expect("no index"), c.expect("no clip"))))
|
||||
("delete" [i: usize]
|
||||
Some(Self::Delete(i.expect("no index"))))
|
||||
("swap" [a: usize, b: usize]
|
||||
Some(Self::Swap(a.expect("no index"), b.expect("no index"))))
|
||||
("import" [i: usize, p: PathBuf]
|
||||
Some(Self::Import(i.expect("no index"), p.expect("no path"))))
|
||||
("export" [i: usize, p: PathBuf]
|
||||
Some(Self::Export(i.expect("no index"), p.expect("no path"))))
|
||||
("set-name" [i: usize, n: Arc<str>]
|
||||
Some(Self::SetName(i.expect("no index"), n.expect("no name"))))
|
||||
("set-length" [i: usize, l: usize]
|
||||
Some(Self::SetLength(i.expect("no index"), l.expect("no length"))))
|
||||
("set-color" [i: usize, c: ItemColor]
|
||||
Some(Self::SetColor(i.expect("no index"), c.expect("no color"))))
|
||||
});
|
||||
|
||||
impl<T: HasClips> Command<T> for PoolClipCommand {
|
||||
fn execute (self, model: &mut T) -> Perhaps<Self> {
|
||||
use PoolClipCommand::*;
|
||||
Ok(match self {
|
||||
Add(mut index, clip) => {
|
||||
let clip = Arc::new(RwLock::new(clip));
|
||||
let mut clips = model.clips_mut();
|
||||
if index >= clips.len() {
|
||||
index = clips.len();
|
||||
clips.push(clip)
|
||||
} else {
|
||||
clips.insert(index, clip);
|
||||
}
|
||||
Some(Self::Delete(index))
|
||||
},
|
||||
Delete(index) => {
|
||||
let clip = model.clips_mut().remove(index).read().unwrap().clone();
|
||||
Some(Self::Add(index, clip))
|
||||
},
|
||||
Swap(index, other) => {
|
||||
model.clips_mut().swap(index, other);
|
||||
Some(Self::Swap(index, other))
|
||||
},
|
||||
Import(index, path) => {
|
||||
let bytes = std::fs::read(&path)?;
|
||||
let smf = Smf::parse(bytes.as_slice())?;
|
||||
let mut t = 0u32;
|
||||
let mut events = vec![];
|
||||
for track in smf.tracks.iter() {
|
||||
for event in track.iter() {
|
||||
t += event.delta.as_int();
|
||||
if let TrackEventKind::Midi { channel, message } = event.kind {
|
||||
events.push((t, channel.as_int(), message));
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut clip = MidiClip::new("imported", true, t as usize + 1, None, None);
|
||||
for event in events.iter() {
|
||||
clip.notes[event.0 as usize].push(event.2);
|
||||
}
|
||||
Self::Add(index, clip).execute(model)?
|
||||
},
|
||||
Export(_index, _path) => {
|
||||
todo!("export clip to midi file");
|
||||
},
|
||||
SetName(index, name) => {
|
||||
let clip = &mut model.clips_mut()[index];
|
||||
let old_name = clip.read().unwrap().name.clone();
|
||||
clip.write().unwrap().name = name;
|
||||
Some(Self::SetName(index, old_name))
|
||||
},
|
||||
SetLength(index, length) => {
|
||||
let clip = &mut model.clips_mut()[index];
|
||||
let old_len = clip.read().unwrap().length;
|
||||
clip.write().unwrap().length = length;
|
||||
Some(Self::SetLength(index, old_len))
|
||||
},
|
||||
SetColor(index, color) => {
|
||||
let mut color = ItemPalette::from(color);
|
||||
std::mem::swap(&mut color, &mut model.clips()[index].write().unwrap().color);
|
||||
Some(Self::SetColor(index, color.base))
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
26
crates/midi/src/pool/pool_clips.rs
Normal file
26
crates/midi/src/pool/pool_clips.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
use crate::*;
|
||||
|
||||
pub type ClipPool = Vec<Arc<RwLock<MidiClip>>>;
|
||||
|
||||
pub trait HasClips {
|
||||
fn clips <'a> (&'a self) -> std::sync::RwLockReadGuard<'a, ClipPool>;
|
||||
fn clips_mut <'a> (&'a self) -> std::sync::RwLockWriteGuard<'a, ClipPool>;
|
||||
fn add_clip (&self) -> (usize, Arc<RwLock<MidiClip>>) {
|
||||
let clip = Arc::new(RwLock::new(MidiClip::new("Clip", true, 384, None, None)));
|
||||
self.clips_mut().push(clip.clone());
|
||||
(self.clips().len() - 1, clip)
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! has_clips {
|
||||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||||
impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? {
|
||||
fn clips <'a> (&'a $self) -> std::sync::RwLockReadGuard<'a, ClipPool> {
|
||||
$cb.read().unwrap()
|
||||
}
|
||||
fn clips_mut <'a> (&'a $self) -> std::sync::RwLockWriteGuard<'a, ClipPool> {
|
||||
$cb.write().unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
106
crates/midi/src/pool/pool_model.rs
Normal file
106
crates/midi/src/pool/pool_model.rs
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MidiPool {
|
||||
pub visible: bool,
|
||||
/// Collection of clips
|
||||
pub clips: Arc<RwLock<Vec<Arc<RwLock<MidiClip>>>>>,
|
||||
/// Selected clip
|
||||
pub clip: AtomicUsize,
|
||||
/// Mode switch
|
||||
pub mode: Option<PoolMode>,
|
||||
|
||||
pub keys: SourceIter<'static>,
|
||||
pub keys_rename: SourceIter<'static>,
|
||||
pub keys_length: SourceIter<'static>,
|
||||
pub keys_file: SourceIter<'static>,
|
||||
}
|
||||
|
||||
impl Default for MidiPool {
|
||||
fn default () -> Self {
|
||||
Self {
|
||||
visible: true,
|
||||
clips: Arc::from(RwLock::from(vec![])),
|
||||
clip: 0.into(),
|
||||
mode: None,
|
||||
keys: SourceIter(include_str!("../../edn/keys_pool.edn")),
|
||||
keys_file: SourceIter(include_str!("../../edn/keys_pool_file.edn")),
|
||||
keys_rename: SourceIter(include_str!("../../edn/keys_clip_rename.edn")),
|
||||
keys_length: SourceIter(include_str!("../../edn/keys_clip_length.edn")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
has_clips!(|self: MidiPool|self.clips);
|
||||
|
||||
has_clip!(|self: MidiPool|self.clips().get(self.clip_index()).map(|c|c.clone()));
|
||||
|
||||
from!(|clip:&Arc<RwLock<MidiClip>>|MidiPool = {
|
||||
let model = Self::default();
|
||||
model.clips.write().unwrap().push(clip.clone());
|
||||
model.clip.store(1, Relaxed);
|
||||
model
|
||||
});
|
||||
|
||||
impl MidiPool {
|
||||
pub fn clip_index (&self) -> usize {
|
||||
self.clip.load(Relaxed)
|
||||
}
|
||||
pub fn set_clip_index (&self, value: usize) {
|
||||
self.clip.store(value, Relaxed);
|
||||
}
|
||||
pub fn mode (&self) -> &Option<PoolMode> {
|
||||
&self.mode
|
||||
}
|
||||
pub fn mode_mut (&mut self) -> &mut Option<PoolMode> {
|
||||
&mut self.mode
|
||||
}
|
||||
pub fn begin_clip_length (&mut self) {
|
||||
let length = self.clips()[self.clip_index()].read().unwrap().length;
|
||||
*self.mode_mut() = Some(PoolMode::Length(
|
||||
self.clip_index(),
|
||||
length,
|
||||
ClipLengthFocus::Bar
|
||||
));
|
||||
}
|
||||
pub fn begin_clip_rename (&mut self) {
|
||||
let name = self.clips()[self.clip_index()].read().unwrap().name.clone();
|
||||
*self.mode_mut() = Some(PoolMode::Rename(
|
||||
self.clip_index(),
|
||||
name
|
||||
));
|
||||
}
|
||||
pub fn begin_import (&mut self) -> Usually<()> {
|
||||
*self.mode_mut() = Some(PoolMode::Import(
|
||||
self.clip_index(),
|
||||
FileBrowser::new(None)?
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
pub fn begin_export (&mut self) -> Usually<()> {
|
||||
*self.mode_mut() = Some(PoolMode::Export(
|
||||
self.clip_index(),
|
||||
FileBrowser::new(None)?
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
pub fn new_clip (&self) -> MidiClip {
|
||||
MidiClip::new("Clip", true, 4 * PPQ, None, Some(ItemPalette::random()))
|
||||
}
|
||||
pub fn cloned_clip (&self) -> MidiClip {
|
||||
let index = self.clip_index();
|
||||
let mut clip = self.clips()[index].read().unwrap().duplicate();
|
||||
clip.color = ItemPalette::random_near(clip.color, 0.25);
|
||||
clip
|
||||
}
|
||||
pub fn add_new_clip (&self) -> (usize, Arc<RwLock<MidiClip>>) {
|
||||
let clip = Arc::new(RwLock::new(self.new_clip()));
|
||||
let index = {
|
||||
let mut clips = self.clips.write().unwrap();
|
||||
clips.push(clip.clone());
|
||||
clips.len().saturating_sub(1)
|
||||
};
|
||||
self.clip.store(index, Relaxed);
|
||||
(index, clip)
|
||||
}
|
||||
}
|
||||
29
crates/midi/src/pool/pool_view.rs
Normal file
29
crates/midi/src/pool/pool_view.rs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
use crate::*;
|
||||
|
||||
pub struct PoolView<'a>(pub bool, pub &'a MidiPool);
|
||||
|
||||
content!(TuiOut: |self: PoolView<'a>| {
|
||||
let Self(compact, model) = self;
|
||||
let MidiPool { clips, .. } = self.1;
|
||||
//let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||Tui::g(32).into());
|
||||
let on_bg = |x|x;//Bsp::b(Repeat(" "), Tui::bg(color.darkest.rgb, x));
|
||||
let border = |x|x;//Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)).enclose(x);
|
||||
let iter = | |model.clips().clone().into_iter();
|
||||
let height = clips.read().unwrap().len() as u16;
|
||||
Tui::bg(Color::Reset, Fixed::y(height, on_bg(border(Map::new(iter, move|clip: Arc<RwLock<MidiClip>>, i|{
|
||||
let item_height = 1;
|
||||
let item_offset = i as u16 * item_height;
|
||||
let selected = i == model.clip_index();
|
||||
let MidiClip { ref name, color, length, .. } = *clip.read().unwrap();
|
||||
let bg = if selected { color.light.rgb } else { color.base.rgb };
|
||||
let fg = color.lightest.rgb;
|
||||
let name = if *compact { format!(" {i:>3}") } else { format!(" {i:>3} {name}") };
|
||||
let length = if *compact { String::default() } else { format!("{length} ") };
|
||||
Fixed::y(1, map_south(item_offset, item_height, Tui::bg(bg, lay!(
|
||||
Fill::x(Align::w(Tui::fg(fg, Tui::bold(selected, name)))),
|
||||
Fill::x(Align::e(Tui::fg(fg, Tui::bold(selected, length)))),
|
||||
Fill::x(Align::w(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "▶"))))),
|
||||
Fill::x(Align::e(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "◀"))))),
|
||||
))))
|
||||
})))))
|
||||
});
|
||||
31
crates/midi/src/port.rs
Normal file
31
crates/midi/src/port.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
use crate::*;
|
||||
|
||||
mod port_in; pub use self::port_in::*;
|
||||
mod port_out; pub use self::port_out::*;
|
||||
|
||||
/// Update notes_in array
|
||||
pub fn update_keys (keys: &mut[bool;128], message: &MidiMessage) {
|
||||
match message {
|
||||
MidiMessage::NoteOn { key, .. } => { keys[key.as_int() as usize] = true; }
|
||||
MidiMessage::NoteOff { key, .. } => { keys[key.as_int() as usize] = false; },
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return boxed iterator of MIDI events
|
||||
pub fn parse_midi_input <'a> (input: MidiIter<'a>) -> Box<dyn Iterator<Item=(usize, LiveEvent<'a>, &'a [u8])> + 'a> {
|
||||
Box::new(input.map(|RawMidi { time, bytes }|(
|
||||
time as usize,
|
||||
LiveEvent::parse(bytes).unwrap(),
|
||||
bytes
|
||||
)))
|
||||
}
|
||||
|
||||
/// Add "all notes off" to the start of a buffer.
|
||||
pub fn all_notes_off (output: &mut [Vec<Vec<u8>>]) {
|
||||
let mut buf = vec![];
|
||||
let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() };
|
||||
let evt = LiveEvent::Midi { channel: 0.into(), message: msg };
|
||||
evt.write(&mut buf).unwrap();
|
||||
output[0].push(buf);
|
||||
}
|
||||
|
|
@ -3,25 +3,41 @@ use crate::*;
|
|||
/// Trait for thing that may receive MIDI.
|
||||
pub trait HasMidiIns {
|
||||
fn midi_ins (&self) -> &Vec<JackMidiIn>;
|
||||
|
||||
fn midi_ins_mut (&mut self) -> &mut Vec<JackMidiIn>;
|
||||
|
||||
fn has_midi_ins (&self) -> bool {
|
||||
!self.midi_ins().is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait MidiRecordApi: HasClock + HasPlayClip + HasMidiIns {
|
||||
fn notes_in (&self) -> &Arc<RwLock<[bool;128]>>;
|
||||
fn notes_in (&self) -> &Arc<RwLock<[bool;128]>>;
|
||||
|
||||
fn recording (&self) -> bool;
|
||||
|
||||
fn recording_mut (&mut self) -> &mut bool;
|
||||
|
||||
fn toggle_record (&mut self) {
|
||||
*self.recording_mut() = !self.recording();
|
||||
}
|
||||
|
||||
fn monitoring (&self) -> bool;
|
||||
|
||||
fn monitoring_mut (&mut self) -> &mut bool;
|
||||
|
||||
fn toggle_monitor (&mut self) {
|
||||
*self.monitoring_mut() = !self.monitoring();
|
||||
}
|
||||
|
||||
fn overdub (&self) -> bool;
|
||||
|
||||
fn overdub_mut (&mut self) -> &mut bool;
|
||||
|
||||
fn toggle_overdub (&mut self) {
|
||||
*self.overdub_mut() = !self.overdub();
|
||||
}
|
||||
|
||||
fn monitor (&mut self, scope: &ProcessScope, midi_buf: &mut Vec<Vec<Vec<u8>>>) {
|
||||
// For highlighting keys and note repeat
|
||||
let notes_in = self.notes_in().clone();
|
||||
|
|
@ -32,11 +48,13 @@ pub trait MidiRecordApi: HasClock + HasPlayClip + HasMidiIns {
|
|||
if monitoring {
|
||||
midi_buf[sample].push(bytes.to_vec());
|
||||
}
|
||||
// FIXME: don't lock on every event!
|
||||
update_keys(&mut notes_in.write().unwrap(), &message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn record (&mut self, scope: &ProcessScope, midi_buf: &mut Vec<Vec<Vec<u8>>>) {
|
||||
if self.monitoring() {
|
||||
self.monitor(scope, midi_buf);
|
||||
|
|
@ -51,6 +69,7 @@ pub trait MidiRecordApi: HasClock + HasPlayClip + HasMidiIns {
|
|||
self.record_next();
|
||||
}
|
||||
}
|
||||
|
||||
fn record_clip (
|
||||
&mut self,
|
||||
scope: &ProcessScope,
|
||||
|
|
@ -80,12 +99,9 @@ pub trait MidiRecordApi: HasClock + HasPlayClip + HasMidiIns {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn record_next (&mut self) {
|
||||
// TODO switch to next clip and record into it
|
||||
}
|
||||
fn overdub (&self) -> bool;
|
||||
fn overdub_mut (&mut self) -> &mut bool;
|
||||
fn toggle_overdub (&mut self) {
|
||||
*self.overdub_mut() = !self.overdub();
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue