mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 03:36:41 +01:00
This commit is contained in:
parent
77703d83a5
commit
22d3cc5a5f
49 changed files with 1332 additions and 1316 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1540,6 +1540,7 @@ dependencies = [
|
|||
"symphonia",
|
||||
"tek_engine",
|
||||
"tengri",
|
||||
"uuid",
|
||||
"wavers",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ impose!([app: Tek]
|
|||
|
||||
(SceneCommand:
|
||||
("add" [] Some(Self::Add))
|
||||
("delete" [a: Option<usize>] Some(Self::Del(a.flatten().unwrap())))
|
||||
("delete" [a: Option<usize>] Some(Self::Del(a.flatten().unwrap())))
|
||||
("zoom" [a: usize] Some(Self::SetZoom(a.unwrap())))
|
||||
("color" [a: usize] Some(Self::SetColor(a.unwrap(), ItemTheme::G[128])))
|
||||
("enqueue" [a: usize] Some(Self::Enqueue(a.unwrap())))
|
||||
|
|
|
|||
3
crates/app/src/editor.rs
Normal file
3
crates/app/src/editor.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
mod editor_api;
|
||||
mod editor_model;
|
||||
mod editor_view;
|
||||
97
crates/app/src/editor/editor_api.rs
Normal file
97
crates/app/src/editor/editor_api.rs
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
use crate::*;
|
||||
|
||||
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(),
|
||||
|
||||
":note-pos" => self.note_pos(),
|
||||
":note-pos-next" => self.note_pos() + 1,
|
||||
":note-pos-prev" => self.note_pos().saturating_sub(1),
|
||||
":note-pos-next-octave" => self.note_pos() + 12,
|
||||
":note-pos-prev-octave" => self.note_pos().saturating_sub(12),
|
||||
|
||||
":note-len" => self.note_len(),
|
||||
":note-len-next" => self.note_len() + 1,
|
||||
":note-len-prev" => self.note_len().saturating_sub(1),
|
||||
|
||||
":note-range" => self.note_axis().get(),
|
||||
":note-range-prev" => self.note_axis().get() + 1,
|
||||
":note-range-next" => self.note_axis().get().saturating_sub(1),
|
||||
|
||||
":time-pos" => self.time_pos(),
|
||||
":time-pos-next" => self.time_pos() + self.time_zoom().get(),
|
||||
":time-pos-prev" => self.time_pos().saturating_sub(self.time_zoom().get()),
|
||||
|
||||
":time-zoom" => self.time_zoom().get(),
|
||||
":time-zoom-next" => self.time_zoom().get() + 1,
|
||||
":time-zoom-prev" => self.time_zoom().get().saturating_sub(1).max(1),
|
||||
});
|
||||
|
||||
atom_command!(MidiEditCommand: |state: MidiEditor| {
|
||||
("note/append" [] Some(Self::AppendNote))
|
||||
("note/put" [] Some(Self::PutNote))
|
||||
("note/del" [] Some(Self::DelNote))
|
||||
("note/pos" [a: usize] Some(Self::SetNoteCursor(a.expect("no note cursor"))))
|
||||
("note/len" [a: usize] Some(Self::SetNoteLength(a.expect("no note length"))))
|
||||
("time/pos" [a: usize] Some(Self::SetTimeCursor(a.expect("no time cursor"))))
|
||||
("time/zoom" [a: usize] Some(Self::SetTimeZoom(a.expect("no time zoom"))))
|
||||
("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,
|
||||
PutNote,
|
||||
DelNote,
|
||||
SetNoteCursor(usize),
|
||||
SetNoteLength(usize),
|
||||
SetNoteScroll(usize),
|
||||
SetTimeCursor(usize),
|
||||
SetTimeScroll(usize),
|
||||
SetTimeZoom(usize),
|
||||
SetTimeLock(bool),
|
||||
Show(Option<Arc<RwLock<MidiClip>>>),
|
||||
}
|
||||
|
||||
handle!(TuiIn: |self: MidiEditor, input|Ok(if let Some(command) = self.keys.command(self, input) {
|
||||
command.execute(self)?;
|
||||
Some(true)
|
||||
} else {
|
||||
None
|
||||
}));
|
||||
|
||||
impl Command<MidiEditor> for MidiEditCommand {
|
||||
fn execute (self, state: &mut MidiEditor) -> Perhaps<Self> {
|
||||
use MidiEditCommand::*;
|
||||
match self {
|
||||
Show(clip) => { state.set_clip(clip.as_ref()); },
|
||||
DelNote => {},
|
||||
PutNote => { state.put_note(false); },
|
||||
AppendNote => { state.put_note(true); },
|
||||
SetTimeZoom(x) => { state.time_zoom().set(x); state.redraw(); },
|
||||
SetTimeLock(x) => { state.time_lock().set(x); },
|
||||
SetTimeScroll(x) => { state.time_start().set(x); },
|
||||
SetNoteScroll(x) => { state.note_lo().set(x.min(127)); },
|
||||
SetNoteLength(x) => {
|
||||
let note_len = state.note_len();
|
||||
let time_zoom = state.time_zoom().get();
|
||||
state.set_note_len(x);
|
||||
//if note_len / time_zoom != x / time_zoom {
|
||||
state.redraw();
|
||||
//}
|
||||
},
|
||||
SetTimeCursor(x) => { state.set_time_pos(x); },
|
||||
SetNoteCursor(note) => { state.set_note_pos(note.min(127)); },
|
||||
//_ => todo!("{:?}", self)
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -51,39 +51,6 @@ from!(|clip: Option<Arc<RwLock<MidiClip>>>|MidiEditor = {
|
|||
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(),
|
||||
|
||||
":note-pos" => self.note_pos(),
|
||||
":note-pos-next" => self.note_pos() + 1,
|
||||
":note-pos-prev" => self.note_pos().saturating_sub(1),
|
||||
":note-pos-next-octave" => self.note_pos() + 12,
|
||||
":note-pos-prev-octave" => self.note_pos().saturating_sub(12),
|
||||
|
||||
":note-len" => self.note_len(),
|
||||
":note-len-next" => self.note_len() + 1,
|
||||
":note-len-prev" => self.note_len().saturating_sub(1),
|
||||
|
||||
":note-range" => self.note_axis().get(),
|
||||
":note-range-prev" => self.note_axis().get() + 1,
|
||||
":note-range-next" => self.note_axis().get().saturating_sub(1),
|
||||
|
||||
":time-pos" => self.time_pos(),
|
||||
":time-pos-next" => self.time_pos() + self.time_zoom().get(),
|
||||
":time-pos-prev" => self.time_pos().saturating_sub(self.time_zoom().get()),
|
||||
|
||||
":time-zoom" => self.time_zoom().get(),
|
||||
":time-zoom-next" => self.time_zoom().get() + 1,
|
||||
":time-zoom-prev" => self.time_zoom().get().saturating_sub(1).max(1),
|
||||
});
|
||||
|
||||
impl MidiEditor {
|
||||
|
||||
/// Put note at current position
|
||||
|
|
@ -181,64 +148,32 @@ impl MidiViewer for MidiEditor {
|
|||
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))
|
||||
("note/del" [] Some(Self::DelNote))
|
||||
("note/pos" [a: usize] Some(Self::SetNoteCursor(a.expect("no note cursor"))))
|
||||
("note/len" [a: usize] Some(Self::SetNoteLength(a.expect("no note length"))))
|
||||
("time/pos" [a: usize] Some(Self::SetTimeCursor(a.expect("no time cursor"))))
|
||||
("time/zoom" [a: usize] Some(Self::SetTimeZoom(a.expect("no time zoom"))))
|
||||
("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,
|
||||
PutNote,
|
||||
DelNote,
|
||||
SetNoteCursor(usize),
|
||||
SetNoteLength(usize),
|
||||
SetNoteScroll(usize),
|
||||
SetTimeCursor(usize),
|
||||
SetTimeScroll(usize),
|
||||
SetTimeZoom(usize),
|
||||
SetTimeLock(bool),
|
||||
Show(Option<Arc<RwLock<MidiClip>>>),
|
||||
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 }
|
||||
}
|
||||
|
||||
handle!(TuiIn: |self: MidiEditor, input|Ok(if let Some(command) = self.keys.command(self, input) {
|
||||
command.execute(self)?;
|
||||
Some(true)
|
||||
} else {
|
||||
None
|
||||
}));
|
||||
|
||||
impl Command<MidiEditor> for MidiEditCommand {
|
||||
fn execute (self, state: &mut MidiEditor) -> Perhaps<Self> {
|
||||
use MidiEditCommand::*;
|
||||
match self {
|
||||
Show(clip) => { state.set_clip(clip.as_ref()); },
|
||||
DelNote => {},
|
||||
PutNote => { state.put_note(false); },
|
||||
AppendNote => { state.put_note(true); },
|
||||
SetTimeZoom(x) => { state.time_zoom().set(x); state.redraw(); },
|
||||
SetTimeLock(x) => { state.time_lock().set(x); },
|
||||
SetTimeScroll(x) => { state.time_start().set(x); },
|
||||
SetNoteScroll(x) => { state.note_lo().set(x.min(127)); },
|
||||
SetNoteLength(x) => {
|
||||
let note_len = state.note_len();
|
||||
let time_zoom = state.time_zoom().get();
|
||||
state.set_note_len(x);
|
||||
//if note_len / time_zoom != x / time_zoom {
|
||||
state.redraw();
|
||||
//}
|
||||
},
|
||||
SetTimeCursor(x) => { state.set_time_pos(x); },
|
||||
SetNoteCursor(note) => { state.set_note_pos(note.min(127)); },
|
||||
//_ => todo!("{:?}", self)
|
||||
#[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 }
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
};
|
||||
(|$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 }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -325,3 +325,39 @@ fn to_key (note: usize) -> &'static str {
|
|||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub struct OctaveVertical {
|
||||
on: [bool; 12],
|
||||
colors: [Color; 3]
|
||||
}
|
||||
|
||||
impl Default for OctaveVertical {
|
||||
fn default () -> Self {
|
||||
Self {
|
||||
on: [false; 12],
|
||||
colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OctaveVertical {
|
||||
fn color (&self, pitch: usize) -> Color {
|
||||
let pitch = pitch % 12;
|
||||
self.colors[if self.on[pitch] { 2 } else {
|
||||
match pitch { 0 | 2 | 4 | 5 | 6 | 8 | 10 => 0, _ => 1 }
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
impl Content<TuiOut> for OctaveVertical {
|
||||
fn content (&self) -> impl Render<TuiOut> {
|
||||
row!(
|
||||
Tui::fg_bg(self.color(0), self.color(1), "▙"),
|
||||
Tui::fg_bg(self.color(2), self.color(3), "▙"),
|
||||
Tui::fg_bg(self.color(4), self.color(5), "▌"),
|
||||
Tui::fg_bg(self.color(6), self.color(7), "▟"),
|
||||
Tui::fg_bg(self.color(8), self.color(9), "▟"),
|
||||
Tui::fg_bg(self.color(10), self.color(11), "▟"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -19,11 +19,8 @@
|
|||
pub type Usually<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
/// Standard optional result type.
|
||||
pub type Perhaps<T> = std::result::Result<Option<T>, Box<dyn std::error::Error>>;
|
||||
pub use ::tek_time::{self, *};
|
||||
pub use ::tek_jack::{self, *, jack::*};
|
||||
pub use ::tek_midi::{self, *, midly::{MidiMessage, num::*, live::*}};
|
||||
pub use ::tek_sampler::{self, *};
|
||||
#[cfg(feature = "host")] pub use ::tek_plugin::{self, *};
|
||||
pub use ::tek_engine:: *;
|
||||
pub use ::tek_device::{self, *};
|
||||
pub use ::tengri::dsl::*;
|
||||
pub use ::tengri::input::*;
|
||||
pub use ::tengri::output::*;
|
||||
|
|
@ -41,6 +38,9 @@ mod audio; pub use self::audio::*;
|
|||
mod model; pub use self::model::*;
|
||||
mod view; pub use self::view::*;
|
||||
|
||||
mod pool;
|
||||
mod editor;
|
||||
|
||||
#[cfg(test)] #[test] fn test_model () {
|
||||
let mut tek = Tek::default();
|
||||
let _ = tek.clip();
|
||||
|
|
|
|||
|
|
@ -787,11 +787,3 @@ impl HasTracks for Tek {
|
|||
fn tracks (&self) -> &Vec<Track> { &self.tracks }
|
||||
fn tracks_mut (&mut self) -> &mut Vec<Track> { &mut self.tracks }
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Device {
|
||||
Sequencer(MidiPlayer),
|
||||
Sampler(Sampler),
|
||||
#[cfg(feature="host")]
|
||||
Plugin(Plugin),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use crate::*;
|
||||
use super::*;
|
||||
|
||||
handle!(TuiIn: |self: MidiPool, input|{
|
||||
//Ok(if let Some(command) = match self.mode() {
|
||||
|
|
@ -176,3 +177,138 @@ impl<T: HasClips> Command<T> for PoolClipCommand {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[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!()
|
||||
});
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
#[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
|
||||
});
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
use crate::*;
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MidiPool {
|
||||
|
|
@ -117,3 +118,73 @@ impl MidiPool {
|
|||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// 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, }
|
||||
}
|
||||
}
|
||||
|
||||
/// 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),
|
||||
}
|
||||
|
|
@ -27,3 +27,19 @@ content!(TuiOut: |self: PoolView<'a>| {
|
|||
))))
|
||||
})))))
|
||||
});
|
||||
|
||||
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()),
|
||||
}
|
||||
});
|
||||
|
|
@ -6,12 +6,14 @@ version = { workspace = true }
|
|||
[dependencies]
|
||||
tengri = { workspace = true }
|
||||
tek_engine = { workspace = true }
|
||||
uuid = { workspace = true, optional = true }
|
||||
livi = { workspace = true, optional = true }
|
||||
symphonia = { workspace = true, optional = true }
|
||||
wavers = { workspace = true, optional = true }
|
||||
|
||||
[features]
|
||||
default = [ "sequencer", "sampler" ]
|
||||
default = [ "clock", "sequencer", "sampler" ]
|
||||
lv2 = [ "livi" ]
|
||||
sampler = [ "symphonia", "wavers" ]
|
||||
sequencer = []
|
||||
sequencer = [ "clock", "uuid" ]
|
||||
clock = []
|
||||
|
|
|
|||
|
|
@ -17,11 +17,11 @@ provide_num!(u32: |self: Clock| {});
|
|||
provide!(f64: |self: Clock| {});
|
||||
|
||||
atom_command!(ClockCommand: |state: Clock| {
|
||||
("play" [] Some(Self::Play(None)))
|
||||
("play" [] Some(Self::Play(None)))
|
||||
("play" [t: u32] Some(Self::Play(t)))
|
||||
("pause" [] Some(Self::Pause(None)))
|
||||
("pause" [] Some(Self::Pause(None)))
|
||||
("pause" [t: u32] Some(Self::Pause(t)))
|
||||
("toggle" [] Some(if state.is_rolling() { Self::Pause(None) } else { Self::Play(None) }))
|
||||
("toggle" [] Some(if state.is_rolling() { Self::Pause(None) } else { Self::Play(None) }))
|
||||
("toggle" [t: u32] Some(if state.is_rolling() { Self::Pause(t) } else { Self::Play(t) }))
|
||||
("seek/usec" [t: f64] Some(Self::SeekUsec(t.expect("no usec"))))
|
||||
("seek/pulse" [t: f64] Some(Self::SeekPulse(t.expect("no pulse"))))
|
||||
|
|
@ -3,7 +3,7 @@ use crate::*;
|
|||
#[derive(Clone, Default)]
|
||||
pub struct Clock {
|
||||
/// JACK transport handle.
|
||||
pub transport: Arc<Option<Transport>>,
|
||||
pub transport: Arc<Option<tek_engine::jack::Transport>>,
|
||||
/// Global temporal resolution (shared by [Moment] fields)
|
||||
pub timebase: Arc<Timebase>,
|
||||
/// Current global sample and usec (monotonic from JACK clock)
|
||||
|
|
@ -127,7 +127,7 @@ impl Clock {
|
|||
self.set_chunk(scope.n_frames() as usize);
|
||||
|
||||
// Store reported global frame and usec
|
||||
let CycleTimes { current_frames, current_usecs, .. } = scope.cycle_times()?;
|
||||
let tek_engine::jack::CycleTimes { current_frames, current_usecs, .. } = scope.cycle_times()?;
|
||||
self.global.sample.set(current_frames as f64);
|
||||
self.global.usec.set(current_usecs as f64);
|
||||
|
||||
|
|
@ -157,12 +157,12 @@ impl Clock {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn bbt (&self) -> PositionBBT {
|
||||
pub fn bbt (&self) -> tek_engine::jack::PositionBBT {
|
||||
let pulse = self.playhead.pulse.get() as i32;
|
||||
let ppq = self.timebase.ppq.get() as i32;
|
||||
let bpm = self.timebase.bpm.get();
|
||||
let bar = (pulse / ppq) / 4;
|
||||
PositionBBT {
|
||||
tek_engine::jack::PositionBBT {
|
||||
bar: 1 + bar,
|
||||
beat: 1 + (pulse / ppq) % 4,
|
||||
tick: (pulse % ppq),
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
#![feature(let_chains)]
|
||||
|
||||
pub(crate) use std::cmp::Ord;
|
||||
pub(crate) use std::fmt::{Debug, Formatter};
|
||||
pub(crate) use std::thread::JoinHandle;
|
||||
pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::Relaxed}};
|
||||
pub(crate) use std::fs::File;
|
||||
pub(crate) use std::path::PathBuf;
|
||||
pub(crate) use std::error::Error;
|
||||
pub(crate) use std::ffi::OsString;
|
||||
|
||||
pub(crate) use ::tengri::{dsl::*, input::*, output::*, tui::{*, ratatui::prelude::*}};
|
||||
pub(crate) use ::tek_engine::*;
|
||||
pub(crate) use ::tek_engine::midi::{u7, LiveEvent, MidiMessage};
|
||||
pub(crate) use ::tek_engine::jack::{Control, ProcessScope, MidiWriter, RawMidi};
|
||||
pub(crate) use ratatui::{prelude::Rect, widgets::{Widget, canvas::{Canvas, Line}}};
|
||||
|
||||
#[cfg(feature = "clock")] mod clock;
|
||||
#[cfg(feature = "clock")] pub use self::clock::*;
|
||||
|
||||
#[cfg(feature = "sequencer")] mod sequencer;
|
||||
#[cfg(feature = "sequencer")] pub use self::sequencer::*;
|
||||
|
||||
#[cfg(feature = "sampler")] mod sampler;
|
||||
#[cfg(feature = "sampler")] pub use self::sampler::*;
|
||||
|
||||
#[cfg(feature = "plugin")] mod plugin;
|
||||
#[cfg(feature = "plugin")] pub use self::plugin::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Device {
|
||||
#[cfg(feature = "sequencer")]
|
||||
Sequencer(MidiPlayer),
|
||||
#[cfg(feature = "sampler")]
|
||||
Sampler(Sampler),
|
||||
#[cfg(feature = "plugin")]
|
||||
Plugin(Plugin),
|
||||
}
|
||||
|
|
@ -1,8 +1,281 @@
|
|||
mod plugin; pub use self::plugin::*;
|
||||
mod lv2; pub use self::lv2::*;
|
||||
pub(crate) use std::cmp::Ord;
|
||||
pub(crate) use std::fmt::{Debug, Formatter};
|
||||
pub(crate) use std::sync::{Arc, RwLock};
|
||||
pub(crate) use std::thread::JoinHandle;
|
||||
pub(crate) use ::tek_jack::{*, jack::*};
|
||||
pub(crate) use ::tengri::{output::*, tui::{*, ratatui::prelude::*}};
|
||||
use crate::*;
|
||||
|
||||
mod lv2;
|
||||
mod lv2_gui;
|
||||
mod lv2_tui;
|
||||
mod vst2_tui;
|
||||
mod vst3_tui;
|
||||
|
||||
/// A plugin device.
|
||||
#[derive(Debug)]
|
||||
pub struct Plugin {
|
||||
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
||||
pub jack: Jack,
|
||||
pub name: Arc<str>,
|
||||
pub path: Option<Arc<str>>,
|
||||
pub plugin: Option<PluginKind>,
|
||||
pub selected: usize,
|
||||
pub mapping: bool,
|
||||
pub midi_ins: Vec<Port<MidiIn>>,
|
||||
pub midi_outs: Vec<Port<MidiOut>>,
|
||||
pub audio_ins: Vec<Port<AudioIn>>,
|
||||
pub audio_outs: Vec<Port<AudioOut>>,
|
||||
}
|
||||
|
||||
/// Supported plugin formats.
|
||||
#[derive(Default)]
|
||||
pub enum PluginKind {
|
||||
#[default] None,
|
||||
LV2(LV2Plugin),
|
||||
VST2 { instance: () /*::vst::host::PluginInstance*/ },
|
||||
VST3,
|
||||
}
|
||||
|
||||
impl Debug for PluginKind {
|
||||
fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||
write!(f, "{}", match self {
|
||||
Self::None => "(none)",
|
||||
Self::LV2(_) => "LV2",
|
||||
Self::VST2{..} => "VST2",
|
||||
Self::VST3 => "VST3",
|
||||
})
|
||||
}
|
||||
}
|
||||
impl Plugin {
|
||||
pub fn new_lv2 (
|
||||
jack: &Jack,
|
||||
name: &str,
|
||||
path: &str,
|
||||
) -> Usually<Self> {
|
||||
Ok(Self {
|
||||
jack: jack.clone(),
|
||||
name: name.into(),
|
||||
path: Some(String::from(path).into()),
|
||||
plugin: Some(PluginKind::LV2(LV2Plugin::new(path)?)),
|
||||
selected: 0,
|
||||
mapping: false,
|
||||
midi_ins: vec![],
|
||||
midi_outs: vec![],
|
||||
audio_ins: vec![],
|
||||
audio_outs: vec![],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PluginAudio(Arc<RwLock<Plugin>>);
|
||||
from!(|model: &Arc<RwLock<Plugin>>| PluginAudio = Self(model.clone()));
|
||||
audio!(|self: PluginAudio, _client, scope|{
|
||||
let state = &mut*self.0.write().unwrap();
|
||||
match state.plugin.as_mut() {
|
||||
Some(PluginKind::LV2(LV2Plugin {
|
||||
features,
|
||||
ref mut instance,
|
||||
ref mut input_buffer,
|
||||
..
|
||||
})) => {
|
||||
let urid = features.midi_urid();
|
||||
input_buffer.clear();
|
||||
for port in state.midi_ins.iter() {
|
||||
let mut atom = ::livi::event::LV2AtomSequence::new(
|
||||
&features,
|
||||
scope.n_frames() as usize
|
||||
);
|
||||
for event in port.iter(scope) {
|
||||
match event.bytes.len() {
|
||||
3 => atom.push_midi_event::<3>(
|
||||
event.time as i64,
|
||||
urid,
|
||||
&event.bytes[0..3]
|
||||
).unwrap(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
input_buffer.push(atom);
|
||||
}
|
||||
let mut outputs = vec![];
|
||||
for _ in state.midi_outs.iter() {
|
||||
outputs.push(::livi::event::LV2AtomSequence::new(
|
||||
features,
|
||||
scope.n_frames() as usize
|
||||
));
|
||||
}
|
||||
let ports = ::livi::EmptyPortConnections::new()
|
||||
.with_atom_sequence_inputs(input_buffer.iter())
|
||||
.with_atom_sequence_outputs(outputs.iter_mut())
|
||||
.with_audio_inputs(state.audio_ins.iter().map(|o|o.as_slice(scope)))
|
||||
.with_audio_outputs(state.audio_outs.iter_mut().map(|o|o.as_mut_slice(scope)));
|
||||
unsafe {
|
||||
instance.run(scope.n_frames() as usize, ports).unwrap()
|
||||
};
|
||||
},
|
||||
_ => todo!("only lv2 is supported")
|
||||
}
|
||||
Control::Continue
|
||||
});
|
||||
|
||||
//fn jack_from_lv2 (name: &str, plugin: &::livi::Plugin) -> Usually<Jack> {
|
||||
//let counts = plugin.port_counts();
|
||||
//let mut jack = Jack::new(name)?;
|
||||
//for i in 0..counts.atom_sequence_inputs {
|
||||
//jack = jack.midi_in(&format!("midi-in-{i}"))
|
||||
//}
|
||||
//for i in 0..counts.atom_sequence_outputs {
|
||||
//jack = jack.midi_out(&format!("midi-out-{i}"));
|
||||
//}
|
||||
//for i in 0..counts.audio_inputs {
|
||||
//jack = jack.audio_in(&format!("audio-in-{i}"));
|
||||
//}
|
||||
//for i in 0..counts.audio_outputs {
|
||||
//jack = jack.audio_out(&format!("audio-out-{i}"));
|
||||
//}
|
||||
//Ok(jack)
|
||||
//}
|
||||
|
||||
impl Plugin {
|
||||
/// Create a plugin host device.
|
||||
pub fn new (
|
||||
jack: &Jack,
|
||||
name: &str,
|
||||
) -> Usually<Self> {
|
||||
Ok(Self {
|
||||
//_engine: Default::default(),
|
||||
jack: jack.clone(),
|
||||
name: name.into(),
|
||||
path: None,
|
||||
plugin: None,
|
||||
selected: 0,
|
||||
mapping: false,
|
||||
audio_ins: vec![],
|
||||
audio_outs: vec![],
|
||||
midi_ins: vec![],
|
||||
midi_outs: vec![],
|
||||
//ports: JackPorts::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
impl Content<TuiOut> for Plugin {
|
||||
fn render (&self, to: &mut TuiOut) {
|
||||
let area = to.area();
|
||||
let [x, y, _, height] = area;
|
||||
let mut width = 20u16;
|
||||
match &self.plugin {
|
||||
Some(PluginKind::LV2(LV2Plugin { port_list, instance, .. })) => {
|
||||
let start = self.selected.saturating_sub((height as usize / 2).saturating_sub(1));
|
||||
let end = start + height as usize - 2;
|
||||
//draw_box(buf, Rect { x, y, width, height });
|
||||
for i in start..end {
|
||||
if let Some(port) = port_list.get(i) {
|
||||
let value = if let Some(value) = instance.control_input(port.index) {
|
||||
value
|
||||
} else {
|
||||
port.default_value
|
||||
};
|
||||
//let label = &format!("C·· M·· {:25} = {value:.03}", port.name);
|
||||
let label = &format!("{:25} = {value:.03}", port.name);
|
||||
width = width.max(label.len() as u16 + 4);
|
||||
let style = if i == self.selected {
|
||||
Some(Style::default().green())
|
||||
} else {
|
||||
None
|
||||
} ;
|
||||
to.blit(&label, x + 2, y + 1 + i as u16 - start as u16, style);
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
};
|
||||
draw_header(self, to, x, y, width);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_header (state: &Plugin, to: &mut TuiOut, x: u16, y: u16, w: u16) {
|
||||
let style = Style::default().gray();
|
||||
let label1 = format!(" {}", state.name);
|
||||
to.blit(&label1, x + 1, y, Some(style.white().bold()));
|
||||
if let Some(ref path) = state.path {
|
||||
let label2 = format!("{}…", &path[..((w as usize - 10).min(path.len()))]);
|
||||
to.blit(&label2, x + 2 + label1.len() as u16, y, Some(style.not_dim()));
|
||||
}
|
||||
//Ok(Rect { x, y, width: w, height: 1 })
|
||||
}
|
||||
|
||||
//handle!(TuiIn: |self:Plugin, from|{
|
||||
//match from.event() {
|
||||
//kpat!(KeyCode::Up) => {
|
||||
//self.selected = self.selected.saturating_sub(1);
|
||||
//Ok(Some(true))
|
||||
//},
|
||||
//kpat!(KeyCode::Down) => {
|
||||
//self.selected = (self.selected + 1).min(match &self.plugin {
|
||||
//Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1,
|
||||
//_ => unimplemented!()
|
||||
//});
|
||||
//Ok(Some(true))
|
||||
//},
|
||||
//kpat!(KeyCode::PageUp) => {
|
||||
//self.selected = self.selected.saturating_sub(8);
|
||||
//Ok(Some(true))
|
||||
//},
|
||||
//kpat!(KeyCode::PageDown) => {
|
||||
//self.selected = (self.selected + 10).min(match &self.plugin {
|
||||
//Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1,
|
||||
//_ => unimplemented!()
|
||||
//});
|
||||
//Ok(Some(true))
|
||||
//},
|
||||
//kpat!(KeyCode::Char(',')) => {
|
||||
//match self.plugin.as_mut() {
|
||||
//Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => {
|
||||
//let index = port_list[self.selected].index;
|
||||
//if let Some(value) = instance.control_input(index) {
|
||||
//instance.set_control_input(index, value - 0.01);
|
||||
//}
|
||||
//},
|
||||
//_ => {}
|
||||
//}
|
||||
//Ok(Some(true))
|
||||
//},
|
||||
//kpat!(KeyCode::Char('.')) => {
|
||||
//match self.plugin.as_mut() {
|
||||
//Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => {
|
||||
//let index = port_list[self.selected].index;
|
||||
//if let Some(value) = instance.control_input(index) {
|
||||
//instance.set_control_input(index, value + 0.01);
|
||||
//}
|
||||
//},
|
||||
//_ => {}
|
||||
//}
|
||||
//Ok(Some(true))
|
||||
//},
|
||||
//kpat!(KeyCode::Char('g')) => {
|
||||
//match self.plugin {
|
||||
////Some(PluginKind::LV2(ref mut plugin)) => {
|
||||
////plugin.ui_thread = Some(run_lv2_ui(LV2PluginUI::new()?)?);
|
||||
////},
|
||||
//Some(_) => unreachable!(),
|
||||
//None => {}
|
||||
//}
|
||||
//Ok(Some(true))
|
||||
//},
|
||||
//_ => Ok(None)
|
||||
//}
|
||||
//});
|
||||
|
||||
//from_atom!("plugin/lv2" => |jack: &Jack, args| -> Plugin {
|
||||
//let mut name = String::new();
|
||||
//let mut path = String::new();
|
||||
//atom!(atom in args {
|
||||
//Atom::Map(map) => {
|
||||
//if let Some(Atom::Str(n)) = map.get(&Atom::Key(":name")) {
|
||||
//name = String::from(*n);
|
||||
//}
|
||||
//if let Some(Atom::Str(p)) = map.get(&Atom::Key(":path")) {
|
||||
//path = String::from(*p);
|
||||
//}
|
||||
//},
|
||||
//_ => panic!("unexpected in lv2 '{name}'"),
|
||||
//});
|
||||
//Plugin::new_lv2(jack, &name, &path)
|
||||
//});
|
||||
|
|
|
|||
|
|
@ -1,275 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
/// A plugin device.
|
||||
#[derive(Debug)]
|
||||
pub struct Plugin {
|
||||
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
||||
pub jack: Jack,
|
||||
pub name: Arc<str>,
|
||||
pub path: Option<Arc<str>>,
|
||||
pub plugin: Option<PluginKind>,
|
||||
pub selected: usize,
|
||||
pub mapping: bool,
|
||||
pub midi_ins: Vec<Port<MidiIn>>,
|
||||
pub midi_outs: Vec<Port<MidiOut>>,
|
||||
pub audio_ins: Vec<Port<AudioIn>>,
|
||||
pub audio_outs: Vec<Port<AudioOut>>,
|
||||
}
|
||||
|
||||
/// Supported plugin formats.
|
||||
#[derive(Default)]
|
||||
pub enum PluginKind {
|
||||
#[default] None,
|
||||
LV2(LV2Plugin),
|
||||
VST2 { instance: () /*::vst::host::PluginInstance*/ },
|
||||
VST3,
|
||||
}
|
||||
|
||||
impl Debug for PluginKind {
|
||||
fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||
write!(f, "{}", match self {
|
||||
Self::None => "(none)",
|
||||
Self::LV2(_) => "LV2",
|
||||
Self::VST2{..} => "VST2",
|
||||
Self::VST3 => "VST3",
|
||||
})
|
||||
}
|
||||
}
|
||||
impl Plugin {
|
||||
pub fn new_lv2 (
|
||||
jack: &Jack,
|
||||
name: &str,
|
||||
path: &str,
|
||||
) -> Usually<Self> {
|
||||
Ok(Self {
|
||||
jack: jack.clone(),
|
||||
name: name.into(),
|
||||
path: Some(String::from(path).into()),
|
||||
plugin: Some(PluginKind::LV2(LV2Plugin::new(path)?)),
|
||||
selected: 0,
|
||||
mapping: false,
|
||||
midi_ins: vec![],
|
||||
midi_outs: vec![],
|
||||
audio_ins: vec![],
|
||||
audio_outs: vec![],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PluginAudio(Arc<RwLock<Plugin>>);
|
||||
from!(|model: &Arc<RwLock<Plugin>>| PluginAudio = Self(model.clone()));
|
||||
audio!(|self: PluginAudio, _client, scope|{
|
||||
let state = &mut*self.0.write().unwrap();
|
||||
match state.plugin.as_mut() {
|
||||
Some(PluginKind::LV2(LV2Plugin {
|
||||
features,
|
||||
ref mut instance,
|
||||
ref mut input_buffer,
|
||||
..
|
||||
})) => {
|
||||
let urid = features.midi_urid();
|
||||
input_buffer.clear();
|
||||
for port in state.midi_ins.iter() {
|
||||
let mut atom = ::livi::event::LV2AtomSequence::new(
|
||||
&features,
|
||||
scope.n_frames() as usize
|
||||
);
|
||||
for event in port.iter(scope) {
|
||||
match event.bytes.len() {
|
||||
3 => atom.push_midi_event::<3>(
|
||||
event.time as i64,
|
||||
urid,
|
||||
&event.bytes[0..3]
|
||||
).unwrap(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
input_buffer.push(atom);
|
||||
}
|
||||
let mut outputs = vec![];
|
||||
for _ in state.midi_outs.iter() {
|
||||
outputs.push(::livi::event::LV2AtomSequence::new(
|
||||
features,
|
||||
scope.n_frames() as usize
|
||||
));
|
||||
}
|
||||
let ports = ::livi::EmptyPortConnections::new()
|
||||
.with_atom_sequence_inputs(input_buffer.iter())
|
||||
.with_atom_sequence_outputs(outputs.iter_mut())
|
||||
.with_audio_inputs(state.audio_ins.iter().map(|o|o.as_slice(scope)))
|
||||
.with_audio_outputs(state.audio_outs.iter_mut().map(|o|o.as_mut_slice(scope)));
|
||||
unsafe {
|
||||
instance.run(scope.n_frames() as usize, ports).unwrap()
|
||||
};
|
||||
},
|
||||
_ => todo!("only lv2 is supported")
|
||||
}
|
||||
Control::Continue
|
||||
});
|
||||
|
||||
//fn jack_from_lv2 (name: &str, plugin: &::livi::Plugin) -> Usually<Jack> {
|
||||
//let counts = plugin.port_counts();
|
||||
//let mut jack = Jack::new(name)?;
|
||||
//for i in 0..counts.atom_sequence_inputs {
|
||||
//jack = jack.midi_in(&format!("midi-in-{i}"))
|
||||
//}
|
||||
//for i in 0..counts.atom_sequence_outputs {
|
||||
//jack = jack.midi_out(&format!("midi-out-{i}"));
|
||||
//}
|
||||
//for i in 0..counts.audio_inputs {
|
||||
//jack = jack.audio_in(&format!("audio-in-{i}"));
|
||||
//}
|
||||
//for i in 0..counts.audio_outputs {
|
||||
//jack = jack.audio_out(&format!("audio-out-{i}"));
|
||||
//}
|
||||
//Ok(jack)
|
||||
//}
|
||||
|
||||
impl Plugin {
|
||||
/// Create a plugin host device.
|
||||
pub fn new (
|
||||
jack: &Jack,
|
||||
name: &str,
|
||||
) -> Usually<Self> {
|
||||
Ok(Self {
|
||||
//_engine: Default::default(),
|
||||
jack: jack.clone(),
|
||||
name: name.into(),
|
||||
path: None,
|
||||
plugin: None,
|
||||
selected: 0,
|
||||
mapping: false,
|
||||
audio_ins: vec![],
|
||||
audio_outs: vec![],
|
||||
midi_ins: vec![],
|
||||
midi_outs: vec![],
|
||||
//ports: JackPorts::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
impl Content<TuiOut> for Plugin {
|
||||
fn render (&self, to: &mut TuiOut) {
|
||||
let area = to.area();
|
||||
let [x, y, _, height] = area;
|
||||
let mut width = 20u16;
|
||||
match &self.plugin {
|
||||
Some(PluginKind::LV2(LV2Plugin { port_list, instance, .. })) => {
|
||||
let start = self.selected.saturating_sub((height as usize / 2).saturating_sub(1));
|
||||
let end = start + height as usize - 2;
|
||||
//draw_box(buf, Rect { x, y, width, height });
|
||||
for i in start..end {
|
||||
if let Some(port) = port_list.get(i) {
|
||||
let value = if let Some(value) = instance.control_input(port.index) {
|
||||
value
|
||||
} else {
|
||||
port.default_value
|
||||
};
|
||||
//let label = &format!("C·· M·· {:25} = {value:.03}", port.name);
|
||||
let label = &format!("{:25} = {value:.03}", port.name);
|
||||
width = width.max(label.len() as u16 + 4);
|
||||
let style = if i == self.selected {
|
||||
Some(Style::default().green())
|
||||
} else {
|
||||
None
|
||||
} ;
|
||||
to.blit(&label, x + 2, y + 1 + i as u16 - start as u16, style);
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
};
|
||||
draw_header(self, to, x, y, width);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_header (state: &Plugin, to: &mut TuiOut, x: u16, y: u16, w: u16) {
|
||||
let style = Style::default().gray();
|
||||
let label1 = format!(" {}", state.name);
|
||||
to.blit(&label1, x + 1, y, Some(style.white().bold()));
|
||||
if let Some(ref path) = state.path {
|
||||
let label2 = format!("{}…", &path[..((w as usize - 10).min(path.len()))]);
|
||||
to.blit(&label2, x + 2 + label1.len() as u16, y, Some(style.not_dim()));
|
||||
}
|
||||
//Ok(Rect { x, y, width: w, height: 1 })
|
||||
}
|
||||
|
||||
//handle!(TuiIn: |self:Plugin, from|{
|
||||
//match from.event() {
|
||||
//kpat!(KeyCode::Up) => {
|
||||
//self.selected = self.selected.saturating_sub(1);
|
||||
//Ok(Some(true))
|
||||
//},
|
||||
//kpat!(KeyCode::Down) => {
|
||||
//self.selected = (self.selected + 1).min(match &self.plugin {
|
||||
//Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1,
|
||||
//_ => unimplemented!()
|
||||
//});
|
||||
//Ok(Some(true))
|
||||
//},
|
||||
//kpat!(KeyCode::PageUp) => {
|
||||
//self.selected = self.selected.saturating_sub(8);
|
||||
//Ok(Some(true))
|
||||
//},
|
||||
//kpat!(KeyCode::PageDown) => {
|
||||
//self.selected = (self.selected + 10).min(match &self.plugin {
|
||||
//Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1,
|
||||
//_ => unimplemented!()
|
||||
//});
|
||||
//Ok(Some(true))
|
||||
//},
|
||||
//kpat!(KeyCode::Char(',')) => {
|
||||
//match self.plugin.as_mut() {
|
||||
//Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => {
|
||||
//let index = port_list[self.selected].index;
|
||||
//if let Some(value) = instance.control_input(index) {
|
||||
//instance.set_control_input(index, value - 0.01);
|
||||
//}
|
||||
//},
|
||||
//_ => {}
|
||||
//}
|
||||
//Ok(Some(true))
|
||||
//},
|
||||
//kpat!(KeyCode::Char('.')) => {
|
||||
//match self.plugin.as_mut() {
|
||||
//Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => {
|
||||
//let index = port_list[self.selected].index;
|
||||
//if let Some(value) = instance.control_input(index) {
|
||||
//instance.set_control_input(index, value + 0.01);
|
||||
//}
|
||||
//},
|
||||
//_ => {}
|
||||
//}
|
||||
//Ok(Some(true))
|
||||
//},
|
||||
//kpat!(KeyCode::Char('g')) => {
|
||||
//match self.plugin {
|
||||
////Some(PluginKind::LV2(ref mut plugin)) => {
|
||||
////plugin.ui_thread = Some(run_lv2_ui(LV2PluginUI::new()?)?);
|
||||
////},
|
||||
//Some(_) => unreachable!(),
|
||||
//None => {}
|
||||
//}
|
||||
//Ok(Some(true))
|
||||
//},
|
||||
//_ => Ok(None)
|
||||
//}
|
||||
//});
|
||||
|
||||
//from_atom!("plugin/lv2" => |jack: &Jack, args| -> Plugin {
|
||||
//let mut name = String::new();
|
||||
//let mut path = String::new();
|
||||
//atom!(atom in args {
|
||||
//Atom::Map(map) => {
|
||||
//if let Some(Atom::Str(n)) = map.get(&Atom::Key(":name")) {
|
||||
//name = String::from(*n);
|
||||
//}
|
||||
//if let Some(Atom::Str(p)) = map.get(&Atom::Key(":path")) {
|
||||
//path = String::from(*p);
|
||||
//}
|
||||
//},
|
||||
//_ => panic!("unexpected in lv2 '{name}'"),
|
||||
//});
|
||||
//Plugin::new_lv2(jack, &name, &path)
|
||||
//});
|
||||
|
|
@ -1,13 +1,5 @@
|
|||
#![feature(let_chains)]
|
||||
use crate::*;
|
||||
|
||||
pub(crate) use ::tek_jack::{*, jack::*};
|
||||
pub(crate) use ::tek_midi::{*, midly::{*, live::*, num::*}};
|
||||
pub(crate) use ::tengri::{dsl::*, input::*, output::*, tui::{*, ratatui::prelude::*}};
|
||||
pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::Relaxed}};
|
||||
pub(crate) use std::fs::File;
|
||||
pub(crate) use std::path::PathBuf;
|
||||
pub(crate) use std::error::Error;
|
||||
pub(crate) use std::ffi::OsString;
|
||||
pub(crate) use symphonia::{
|
||||
core::{
|
||||
formats::Packet,
|
||||
|
|
@ -19,7 +11,6 @@ pub(crate) use symphonia::{
|
|||
},
|
||||
default::get_codecs,
|
||||
};
|
||||
pub(crate) use ratatui::{prelude::Rect, widgets::{Widget, canvas::{Canvas, Line}}};
|
||||
|
||||
mod sampler_api; pub use self::sampler_api::*;
|
||||
mod sampler_audio; pub use self::sampler_audio::*;
|
||||
|
|
|
|||
|
|
@ -53,10 +53,8 @@ impl Sampler {
|
|||
|
||||
const _EMPTY: &[(f64, f64)] = &[(0., 0.), (1., 1.), (2., 2.), (0., 2.), (2., 0.)];
|
||||
|
||||
pub fn view_list <'a> (
|
||||
&'a self,
|
||||
compact: bool,
|
||||
editor: &MidiEditor
|
||||
pub fn view_list <'a, T: NotePoint + NoteRange> (
|
||||
&'a self, compact: bool, editor: &T
|
||||
) -> impl Content<TuiOut> + 'a {
|
||||
let note_lo = editor.note_lo().load(Relaxed);
|
||||
let note_pt = editor.note_pos();
|
||||
|
|
|
|||
|
|
@ -1,38 +1,9 @@
|
|||
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::*;
|
||||
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 }
|
||||
}
|
||||
};
|
||||
}
|
||||
mod seq_clip; pub use self::seq_clip::*;
|
||||
mod seq_launch; pub use self::seq_launch::*;
|
||||
mod seq_model; pub use self::seq_model::*;
|
||||
mod seq_view; pub use self::seq_view::*;
|
||||
|
||||
#[cfg(test)] #[test] pub fn test_midi_clip () {
|
||||
let clip = MidiClip::stop_all();
|
||||
|
|
@ -87,4 +87,3 @@ pub trait HasPlayClip: HasClock {
|
|||
FieldV(color, "Next:", format!("{} {}", time, name))
|
||||
}
|
||||
}
|
||||
|
||||
453
crates/device/src/sequencer/seq_model.rs
Normal file
453
crates/device/src/sequencer/seq_model.rs
Normal file
|
|
@ -0,0 +1,453 @@
|
|||
//! MIDI player
|
||||
use crate::*;
|
||||
use tek_engine::jack::*;
|
||||
|
||||
pub trait HasPlayer {
|
||||
fn player (&self) -> &impl MidiPlayerApi;
|
||||
fn player_mut (&mut self) -> &mut impl MidiPlayerApi;
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! has_player {
|
||||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||||
impl $(<$($L),*$($T $(: $U)?),*>)? HasPlayer for $Struct $(<$($L),*$($T),*>)? {
|
||||
fn player (&$self) -> &impl MidiPlayerApi { &$cb }
|
||||
fn player_mut (&mut $self) -> &mut impl MidiPlayerApi { &mut$cb }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait MidiPlayerApi: MidiRecordApi + MidiPlaybackApi + Send + Sync {}
|
||||
|
||||
impl MidiPlayerApi for MidiPlayer {}
|
||||
|
||||
/// Contains state for playing a clip
|
||||
pub struct MidiPlayer {
|
||||
/// State of clock and playhead
|
||||
pub clock: Clock,
|
||||
/// Start time and clip being played
|
||||
pub play_clip: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
|
||||
/// Start time and next clip
|
||||
pub next_clip: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
|
||||
/// Play input through output.
|
||||
pub monitoring: bool,
|
||||
/// Write input to sequence.
|
||||
pub recording: bool,
|
||||
/// Overdub input to sequence.
|
||||
pub overdub: bool,
|
||||
/// Send all notes off
|
||||
pub reset: bool, // TODO?: after Some(nframes)
|
||||
/// Record from MIDI ports to current sequence.
|
||||
pub midi_ins: Vec<JackMidiIn>,
|
||||
/// Play from current sequence to MIDI ports
|
||||
pub midi_outs: Vec<JackMidiOut>,
|
||||
/// Notes currently held at input
|
||||
pub notes_in: Arc<RwLock<[bool; 128]>>,
|
||||
/// Notes currently held at output
|
||||
pub notes_out: Arc<RwLock<[bool; 128]>>,
|
||||
/// MIDI output buffer
|
||||
pub note_buf: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Default for MidiPlayer {
|
||||
fn default () -> Self {
|
||||
Self {
|
||||
play_clip: None,
|
||||
next_clip: None,
|
||||
recording: false,
|
||||
monitoring: false,
|
||||
overdub: false,
|
||||
|
||||
notes_in: RwLock::new([false;128]).into(),
|
||||
notes_out: RwLock::new([false;128]).into(),
|
||||
note_buf: vec![0;8],
|
||||
reset: true,
|
||||
|
||||
midi_ins: vec![],
|
||||
midi_outs: vec![],
|
||||
clock: Clock::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MidiPlayer {
|
||||
pub fn new (
|
||||
name: impl AsRef<str>,
|
||||
jack: &Jack,
|
||||
clock: Option<&Clock>,
|
||||
clip: Option<&Arc<RwLock<MidiClip>>>,
|
||||
midi_from: &[PortConnect],
|
||||
midi_to: &[PortConnect],
|
||||
) -> Usually<Self> {
|
||||
let _name = name.as_ref();
|
||||
let clock = clock.cloned().unwrap_or_default();
|
||||
Ok(Self {
|
||||
midi_ins: vec![JackMidiIn::new(jack, format!("M/{}", name.as_ref()), midi_from)?,],
|
||||
midi_outs: vec![JackMidiOut::new(jack, format!("{}/M", name.as_ref()), midi_to)?, ],
|
||||
play_clip: clip.map(|clip|(Moment::zero(&clock.timebase), Some(clip.clone()))),
|
||||
clock,
|
||||
note_buf: vec![0;8],
|
||||
reset: true,
|
||||
recording: false,
|
||||
monitoring: false,
|
||||
overdub: false,
|
||||
next_clip: None,
|
||||
notes_in: RwLock::new([false;128]).into(),
|
||||
notes_out: RwLock::new([false;128]).into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for MidiPlayer {
|
||||
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||
f.debug_struct("MidiPlayer")
|
||||
.field("clock", &self.clock)
|
||||
.field("play_clip", &self.play_clip)
|
||||
.field("next_clip", &self.next_clip)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
has_clock!(|self: MidiPlayer|self.clock);
|
||||
|
||||
impl HasMidiIns for MidiPlayer {
|
||||
fn midi_ins (&self) -> &Vec<JackMidiIn> { &self.midi_ins }
|
||||
fn midi_ins_mut (&mut self) -> &mut Vec<JackMidiIn> { &mut self.midi_ins }
|
||||
}
|
||||
|
||||
impl HasMidiOuts for MidiPlayer {
|
||||
fn midi_outs (&self) -> &Vec<JackMidiOut> { &self.midi_outs }
|
||||
fn midi_outs_mut (&mut self) -> &mut Vec<JackMidiOut> { &mut self.midi_outs }
|
||||
fn midi_note (&mut self) -> &mut Vec<u8> { &mut self.note_buf }
|
||||
}
|
||||
|
||||
/// Hosts the JACK callback for a single MIDI player
|
||||
pub struct PlayerAudio<'a, T: MidiPlayerApi>(
|
||||
/// Player
|
||||
pub &'a mut T,
|
||||
/// Note buffer
|
||||
pub &'a mut Vec<u8>,
|
||||
/// Note chunk buffer
|
||||
pub &'a mut Vec<Vec<Vec<u8>>>,
|
||||
);
|
||||
|
||||
/// JACK process callback for a sequencer's clip player/recorder.
|
||||
impl<T: MidiPlayerApi> Audio for PlayerAudio<'_, T> {
|
||||
fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
|
||||
let model = &mut self.0;
|
||||
let note_buf = &mut self.1;
|
||||
let midi_buf = &mut self.2;
|
||||
// Clear output buffer(s)
|
||||
model.clear(scope, midi_buf, false);
|
||||
// Write chunk of clip to output, handle switchover
|
||||
if model.play(scope, note_buf, midi_buf) {
|
||||
model.switchover(scope, note_buf, midi_buf);
|
||||
}
|
||||
if model.has_midi_ins() {
|
||||
if model.recording() || model.monitoring() {
|
||||
// Record and/or monitor input
|
||||
model.record(scope, midi_buf)
|
||||
} else if model.has_midi_outs() && model.monitoring() {
|
||||
// Monitor input to output
|
||||
model.monitor(scope, midi_buf)
|
||||
}
|
||||
}
|
||||
// Write to output port(s)
|
||||
model.write(scope, midi_buf);
|
||||
Control::Continue
|
||||
}
|
||||
}
|
||||
|
||||
impl MidiRecordApi for MidiPlayer {
|
||||
fn recording (&self) -> bool {
|
||||
self.recording
|
||||
}
|
||||
fn recording_mut (&mut self) -> &mut bool {
|
||||
&mut self.recording
|
||||
}
|
||||
fn monitoring (&self) -> bool {
|
||||
self.monitoring
|
||||
}
|
||||
fn monitoring_mut (&mut self) -> &mut bool {
|
||||
&mut self.monitoring
|
||||
}
|
||||
fn overdub (&self) -> bool {
|
||||
self.overdub
|
||||
}
|
||||
fn overdub_mut (&mut self) -> &mut bool {
|
||||
&mut self.overdub
|
||||
}
|
||||
fn notes_in (&self) -> &Arc<RwLock<[bool; 128]>> {
|
||||
&self.notes_in
|
||||
}
|
||||
}
|
||||
|
||||
impl MidiPlaybackApi for MidiPlayer {
|
||||
fn notes_out (&self) -> &Arc<RwLock<[bool; 128]>> {
|
||||
&self.notes_out
|
||||
}
|
||||
}
|
||||
|
||||
impl HasPlayClip for MidiPlayer {
|
||||
fn reset (&self) -> bool {
|
||||
self.reset
|
||||
}
|
||||
fn reset_mut (&mut self) -> &mut bool {
|
||||
&mut self.reset
|
||||
}
|
||||
fn play_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
|
||||
&self.play_clip
|
||||
}
|
||||
fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
|
||||
&mut self.play_clip
|
||||
}
|
||||
fn next_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
|
||||
&self.next_clip
|
||||
}
|
||||
fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
|
||||
&mut self.next_clip
|
||||
}
|
||||
}
|
||||
|
||||
pub trait MidiRecordApi: HasClock + HasPlayClip + HasMidiIns {
|
||||
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();
|
||||
let monitoring = self.monitoring();
|
||||
for input in self.midi_ins_mut().iter() {
|
||||
for (sample, event, bytes) in parse_midi_input(input.port().iter(scope)) {
|
||||
if let LiveEvent::Midi { message, .. } = event {
|
||||
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);
|
||||
}
|
||||
if !self.clock().is_rolling() {
|
||||
return
|
||||
}
|
||||
if let Some((started, ref clip)) = self.play_clip().clone() {
|
||||
self.record_clip(scope, started, clip, midi_buf);
|
||||
}
|
||||
if let Some((_start_at, _clip)) = &self.next_clip() {
|
||||
self.record_next();
|
||||
}
|
||||
}
|
||||
|
||||
fn record_clip (
|
||||
&mut self,
|
||||
scope: &ProcessScope,
|
||||
started: Moment,
|
||||
clip: &Option<Arc<RwLock<MidiClip>>>,
|
||||
_midi_buf: &mut Vec<Vec<Vec<u8>>>
|
||||
) {
|
||||
if let Some(clip) = clip {
|
||||
let sample0 = scope.last_frame_time() as usize;
|
||||
let start = started.sample.get() as usize;
|
||||
let _recording = self.recording();
|
||||
let timebase = self.clock().timebase().clone();
|
||||
let quant = self.clock().quant.get();
|
||||
let mut clip = clip.write().unwrap();
|
||||
let length = clip.length;
|
||||
for input in self.midi_ins_mut().iter() {
|
||||
for (sample, event, _bytes) in parse_midi_input(input.port().iter(scope)) {
|
||||
if let LiveEvent::Midi { message, .. } = event {
|
||||
clip.record_event({
|
||||
let sample = (sample0 + sample - start) as f64;
|
||||
let pulse = timebase.samples_to_pulse(sample);
|
||||
let quantized = (pulse / quant).round() * quant;
|
||||
quantized as usize % length
|
||||
}, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn record_next (&mut self) {
|
||||
// TODO switch to next clip and record into it
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pub trait MidiPlaybackApi: HasPlayClip + HasClock + HasMidiOuts {
|
||||
|
||||
fn notes_out (&self) -> &Arc<RwLock<[bool;128]>>;
|
||||
|
||||
/// Clear the section of the output buffer that we will be using,
|
||||
/// emitting "all notes off" at start of buffer if requested.
|
||||
fn clear (
|
||||
&mut self, scope: &ProcessScope, out: &mut [Vec<Vec<u8>>], reset: bool
|
||||
) {
|
||||
let n_frames = (scope.n_frames() as usize).min(out.len());
|
||||
for frame in &mut out[0..n_frames] {
|
||||
frame.clear();
|
||||
}
|
||||
if reset {
|
||||
all_notes_off(out);
|
||||
}
|
||||
}
|
||||
|
||||
/// Output notes from clip to MIDI output ports.
|
||||
fn play (
|
||||
&mut self, scope: &ProcessScope, note_buf: &mut Vec<u8>, out: &mut [Vec<Vec<u8>>]
|
||||
) -> bool {
|
||||
if !self.clock().is_rolling() {
|
||||
return false
|
||||
}
|
||||
// If a clip is playing, write a chunk of MIDI events from it to the output buffer.
|
||||
// If no clip is playing, prepare for switchover immediately.
|
||||
self.play_clip().as_ref().map_or(true, |(started, clip)|{
|
||||
self.play_chunk(scope, note_buf, out, started, clip)
|
||||
})
|
||||
}
|
||||
|
||||
/// Handle switchover from current to next playing clip.
|
||||
fn switchover (
|
||||
&mut self, scope: &ProcessScope, note_buf: &mut Vec<u8>, out: &mut [Vec<Vec<u8>>]
|
||||
) {
|
||||
if !self.clock().is_rolling() {
|
||||
return
|
||||
}
|
||||
let sample0 = scope.last_frame_time() as usize;
|
||||
//let samples = scope.n_frames() as usize;
|
||||
if let Some((start_at, clip)) = &self.next_clip() {
|
||||
let start = start_at.sample.get() as usize;
|
||||
let sample = self.clock().started.read().unwrap()
|
||||
.as_ref().unwrap().sample.get() as usize;
|
||||
// If it's time to switch to the next clip:
|
||||
if start <= sample0.saturating_sub(sample) {
|
||||
// Samples elapsed since clip was supposed to start
|
||||
let _skipped = sample0 - start;
|
||||
// Switch over to enqueued clip
|
||||
let started = Moment::from_sample(self.clock().timebase(), start as f64);
|
||||
// Launch enqueued clip
|
||||
*self.play_clip_mut() = Some((started, clip.clone()));
|
||||
// Unset enqueuement (TODO: where to implement looping?)
|
||||
*self.next_clip_mut() = None;
|
||||
// Fill in remaining ticks of chunk from next clip.
|
||||
self.play(scope, note_buf, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn play_chunk (
|
||||
&self,
|
||||
scope: &ProcessScope,
|
||||
note_buf: &mut Vec<u8>,
|
||||
out: &mut [Vec<Vec<u8>>],
|
||||
started: &Moment,
|
||||
clip: &Option<Arc<RwLock<MidiClip>>>
|
||||
) -> bool {
|
||||
// First sample to populate. Greater than 0 means that the first
|
||||
// pulse of the clip falls somewhere in the middle of the chunk.
|
||||
let sample = (scope.last_frame_time() as usize).saturating_sub(
|
||||
started.sample.get() as usize +
|
||||
self.clock().started.read().unwrap().as_ref().unwrap().sample.get() as usize
|
||||
);
|
||||
// Iterator that emits sample (index into output buffer at which to write MIDI event)
|
||||
// paired with pulse (index into clip from which to take the MIDI event) for each
|
||||
// sample of the output buffer that corresponds to a MIDI pulse.
|
||||
let pulses = self.clock().timebase().pulses_between_samples(sample, sample + scope.n_frames() as usize);
|
||||
// Notes active during current chunk.
|
||||
let notes = &mut self.notes_out().write().unwrap();
|
||||
let length = clip.as_ref().map_or(0, |p|p.read().unwrap().length);
|
||||
for (sample, pulse) in pulses {
|
||||
// If a next clip is enqueued, and we're past the end of the current one,
|
||||
// break the loop here (FIXME count pulse correctly)
|
||||
let past_end = if clip.is_some() { pulse >= length } else { true };
|
||||
if self.next_clip().is_some() && past_end {
|
||||
return true
|
||||
}
|
||||
// If there's a currently playing clip, output notes from it to buffer:
|
||||
if let Some(ref clip) = clip {
|
||||
Self::play_pulse(clip, pulse, sample, note_buf, out, notes)
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn play_pulse (
|
||||
clip: &RwLock<MidiClip>,
|
||||
pulse: usize,
|
||||
sample: usize,
|
||||
note_buf: &mut Vec<u8>,
|
||||
out: &mut [Vec<Vec<u8>>],
|
||||
notes: &mut [bool;128]
|
||||
) {
|
||||
// Source clip from which the MIDI events will be taken.
|
||||
let clip = clip.read().unwrap();
|
||||
// Clip with zero length is not processed
|
||||
if clip.length > 0 {
|
||||
// Current pulse index in source clip
|
||||
let pulse = pulse % clip.length;
|
||||
// Output each MIDI event from clip at appropriate frames of output buffer:
|
||||
for message in clip.notes[pulse].iter() {
|
||||
// Clear output buffer for this MIDI event.
|
||||
note_buf.clear();
|
||||
// TODO: support MIDI channels other than CH1.
|
||||
let channel = 0.into();
|
||||
// Serialize MIDI event into message buffer.
|
||||
LiveEvent::Midi { channel, message: *message }
|
||||
.write(note_buf)
|
||||
.unwrap();
|
||||
// Append serialized message to output buffer.
|
||||
out[sample].push(note_buf.clone());
|
||||
// Update the list of currently held notes.
|
||||
update_keys(&mut*notes, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a chunk of MIDI data from the output buffer to all assigned output ports.
|
||||
fn write (&mut self, scope: &ProcessScope, out: &[Vec<Vec<u8>>]) {
|
||||
let samples = scope.n_frames() as usize;
|
||||
for port in self.midi_outs_mut().iter_mut() {
|
||||
Self::write_port(&mut port.port_mut().writer(scope), samples, out)
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a chunk of MIDI data from the output buffer to an output port.
|
||||
fn write_port (writer: &mut MidiWriter, samples: usize, out: &[Vec<Vec<u8>>]) {
|
||||
for (time, events) in out.iter().enumerate().take(samples) {
|
||||
for bytes in events.iter() {
|
||||
writer.write(&RawMidi { time: time as u32, bytes }).unwrap_or_else(|_|{
|
||||
panic!("Failed to write MIDI data: {bytes:?}");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +1,18 @@
|
|||
#![feature(type_alias_impl_trait)]
|
||||
mod jack_client; pub use self::jack_client::*;
|
||||
mod jack_event; pub use self::jack_event::*;
|
||||
mod jack_port; pub use self::jack_port::*;
|
||||
pub use ::jack::{*, contrib::{*, ClosureProcessHandler}};
|
||||
//contrib::ClosureProcessHandler,
|
||||
//NotificationHandler,
|
||||
//Client, AsyncClient, ClientOptions, ClientStatus,
|
||||
//ProcessScope, Control, Frames,
|
||||
//Port, PortId, PortSpec, PortFlags,
|
||||
//Unowned, MidiIn, MidiOut, AudioIn, AudioOut,
|
||||
//};
|
||||
|
||||
pub(crate) use PortConnectName::*;
|
||||
pub(crate) use PortConnectScope::*;
|
||||
pub(crate) use PortConnectStatus::*;
|
||||
pub(crate) use std::sync::{Arc, RwLock};
|
||||
pub use ::jack; pub(crate) use ::jack::{
|
||||
//contrib::ClosureProcessHandler,
|
||||
NotificationHandler,
|
||||
Client, AsyncClient, ClientOptions, ClientStatus,
|
||||
ProcessScope, Control, Frames,
|
||||
Port, PortId, PortSpec, PortFlags,
|
||||
Unowned, MidiIn, MidiOut, AudioIn, AudioOut,
|
||||
};
|
||||
pub(crate) type Usually<T> = Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
mod jack_client; pub use self::jack_client::*;
|
||||
mod jack_event; pub use self::jack_event::*;
|
||||
mod jack_port; pub use self::jack_port::*;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use crate::*;
|
||||
use ::jack::contrib::*;
|
||||
use super::*;
|
||||
use self::JackState::*;
|
||||
|
||||
/// Things that can provide a [jack::Client] reference.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use crate::*;
|
||||
use super::*;
|
||||
|
||||
/// Event enum for JACK events.
|
||||
#[derive(Debug, Clone, PartialEq)] pub enum JackEvent {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use crate::*;
|
||||
use super::*;
|
||||
|
||||
macro_rules! impl_port {
|
||||
($Name:ident : $Spec:ident -> $Pair:ident |$jack:ident, $name:ident|$port:expr) => {
|
||||
|
|
|
|||
76
crates/engine/src/lib.rs
Normal file
76
crates/engine/src/lib.rs
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
#![feature(type_alias_impl_trait)]
|
||||
|
||||
mod time; pub use self::time::*;
|
||||
mod note; pub use self::note::*;
|
||||
pub mod jack; pub use self::jack::*;
|
||||
pub mod midi; pub use self::midi::*;
|
||||
|
||||
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(crate) use std::ops::{Add, Sub, Mul, Div, Rem};
|
||||
|
||||
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};
|
||||
|
||||
pub use ::atomic_float; pub(crate) use atomic_float::*;
|
||||
|
||||
/// Standard result type.
|
||||
pub(crate) type Usually<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
/// Standard optional result type.
|
||||
pub(crate) type Perhaps<T> = std::result::Result<Option<T>, Box<dyn std::error::Error>>;
|
||||
|
||||
pub trait Gettable<T> {
|
||||
/// Returns current value
|
||||
fn get (&self) -> T;
|
||||
}
|
||||
|
||||
pub trait Mutable<T>: Gettable<T> {
|
||||
/// Sets new value, returns old
|
||||
fn set (&mut self, value: T) -> T;
|
||||
}
|
||||
|
||||
pub trait InteriorMutable<T>: Gettable<T> {
|
||||
/// Sets new value, returns old
|
||||
fn set (&self, value: T) -> T;
|
||||
}
|
||||
|
||||
impl Gettable<bool> for AtomicBool {
|
||||
fn get (&self) -> bool { self.load(Relaxed) }
|
||||
}
|
||||
|
||||
impl InteriorMutable<bool> for AtomicBool {
|
||||
fn set (&self, value: bool) -> bool { self.swap(value, Relaxed) }
|
||||
}
|
||||
|
||||
impl Gettable<usize> for AtomicUsize {
|
||||
fn get (&self) -> usize { self.load(Relaxed) }
|
||||
}
|
||||
|
||||
impl InteriorMutable<usize> for AtomicUsize {
|
||||
fn set (&self, value: usize) -> usize { self.swap(value, Relaxed) }
|
||||
}
|
||||
|
||||
#[cfg(test)] #[test] fn test_time () -> Usually<()> {
|
||||
// TODO!
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)] #[test] fn test_midi_range () {
|
||||
let model = MidiRangeModel::from((1, false));
|
||||
|
||||
let _ = model.time_len();
|
||||
let _ = model.time_zoom();
|
||||
let _ = model.time_lock();
|
||||
let _ = model.time_start();
|
||||
let _ = model.time_axis();
|
||||
let _ = model.time_end();
|
||||
|
||||
let _ = model.note_lo();
|
||||
let _ = model.note_axis();
|
||||
let _ = model.note_hi();
|
||||
}
|
||||
|
|
@ -1,21 +1,59 @@
|
|||
pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicUsize, AtomicBool, Ordering::Relaxed}};
|
||||
pub(crate) use std::path::PathBuf;
|
||||
pub(crate) use std::fmt::Debug;
|
||||
use crate::*;
|
||||
|
||||
pub use ::midly;
|
||||
pub(crate) use ::midly::{*, num::*, live::*};
|
||||
pub use ::midly::{
|
||||
MidiMessage,
|
||||
num::*,
|
||||
live::*,
|
||||
};
|
||||
|
||||
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};
|
||||
/// 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; },
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
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::*;
|
||||
/// Return boxed iterator of MIDI events
|
||||
pub fn parse_midi_input <'a> (input: ::jack::MidiIter<'a>) -> Box<dyn Iterator<Item=(usize, LiveEvent<'a>, &'a [u8])> + 'a> {
|
||||
Box::new(input.map(|::jack::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);
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for thing that may output MIDI.
|
||||
pub trait HasMidiOuts {
|
||||
fn midi_outs (&self) -> &Vec<JackMidiOut>;
|
||||
|
||||
fn midi_outs_mut (&mut self) -> &mut Vec<JackMidiOut>;
|
||||
|
||||
fn has_midi_outs (&self) -> bool {
|
||||
!self.midi_outs().is_empty()
|
||||
}
|
||||
|
||||
/// Buffer for serializing a MIDI event. FIXME rename
|
||||
fn midi_note (&mut self) -> &mut Vec<u8>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,208 +0,0 @@
|
|||
//! MIDI player
|
||||
use crate::*;
|
||||
|
||||
pub trait HasPlayer {
|
||||
fn player (&self) -> &impl MidiPlayerApi;
|
||||
fn player_mut (&mut self) -> &mut impl MidiPlayerApi;
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! has_player {
|
||||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||||
impl $(<$($L),*$($T $(: $U)?),*>)? HasPlayer for $Struct $(<$($L),*$($T),*>)? {
|
||||
fn player (&$self) -> &impl MidiPlayerApi { &$cb }
|
||||
fn player_mut (&mut $self) -> &mut impl MidiPlayerApi { &mut$cb }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait MidiPlayerApi: MidiRecordApi + MidiPlaybackApi + Send + Sync {}
|
||||
|
||||
impl MidiPlayerApi for MidiPlayer {}
|
||||
|
||||
/// Contains state for playing a clip
|
||||
pub struct MidiPlayer {
|
||||
/// State of clock and playhead
|
||||
pub clock: Clock,
|
||||
/// Start time and clip being played
|
||||
pub play_clip: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
|
||||
/// Start time and next clip
|
||||
pub next_clip: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
|
||||
/// Play input through output.
|
||||
pub monitoring: bool,
|
||||
/// Write input to sequence.
|
||||
pub recording: bool,
|
||||
/// Overdub input to sequence.
|
||||
pub overdub: bool,
|
||||
/// Send all notes off
|
||||
pub reset: bool, // TODO?: after Some(nframes)
|
||||
/// Record from MIDI ports to current sequence.
|
||||
pub midi_ins: Vec<JackMidiIn>,
|
||||
/// Play from current sequence to MIDI ports
|
||||
pub midi_outs: Vec<JackMidiOut>,
|
||||
/// Notes currently held at input
|
||||
pub notes_in: Arc<RwLock<[bool; 128]>>,
|
||||
/// Notes currently held at output
|
||||
pub notes_out: Arc<RwLock<[bool; 128]>>,
|
||||
/// MIDI output buffer
|
||||
pub note_buf: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Default for MidiPlayer {
|
||||
fn default () -> Self {
|
||||
Self {
|
||||
play_clip: None,
|
||||
next_clip: None,
|
||||
recording: false,
|
||||
monitoring: false,
|
||||
overdub: false,
|
||||
|
||||
notes_in: RwLock::new([false;128]).into(),
|
||||
notes_out: RwLock::new([false;128]).into(),
|
||||
note_buf: vec![0;8],
|
||||
reset: true,
|
||||
|
||||
midi_ins: vec![],
|
||||
midi_outs: vec![],
|
||||
clock: Clock::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MidiPlayer {
|
||||
pub fn new (
|
||||
name: impl AsRef<str>,
|
||||
jack: &Jack,
|
||||
clock: Option<&Clock>,
|
||||
clip: Option<&Arc<RwLock<MidiClip>>>,
|
||||
midi_from: &[PortConnect],
|
||||
midi_to: &[PortConnect],
|
||||
) -> Usually<Self> {
|
||||
let _name = name.as_ref();
|
||||
let clock = clock.cloned().unwrap_or_default();
|
||||
Ok(Self {
|
||||
midi_ins: vec![JackMidiIn::new(jack, format!("M/{}", name.as_ref()), midi_from)?,],
|
||||
midi_outs: vec![JackMidiOut::new(jack, format!("{}/M", name.as_ref()), midi_to)?, ],
|
||||
play_clip: clip.map(|clip|(Moment::zero(&clock.timebase), Some(clip.clone()))),
|
||||
clock,
|
||||
note_buf: vec![0;8],
|
||||
reset: true,
|
||||
recording: false,
|
||||
monitoring: false,
|
||||
overdub: false,
|
||||
next_clip: None,
|
||||
notes_in: RwLock::new([false;128]).into(),
|
||||
notes_out: RwLock::new([false;128]).into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for MidiPlayer {
|
||||
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||
f.debug_struct("MidiPlayer")
|
||||
.field("clock", &self.clock)
|
||||
.field("play_clip", &self.play_clip)
|
||||
.field("next_clip", &self.next_clip)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
has_clock!(|self: MidiPlayer|self.clock);
|
||||
|
||||
impl HasMidiIns for MidiPlayer {
|
||||
fn midi_ins (&self) -> &Vec<JackMidiIn> { &self.midi_ins }
|
||||
fn midi_ins_mut (&mut self) -> &mut Vec<JackMidiIn> { &mut self.midi_ins }
|
||||
}
|
||||
|
||||
impl HasMidiOuts for MidiPlayer {
|
||||
fn midi_outs (&self) -> &Vec<JackMidiOut> { &self.midi_outs }
|
||||
fn midi_outs_mut (&mut self) -> &mut Vec<JackMidiOut> { &mut self.midi_outs }
|
||||
fn midi_note (&mut self) -> &mut Vec<u8> { &mut self.note_buf }
|
||||
}
|
||||
|
||||
/// Hosts the JACK callback for a single MIDI player
|
||||
pub struct PlayerAudio<'a, T: MidiPlayerApi>(
|
||||
/// Player
|
||||
pub &'a mut T,
|
||||
/// Note buffer
|
||||
pub &'a mut Vec<u8>,
|
||||
/// Note chunk buffer
|
||||
pub &'a mut Vec<Vec<Vec<u8>>>,
|
||||
);
|
||||
|
||||
/// JACK process callback for a sequencer's clip player/recorder.
|
||||
impl<T: MidiPlayerApi> Audio for PlayerAudio<'_, T> {
|
||||
fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
|
||||
let model = &mut self.0;
|
||||
let note_buf = &mut self.1;
|
||||
let midi_buf = &mut self.2;
|
||||
// Clear output buffer(s)
|
||||
model.clear(scope, midi_buf, false);
|
||||
// Write chunk of clip to output, handle switchover
|
||||
if model.play(scope, note_buf, midi_buf) {
|
||||
model.switchover(scope, note_buf, midi_buf);
|
||||
}
|
||||
if model.has_midi_ins() {
|
||||
if model.recording() || model.monitoring() {
|
||||
// Record and/or monitor input
|
||||
model.record(scope, midi_buf)
|
||||
} else if model.has_midi_outs() && model.monitoring() {
|
||||
// Monitor input to output
|
||||
model.monitor(scope, midi_buf)
|
||||
}
|
||||
}
|
||||
// Write to output port(s)
|
||||
model.write(scope, midi_buf);
|
||||
Control::Continue
|
||||
}
|
||||
}
|
||||
|
||||
impl MidiRecordApi for MidiPlayer {
|
||||
fn recording (&self) -> bool {
|
||||
self.recording
|
||||
}
|
||||
fn recording_mut (&mut self) -> &mut bool {
|
||||
&mut self.recording
|
||||
}
|
||||
fn monitoring (&self) -> bool {
|
||||
self.monitoring
|
||||
}
|
||||
fn monitoring_mut (&mut self) -> &mut bool {
|
||||
&mut self.monitoring
|
||||
}
|
||||
fn overdub (&self) -> bool {
|
||||
self.overdub
|
||||
}
|
||||
fn overdub_mut (&mut self) -> &mut bool {
|
||||
&mut self.overdub
|
||||
}
|
||||
fn notes_in (&self) -> &Arc<RwLock<[bool; 128]>> {
|
||||
&self.notes_in
|
||||
}
|
||||
}
|
||||
|
||||
impl MidiPlaybackApi for MidiPlayer {
|
||||
fn notes_out (&self) -> &Arc<RwLock<[bool; 128]>> {
|
||||
&self.notes_out
|
||||
}
|
||||
}
|
||||
|
||||
impl HasPlayClip for MidiPlayer {
|
||||
fn reset (&self) -> bool {
|
||||
self.reset
|
||||
}
|
||||
fn reset_mut (&mut self) -> &mut bool {
|
||||
&mut self.reset
|
||||
}
|
||||
fn play_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
|
||||
&self.play_clip
|
||||
}
|
||||
fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
|
||||
&mut self.play_clip
|
||||
}
|
||||
fn next_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
|
||||
&self.next_clip
|
||||
}
|
||||
fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
|
||||
&mut self.next_clip
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
mod mode_length; pub use self::mode_length::*;
|
||||
mod mode_rename; pub use self::mode_rename::*;
|
||||
mod mode_browse;
|
||||
|
||||
/// 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),
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
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
|
||||
});
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
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
|
||||
});
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
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!()
|
||||
});
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
mod piano_h; pub use self::piano_h::*;
|
||||
mod piano_v; pub use self::piano_v::*;
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
use crate::*;
|
||||
use Color::*;
|
||||
pub struct OctaveVertical {
|
||||
on: [bool; 12],
|
||||
colors: [Color; 3]
|
||||
}
|
||||
impl Default for OctaveVertical {
|
||||
fn default () -> Self {
|
||||
Self {
|
||||
on: [false; 12],
|
||||
colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)]
|
||||
}
|
||||
}
|
||||
}
|
||||
impl OctaveVertical {
|
||||
fn color (&self, pitch: usize) -> Color {
|
||||
let pitch = pitch % 12;
|
||||
self.colors[if self.on[pitch] { 2 } else {
|
||||
match pitch { 0 | 2 | 4 | 5 | 6 | 8 | 10 => 0, _ => 1 }
|
||||
}]
|
||||
}
|
||||
}
|
||||
impl Content<TuiOut> for OctaveVertical {
|
||||
fn content (&self) -> impl Render<TuiOut> {
|
||||
row!(
|
||||
Tui::fg_bg(self.color(0), self.color(1), "▙"),
|
||||
Tui::fg_bg(self.color(2), self.color(3), "▙"),
|
||||
Tui::fg_bg(self.color(4), self.color(5), "▌"),
|
||||
Tui::fg_bg(self.color(6), self.color(7), "▟"),
|
||||
Tui::fg_bg(self.color(8), self.color(9), "▟"),
|
||||
Tui::fg_bg(self.color(10), self.color(11), "▟"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
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);
|
||||
}
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
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 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();
|
||||
let monitoring = self.monitoring();
|
||||
for input in self.midi_ins_mut().iter() {
|
||||
for (sample, event, bytes) in parse_midi_input(input.port().iter(scope)) {
|
||||
if let LiveEvent::Midi { message, .. } = event {
|
||||
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);
|
||||
}
|
||||
if !self.clock().is_rolling() {
|
||||
return
|
||||
}
|
||||
if let Some((started, ref clip)) = self.play_clip().clone() {
|
||||
self.record_clip(scope, started, clip, midi_buf);
|
||||
}
|
||||
if let Some((_start_at, _clip)) = &self.next_clip() {
|
||||
self.record_next();
|
||||
}
|
||||
}
|
||||
|
||||
fn record_clip (
|
||||
&mut self,
|
||||
scope: &ProcessScope,
|
||||
started: Moment,
|
||||
clip: &Option<Arc<RwLock<MidiClip>>>,
|
||||
_midi_buf: &mut Vec<Vec<Vec<u8>>>
|
||||
) {
|
||||
if let Some(clip) = clip {
|
||||
let sample0 = scope.last_frame_time() as usize;
|
||||
let start = started.sample.get() as usize;
|
||||
let _recording = self.recording();
|
||||
let timebase = self.clock().timebase().clone();
|
||||
let quant = self.clock().quant.get();
|
||||
let mut clip = clip.write().unwrap();
|
||||
let length = clip.length;
|
||||
for input in self.midi_ins_mut().iter() {
|
||||
for (sample, event, _bytes) in parse_midi_input(input.port().iter(scope)) {
|
||||
if let LiveEvent::Midi { message, .. } = event {
|
||||
clip.record_event({
|
||||
let sample = (sample0 + sample - start) as f64;
|
||||
let pulse = timebase.samples_to_pulse(sample);
|
||||
let quantized = (pulse / quant).round() * quant;
|
||||
quantized as usize % length
|
||||
}, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn record_next (&mut self) {
|
||||
// TODO switch to next clip and record into it
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,164 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
/// Trait for thing that may output MIDI.
|
||||
pub trait HasMidiOuts {
|
||||
fn midi_outs (&self) -> &Vec<JackMidiOut>;
|
||||
|
||||
fn midi_outs_mut (&mut self) -> &mut Vec<JackMidiOut>;
|
||||
|
||||
fn has_midi_outs (&self) -> bool {
|
||||
!self.midi_outs().is_empty()
|
||||
}
|
||||
|
||||
/// Buffer for serializing a MIDI event. FIXME rename
|
||||
fn midi_note (&mut self) -> &mut Vec<u8>;
|
||||
}
|
||||
|
||||
pub trait MidiPlaybackApi: HasPlayClip + HasClock + HasMidiOuts {
|
||||
|
||||
fn notes_out (&self) -> &Arc<RwLock<[bool;128]>>;
|
||||
|
||||
/// Clear the section of the output buffer that we will be using,
|
||||
/// emitting "all notes off" at start of buffer if requested.
|
||||
fn clear (
|
||||
&mut self, scope: &ProcessScope, out: &mut [Vec<Vec<u8>>], reset: bool
|
||||
) {
|
||||
let n_frames = (scope.n_frames() as usize).min(out.len());
|
||||
for frame in &mut out[0..n_frames] {
|
||||
frame.clear();
|
||||
}
|
||||
if reset {
|
||||
all_notes_off(out);
|
||||
}
|
||||
}
|
||||
|
||||
/// Output notes from clip to MIDI output ports.
|
||||
fn play (
|
||||
&mut self, scope: &ProcessScope, note_buf: &mut Vec<u8>, out: &mut [Vec<Vec<u8>>]
|
||||
) -> bool {
|
||||
if !self.clock().is_rolling() {
|
||||
return false
|
||||
}
|
||||
// If a clip is playing, write a chunk of MIDI events from it to the output buffer.
|
||||
// If no clip is playing, prepare for switchover immediately.
|
||||
self.play_clip().as_ref().map_or(true, |(started, clip)|{
|
||||
self.play_chunk(scope, note_buf, out, started, clip)
|
||||
})
|
||||
}
|
||||
|
||||
/// Handle switchover from current to next playing clip.
|
||||
fn switchover (
|
||||
&mut self, scope: &ProcessScope, note_buf: &mut Vec<u8>, out: &mut [Vec<Vec<u8>>]
|
||||
) {
|
||||
if !self.clock().is_rolling() {
|
||||
return
|
||||
}
|
||||
let sample0 = scope.last_frame_time() as usize;
|
||||
//let samples = scope.n_frames() as usize;
|
||||
if let Some((start_at, clip)) = &self.next_clip() {
|
||||
let start = start_at.sample.get() as usize;
|
||||
let sample = self.clock().started.read().unwrap()
|
||||
.as_ref().unwrap().sample.get() as usize;
|
||||
// If it's time to switch to the next clip:
|
||||
if start <= sample0.saturating_sub(sample) {
|
||||
// Samples elapsed since clip was supposed to start
|
||||
let _skipped = sample0 - start;
|
||||
// Switch over to enqueued clip
|
||||
let started = Moment::from_sample(self.clock().timebase(), start as f64);
|
||||
// Launch enqueued clip
|
||||
*self.play_clip_mut() = Some((started, clip.clone()));
|
||||
// Unset enqueuement (TODO: where to implement looping?)
|
||||
*self.next_clip_mut() = None;
|
||||
// Fill in remaining ticks of chunk from next clip.
|
||||
self.play(scope, note_buf, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn play_chunk (
|
||||
&self,
|
||||
scope: &ProcessScope,
|
||||
note_buf: &mut Vec<u8>,
|
||||
out: &mut [Vec<Vec<u8>>],
|
||||
started: &Moment,
|
||||
clip: &Option<Arc<RwLock<MidiClip>>>
|
||||
) -> bool {
|
||||
// First sample to populate. Greater than 0 means that the first
|
||||
// pulse of the clip falls somewhere in the middle of the chunk.
|
||||
let sample = (scope.last_frame_time() as usize).saturating_sub(
|
||||
started.sample.get() as usize +
|
||||
self.clock().started.read().unwrap().as_ref().unwrap().sample.get() as usize
|
||||
);
|
||||
// Iterator that emits sample (index into output buffer at which to write MIDI event)
|
||||
// paired with pulse (index into clip from which to take the MIDI event) for each
|
||||
// sample of the output buffer that corresponds to a MIDI pulse.
|
||||
let pulses = self.clock().timebase().pulses_between_samples(sample, sample + scope.n_frames() as usize);
|
||||
// Notes active during current chunk.
|
||||
let notes = &mut self.notes_out().write().unwrap();
|
||||
let length = clip.as_ref().map_or(0, |p|p.read().unwrap().length);
|
||||
for (sample, pulse) in pulses {
|
||||
// If a next clip is enqueued, and we're past the end of the current one,
|
||||
// break the loop here (FIXME count pulse correctly)
|
||||
let past_end = if clip.is_some() { pulse >= length } else { true };
|
||||
if self.next_clip().is_some() && past_end {
|
||||
return true
|
||||
}
|
||||
// If there's a currently playing clip, output notes from it to buffer:
|
||||
if let Some(ref clip) = clip {
|
||||
Self::play_pulse(clip, pulse, sample, note_buf, out, notes)
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn play_pulse (
|
||||
clip: &RwLock<MidiClip>,
|
||||
pulse: usize,
|
||||
sample: usize,
|
||||
note_buf: &mut Vec<u8>,
|
||||
out: &mut [Vec<Vec<u8>>],
|
||||
notes: &mut [bool;128]
|
||||
) {
|
||||
// Source clip from which the MIDI events will be taken.
|
||||
let clip = clip.read().unwrap();
|
||||
// Clip with zero length is not processed
|
||||
if clip.length > 0 {
|
||||
// Current pulse index in source clip
|
||||
let pulse = pulse % clip.length;
|
||||
// Output each MIDI event from clip at appropriate frames of output buffer:
|
||||
for message in clip.notes[pulse].iter() {
|
||||
// Clear output buffer for this MIDI event.
|
||||
note_buf.clear();
|
||||
// TODO: support MIDI channels other than CH1.
|
||||
let channel = 0.into();
|
||||
// Serialize MIDI event into message buffer.
|
||||
LiveEvent::Midi { channel, message: *message }
|
||||
.write(note_buf)
|
||||
.unwrap();
|
||||
// Append serialized message to output buffer.
|
||||
out[sample].push(note_buf.clone());
|
||||
// Update the list of currently held notes.
|
||||
update_keys(&mut*notes, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a chunk of MIDI data from the output buffer to all assigned output ports.
|
||||
fn write (&mut self, scope: &ProcessScope, out: &[Vec<Vec<u8>>]) {
|
||||
let samples = scope.n_frames() as usize;
|
||||
for port in self.midi_outs_mut().iter_mut() {
|
||||
Self::write_port(&mut port.port_mut().writer(scope), samples, out)
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a chunk of MIDI data from the output buffer to an output port.
|
||||
fn write_port (writer: &mut MidiWriter, samples: usize, out: &[Vec<Vec<u8>>]) {
|
||||
for (time, events) in out.iter().enumerate().take(samples) {
|
||||
for bytes in events.iter() {
|
||||
writer.write(&RawMidi { time: time as u32, bytes }).unwrap_or_else(|_|{
|
||||
panic!("Failed to write MIDI data: {bytes:?}");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -62,18 +62,3 @@ impl NoteRange for MidiRangeModel {
|
|||
fn note_lo (&self) -> &AtomicUsize { &self.note_lo }
|
||||
fn note_axis (&self) -> &AtomicUsize { &self.note_axis }
|
||||
}
|
||||
|
||||
#[cfg(test)] #[test] fn test_midi_range () {
|
||||
let model = MidiRangeModel::from((1, false));
|
||||
|
||||
let _ = model.time_len();
|
||||
let _ = model.time_zoom();
|
||||
let _ = model.time_lock();
|
||||
let _ = model.time_start();
|
||||
let _ = model.time_axis();
|
||||
let _ = model.time_end();
|
||||
|
||||
let _ = model.note_lo();
|
||||
let _ = model.note_axis();
|
||||
let _ = model.note_hi();
|
||||
}
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
mod clock; pub use self::clock::*;
|
||||
|
||||
mod time_moment; pub use self::time_moment::*;
|
||||
mod time_note; pub use self::time_note::*;
|
||||
mod time_perf; pub use self::time_perf::*;
|
||||
|
|
@ -9,50 +7,3 @@ mod time_sample_rate; pub use self::time_sample_rate::*;
|
|||
mod time_timebase; pub use self::time_timebase::*;
|
||||
mod time_unit; pub use self::time_unit::*;
|
||||
mod time_usec; pub use self::time_usec::*;
|
||||
|
||||
pub(crate) use ::tek_jack::{*, jack::{*, contrib::*}};
|
||||
pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::*}};
|
||||
pub(crate) use std::ops::{Add, Sub, Mul, Div, Rem};
|
||||
pub(crate) use ::tengri::{input::*, dsl::*};
|
||||
pub use ::atomic_float; pub(crate) use atomic_float::*;
|
||||
|
||||
/// Standard result type.
|
||||
pub(crate) type Usually<T> = Result<T, Box<dyn std::error::Error>>;
|
||||
/// Standard optional result type.
|
||||
pub(crate) type Perhaps<T> = Result<Option<T>, Box<dyn std::error::Error>>;
|
||||
|
||||
pub trait Gettable<T> {
|
||||
/// Returns current value
|
||||
fn get (&self) -> T;
|
||||
}
|
||||
|
||||
pub trait Mutable<T>: Gettable<T> {
|
||||
/// Sets new value, returns old
|
||||
fn set (&mut self, value: T) -> T;
|
||||
}
|
||||
|
||||
pub trait InteriorMutable<T>: Gettable<T> {
|
||||
/// Sets new value, returns old
|
||||
fn set (&self, value: T) -> T;
|
||||
}
|
||||
|
||||
impl Gettable<bool> for AtomicBool {
|
||||
fn get (&self) -> bool { self.load(Relaxed) }
|
||||
}
|
||||
|
||||
impl InteriorMutable<bool> for AtomicBool {
|
||||
fn set (&self, value: bool) -> bool { self.swap(value, Relaxed) }
|
||||
}
|
||||
|
||||
impl Gettable<usize> for AtomicUsize {
|
||||
fn get (&self) -> usize { self.load(Relaxed) }
|
||||
}
|
||||
|
||||
impl InteriorMutable<usize> for AtomicUsize {
|
||||
fn set (&self, value: usize) -> usize { self.swap(value, Relaxed) }
|
||||
}
|
||||
|
||||
#[cfg(test)] #[test] fn test_time () -> Usually<()> {
|
||||
// TODO!
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use crate::*;
|
||||
use tengri::tui::PerfModel;
|
||||
use ::jack::ProcessScope;
|
||||
|
||||
pub trait JackPerfModel {
|
||||
fn update_from_jack_scope (&self, t0: Option<u64>, scope: &ProcessScope);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue