mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-04-04 05:10:44 +02:00
Compare commits
No commits in common. "817d2a722c56f89c3a45ac0d3549d988bbcd27bf" and "604a42a4bcb1b3e55f543c35b82e996970e9da7e" have entirely different histories.
817d2a722c
...
604a42a4bc
27 changed files with 6790 additions and 6639 deletions
67
Cargo.lock
generated
67
Cargo.lock
generated
|
|
@ -2642,6 +2642,8 @@ dependencies = [
|
||||||
"proptest-derive",
|
"proptest-derive",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"symphonia",
|
"symphonia",
|
||||||
|
"tek_device",
|
||||||
|
"tek_engine",
|
||||||
"tengri",
|
"tengri",
|
||||||
"toml",
|
"toml",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
|
@ -2650,6 +2652,43 @@ dependencies = [
|
||||||
"xdg",
|
"xdg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tek_device"
|
||||||
|
version = "0.3.0"
|
||||||
|
dependencies = [
|
||||||
|
"atomic_float",
|
||||||
|
"backtrace",
|
||||||
|
"clap",
|
||||||
|
"jack",
|
||||||
|
"konst",
|
||||||
|
"livi",
|
||||||
|
"midly",
|
||||||
|
"palette",
|
||||||
|
"proptest",
|
||||||
|
"proptest-derive",
|
||||||
|
"rand 0.8.5",
|
||||||
|
"symphonia",
|
||||||
|
"tek_engine",
|
||||||
|
"tengri",
|
||||||
|
"toml",
|
||||||
|
"uuid",
|
||||||
|
"wavers",
|
||||||
|
"winit",
|
||||||
|
"xdg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tek_engine"
|
||||||
|
version = "0.3.0"
|
||||||
|
dependencies = [
|
||||||
|
"atomic_float",
|
||||||
|
"jack",
|
||||||
|
"midly",
|
||||||
|
"proptest",
|
||||||
|
"proptest-derive",
|
||||||
|
"tengri",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.25.0"
|
version = "3.25.0"
|
||||||
|
|
@ -2666,6 +2705,32 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tengri"
|
name = "tengri"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
|
dependencies = [
|
||||||
|
"dizzle",
|
||||||
|
"tengri_input",
|
||||||
|
"tengri_output",
|
||||||
|
"tengri_tui",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tengri_input"
|
||||||
|
version = "0.14.0"
|
||||||
|
dependencies = [
|
||||||
|
"dizzle",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tengri_output"
|
||||||
|
version = "0.14.0"
|
||||||
|
dependencies = [
|
||||||
|
"atomic_float",
|
||||||
|
"dizzle",
|
||||||
|
"quanta",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tengri_tui"
|
||||||
|
version = "0.14.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atomic_float",
|
"atomic_float",
|
||||||
"better-panic",
|
"better-panic",
|
||||||
|
|
@ -2675,6 +2740,8 @@ dependencies = [
|
||||||
"quanta",
|
"quanta",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
|
"tengri_input",
|
||||||
|
"tengri_output",
|
||||||
"unicode-width 0.2.0",
|
"unicode-width 0.2.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [ "./app" ]
|
members = [ "./app", "./engine", "./device" ]
|
||||||
exclude = [ "./tengri", "./dizzle" ]
|
exclude = [ "./tengri", "./dizzle" ]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
|
|
|
||||||
25
Justfile
25
Justfile
|
|
@ -119,6 +119,7 @@ plugin:
|
||||||
|
|
||||||
@stats:
|
@stats:
|
||||||
echo
|
echo
|
||||||
|
|
||||||
for file in $(ls app/*.rs); do echo $file; cloc $file | grep Rust; done
|
for file in $(ls app/*.rs); do echo $file; cloc $file | grep Rust; done
|
||||||
echo
|
echo
|
||||||
cloc app | grep Rust
|
cloc app | grep Rust
|
||||||
|
|
@ -130,3 +131,27 @@ plugin:
|
||||||
rg 'TODO' app/ | cat
|
rg 'TODO' app/ | cat
|
||||||
rg 'TODO' app/ | wc -l
|
rg 'TODO' app/ | wc -l
|
||||||
echo
|
echo
|
||||||
|
|
||||||
|
for file in $(ls engine/*.rs); do echo $file; cloc $file | grep Rust; done
|
||||||
|
echo
|
||||||
|
cloc engine | grep Rust
|
||||||
|
rg 'pub struct ' engine/ | cat
|
||||||
|
rg 'pub struct ' engine/ | wc -l
|
||||||
|
rg 'pub trait ' engine/ | cat
|
||||||
|
rg 'pub trait ' engine/ | wc -l
|
||||||
|
echo
|
||||||
|
rg 'TODO' engine/ | cat
|
||||||
|
rg 'TODO' engine/ | wc -l
|
||||||
|
echo
|
||||||
|
|
||||||
|
for file in $(ls device/*.rs); do echo $file; cloc $file | grep Rust; done
|
||||||
|
echo
|
||||||
|
cloc device | grep Rust
|
||||||
|
rg 'pub struct ' device/ | cat
|
||||||
|
rg 'pub struct ' device/ | wc -l
|
||||||
|
rg 'pub trait ' device/ | cat
|
||||||
|
rg 'pub trait ' device/ | wc -l
|
||||||
|
echo
|
||||||
|
rg 'TODO' device/ | cat
|
||||||
|
rg 'TODO' device/ | wc -l
|
||||||
|
echo
|
||||||
|
|
|
||||||
699
app/.scratch.rs
699
app/.scratch.rs
|
|
@ -377,702 +377,3 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//fn begin (browse: &mut Browse) => {
|
|
||||||
//unreachable!();
|
|
||||||
//}
|
|
||||||
//fn cancel (browse: &mut Browse) => {
|
|
||||||
//todo!()
|
|
||||||
////browse.mode = None;
|
|
||||||
////Ok(None)
|
|
||||||
//}
|
|
||||||
//fn confirm (browse: &mut Browse) => {
|
|
||||||
//todo!()
|
|
||||||
////Ok(match browse.mode {
|
|
||||||
////Some(PoolMode::Import(index, ref mut browse)) => {
|
|
||||||
////if browse.is_file() {
|
|
||||||
////let path = browse.path();
|
|
||||||
////browse.mode = None;
|
|
||||||
////let _undo = PoolClipCommand::import(browse, index, path)?;
|
|
||||||
////None
|
|
||||||
////} else if browse.is_dir() {
|
|
||||||
////browse.mode = Some(PoolMode::Import(index, browse.chdir()?));
|
|
||||||
////None
|
|
||||||
////} else {
|
|
||||||
////None
|
|
||||||
////}
|
|
||||||
////},
|
|
||||||
////Some(PoolMode::Export(index, ref mut browse)) => {
|
|
||||||
////todo!()
|
|
||||||
////},
|
|
||||||
////_ => unreachable!(),
|
|
||||||
////})
|
|
||||||
//}
|
|
||||||
//fn select (browse: &mut Browse, index: usize) => {
|
|
||||||
//todo!()
|
|
||||||
////Ok(match browse.mode {
|
|
||||||
////Some(PoolMode::Import(index, ref mut browse)) => {
|
|
||||||
////browse.index = index;
|
|
||||||
////None
|
|
||||||
////},
|
|
||||||
////Some(PoolMode::Export(index, ref mut browse)) => {
|
|
||||||
////browse.index = index;
|
|
||||||
////None
|
|
||||||
////},
|
|
||||||
////_ => unreachable!(),
|
|
||||||
////})
|
|
||||||
//}
|
|
||||||
//fn chdir (browse: &mut Browse, dir: PathBuf) => {
|
|
||||||
//todo!()
|
|
||||||
////Ok(match browse.mode {
|
|
||||||
////Some(PoolMode::Import(index, ref mut browse)) => {
|
|
||||||
////browse.mode = Some(PoolMode::Import(index, Browse::new(Some(dir))?));
|
|
||||||
////None
|
|
||||||
////},
|
|
||||||
////Some(PoolMode::Export(index, ref mut browse)) => {
|
|
||||||
////browse.mode = Some(PoolMode::Export(index, Browse::new(Some(dir))?));
|
|
||||||
////None
|
|
||||||
////},
|
|
||||||
////_ => unreachable!(),
|
|
||||||
////})
|
|
||||||
//}
|
|
||||||
//fn filter (browse: &mut Browse, filter: Arc<str>) => {
|
|
||||||
//todo!()
|
|
||||||
//}
|
|
||||||
|
|
||||||
|
|
||||||
//def_command!(ArrangementCommand: |arranger: Arrangement| {
|
|
||||||
|
|
||||||
//Home => { arranger.editor = None; Ok(None) },
|
|
||||||
|
|
||||||
//Edit => {
|
|
||||||
//let selection = arranger.selection().clone();
|
|
||||||
//arranger.editor = if arranger.editor.is_some() {
|
|
||||||
//None
|
|
||||||
//} else {
|
|
||||||
//match selection {
|
|
||||||
//Selection::TrackClip { track, scene } => {
|
|
||||||
//let clip = &mut arranger.scenes_mut()[scene].clips[track];
|
|
||||||
//if clip.is_none() {
|
|
||||||
////app.clip_auto_create();
|
|
||||||
//*clip = Some(Arc::new(RwLock::new(MidiClip::new(
|
|
||||||
//&format!("t{track:02}s{scene:02}"),
|
|
||||||
//false, 384, None, Some(ItemTheme::random())
|
|
||||||
//))));
|
|
||||||
//}
|
|
||||||
//clip.as_ref().map(|c|c.into())
|
|
||||||
//}
|
|
||||||
//_ => {
|
|
||||||
//None
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//};
|
|
||||||
//if let Some(editor) = arranger.editor.as_mut() {
|
|
||||||
//if let Some(clip) = editor.clip() {
|
|
||||||
//let length = clip.read().unwrap().length.max(1);
|
|
||||||
//let width = arranger.size_inner.w().saturating_sub(20).max(1);
|
|
||||||
//editor.set_time_zoom(length / width);
|
|
||||||
//editor.redraw();
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//Ok(None)
|
|
||||||
//},
|
|
||||||
|
|
||||||
////// Set the selection
|
|
||||||
//Select { selection: Selection } => { *arranger.selection_mut() = *selection; Ok(None) },
|
|
||||||
|
|
||||||
////// Launch the selected clip or scene
|
|
||||||
//Launch => {
|
|
||||||
//match *arranger.selection() {
|
|
||||||
//Selection::Track(t) => {
|
|
||||||
//arranger.tracks[t].sequencer.enqueue_next(None)
|
|
||||||
//},
|
|
||||||
//Selection::TrackClip { track, scene } => {
|
|
||||||
//arranger.tracks[track].sequencer.enqueue_next(arranger.scenes[scene].clips[track].as_ref())
|
|
||||||
//},
|
|
||||||
//Selection::Scene(s) => {
|
|
||||||
//for t in 0..arranger.tracks.len() {
|
|
||||||
//arranger.tracks[t].sequencer.enqueue_next(arranger.scenes[s].clips[t].as_ref())
|
|
||||||
//}
|
|
||||||
//},
|
|
||||||
//_ => {}
|
|
||||||
//};
|
|
||||||
//Ok(None)
|
|
||||||
//},
|
|
||||||
|
|
||||||
////// Set the color of the selected entity
|
|
||||||
//SetColor { palette: Option<ItemTheme> } => {
|
|
||||||
//let mut palette = palette.unwrap_or_else(||ItemTheme::random());
|
|
||||||
//let selection = *arranger.selection();
|
|
||||||
//Ok(Some(Self::SetColor { palette: Some(match selection {
|
|
||||||
//Selection::Mix => {
|
|
||||||
//std::mem::swap(&mut palette, &mut arranger.color);
|
|
||||||
//palette
|
|
||||||
//},
|
|
||||||
//Selection::Scene(s) => {
|
|
||||||
//std::mem::swap(&mut palette, &mut arranger.scenes[s].color);
|
|
||||||
//palette
|
|
||||||
//}
|
|
||||||
//Selection::Track(t) => {
|
|
||||||
//std::mem::swap(&mut palette, &mut arranger.tracks[t].color);
|
|
||||||
//palette
|
|
||||||
//}
|
|
||||||
//Selection::TrackClip { track, scene } => {
|
|
||||||
//if let Some(ref clip) = arranger.scenes[scene].clips[track] {
|
|
||||||
//let mut clip = clip.write().unwrap();
|
|
||||||
//std::mem::swap(&mut palette, &mut clip.color);
|
|
||||||
//palette
|
|
||||||
//} else {
|
|
||||||
//return Ok(None)
|
|
||||||
//}
|
|
||||||
//},
|
|
||||||
//_ => todo!()
|
|
||||||
//}) }))
|
|
||||||
//},
|
|
||||||
|
|
||||||
//Track { track: TrackCommand } => { todo!("delegate") },
|
|
||||||
|
|
||||||
//TrackAdd => {
|
|
||||||
//let index = arranger.track_add(None, None, &[], &[])?.0;
|
|
||||||
//*arranger.selection_mut() = match arranger.selection() {
|
|
||||||
//Selection::Track(_) => Selection::Track(index),
|
|
||||||
//Selection::TrackClip { track: _, scene } => Selection::TrackClip {
|
|
||||||
//track: index, scene: *scene
|
|
||||||
//},
|
|
||||||
//_ => *arranger.selection()
|
|
||||||
//};
|
|
||||||
//Ok(Some(Self::TrackDelete { index }))
|
|
||||||
//},
|
|
||||||
|
|
||||||
//TrackSwap { index: usize, other: usize } => {
|
|
||||||
//let index = *index;
|
|
||||||
//let other = *other;
|
|
||||||
//Ok(Some(Self::TrackSwap { index, other }))
|
|
||||||
//},
|
|
||||||
|
|
||||||
//TrackDelete { index: usize } => {
|
|
||||||
//let index = *index;
|
|
||||||
//let exists = arranger.tracks().get(index).is_some();
|
|
||||||
//if exists {
|
|
||||||
//let track = arranger.tracks_mut().remove(index);
|
|
||||||
//let Track { sequencer: Sequencer { midi_ins, midi_outs, .. }, .. } = track;
|
|
||||||
//for port in midi_ins.into_iter() {
|
|
||||||
//port.close()?;
|
|
||||||
//}
|
|
||||||
//for port in midi_outs.into_iter() {
|
|
||||||
//port.close()?;
|
|
||||||
//}
|
|
||||||
//for scene in arranger.scenes_mut().iter_mut() {
|
|
||||||
//scene.clips.remove(index);
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//Ok(None)
|
|
||||||
////TODO:Ok(Some(Self::TrackAdd ( index, track: Some(deleted_track) })
|
|
||||||
//},
|
|
||||||
|
|
||||||
//MidiIn { input: MidiInputCommand } => {
|
|
||||||
//todo!("delegate"); Ok(None)
|
|
||||||
//},
|
|
||||||
|
|
||||||
//MidiInAdd => {
|
|
||||||
//arranger.midi_in_add()?;
|
|
||||||
//Ok(None)
|
|
||||||
//},
|
|
||||||
|
|
||||||
//MidiOut { output: MidiOutputCommand } => {
|
|
||||||
//todo!("delegate");
|
|
||||||
//Ok(None)
|
|
||||||
//},
|
|
||||||
|
|
||||||
//MidiOutAdd => {
|
|
||||||
//arranger.midi_out_add()?;
|
|
||||||
//Ok(None)
|
|
||||||
//},
|
|
||||||
|
|
||||||
//Device { command: DeviceCommand } => {
|
|
||||||
//todo!("delegate");
|
|
||||||
//Ok(None)
|
|
||||||
//},
|
|
||||||
|
|
||||||
//DeviceAdd { index: usize } => {
|
|
||||||
//todo!("delegate");
|
|
||||||
//Ok(None)
|
|
||||||
//},
|
|
||||||
|
|
||||||
//Scene { scene: SceneCommand } => {
|
|
||||||
//todo!("delegate");
|
|
||||||
//Ok(None)
|
|
||||||
//},
|
|
||||||
|
|
||||||
//OutputAdd => {
|
|
||||||
//arranger.midi_outs.push(MidiOutput::new(
|
|
||||||
//arranger.jack(),
|
|
||||||
//&format!("/M{}", arranger.midi_outs.len() + 1),
|
|
||||||
//&[]
|
|
||||||
//)?);
|
|
||||||
//Ok(None)
|
|
||||||
//},
|
|
||||||
|
|
||||||
//InputAdd => {
|
|
||||||
//arranger.midi_ins.push(MidiInput::new(
|
|
||||||
//arranger.jack(),
|
|
||||||
//&format!("M{}/", arranger.midi_ins.len() + 1),
|
|
||||||
//&[]
|
|
||||||
//)?);
|
|
||||||
//Ok(None)
|
|
||||||
//},
|
|
||||||
|
|
||||||
//SceneAdd => {
|
|
||||||
//let index = arranger.scene_add(None, None)?.0;
|
|
||||||
//*arranger.selection_mut() = match arranger.selection() {
|
|
||||||
//Selection::Scene(_) => Selection::Scene(index),
|
|
||||||
//Selection::TrackClip { track, scene } => Selection::TrackClip {
|
|
||||||
//track: *track,
|
|
||||||
//scene: index
|
|
||||||
//},
|
|
||||||
//_ => *arranger.selection()
|
|
||||||
//};
|
|
||||||
//Ok(None) // TODO
|
|
||||||
//},
|
|
||||||
|
|
||||||
//SceneSwap { index: usize, other: usize } => {
|
|
||||||
//let index = *index;
|
|
||||||
//let other = *other;
|
|
||||||
//Ok(Some(Self::SceneSwap { index, other }))
|
|
||||||
//},
|
|
||||||
|
|
||||||
//SceneDelete { index: usize } => {
|
|
||||||
//let index = *index;
|
|
||||||
//let scenes = arranger.scenes_mut();
|
|
||||||
//Ok(if scenes.get(index).is_some() {
|
|
||||||
//let _scene = scenes.remove(index);
|
|
||||||
//None
|
|
||||||
//} else {
|
|
||||||
//None
|
|
||||||
//})
|
|
||||||
//},
|
|
||||||
|
|
||||||
//SceneLaunch { index: usize } => {
|
|
||||||
//let index = *index;
|
|
||||||
//for track in 0..arranger.tracks.len() {
|
|
||||||
//let clip = arranger.scenes[index].clips[track].as_ref();
|
|
||||||
//arranger.tracks[track].sequencer.enqueue_next(clip);
|
|
||||||
//}
|
|
||||||
//Ok(None)
|
|
||||||
//},
|
|
||||||
|
|
||||||
//Clip { scene: ClipCommand } => {
|
|
||||||
//todo!("delegate")
|
|
||||||
//},
|
|
||||||
|
|
||||||
//ClipGet { a: usize, b: usize } => {
|
|
||||||
////(Get [a: usize, b: usize] cmd_todo!("\n\rtodo: clip: get: {a} {b}"))
|
|
||||||
////("get" [a: usize, b: usize] Some(Self::Get(a.unwrap(), b.unwrap())))
|
|
||||||
//todo!()
|
|
||||||
//},
|
|
||||||
|
|
||||||
//ClipPut { a: usize, b: usize } => {
|
|
||||||
////(Put [t: usize, s: usize, c: MaybeClip]
|
|
||||||
////Some(Self::Put(t, s, arranger.clip_put(t, s, c))))
|
|
||||||
////("put" [a: usize, b: usize, c: MaybeClip] Some(Self::Put(a.unwrap(), b.unwrap(), c.unwrap())))
|
|
||||||
//todo!()
|
|
||||||
//},
|
|
||||||
|
|
||||||
//ClipDel { a: usize, b: usize } => {
|
|
||||||
////("delete" [a: usize, b: usize] Some(Self::Put(a.unwrap(), b.unwrap(), None))))
|
|
||||||
//todo!()
|
|
||||||
//},
|
|
||||||
|
|
||||||
//ClipEnqueue { a: usize, b: usize } => {
|
|
||||||
////(Enqueue [t: usize, s: usize]
|
|
||||||
////cmd!(arranger.tracks[t].sequencer.enqueue_next(arranger.scenes[s].clips[t].as_ref())))
|
|
||||||
////("enqueue" [a: usize, b: usize] Some(Self::Enqueue(a.unwrap(), b.unwrap())))
|
|
||||||
//todo!()
|
|
||||||
//},
|
|
||||||
|
|
||||||
//ClipSwap { a: usize, b: usize }=> {
|
|
||||||
////(Edit [clip: MaybeClip] cmd_todo!("\n\rtodo: clip: edit: {clip:?}"))
|
|
||||||
////("edit" [a: MaybeClip] Some(Self::Edit(a.unwrap())))
|
|
||||||
//todo!()
|
|
||||||
//},
|
|
||||||
|
|
||||||
//});
|
|
||||||
// Update sequencer playhead indicator
|
|
||||||
//self.now().set(0.);
|
|
||||||
//if let Some((ref started_at, Some(ref playing))) = self.sequencer.play_clip {
|
|
||||||
//let clip = clip.read().unwrap();
|
|
||||||
//if *playing.read().unwrap() == *clip {
|
|
||||||
//let pulse = self.current().pulse.get();
|
|
||||||
//let start = started_at.pulse.get();
|
|
||||||
//let now = (pulse - start) % clip.length as f64;
|
|
||||||
//self.now().set(now);
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//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)
|
|
||||||
//}
|
|
||||||
//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)
|
|
||||||
//});
|
|
||||||
|
|
||||||
//pub struct LV2PluginUI {
|
|
||||||
//write: (),
|
|
||||||
//controller: (),
|
|
||||||
//widget: (),
|
|
||||||
//features: (),
|
|
||||||
//transfer: (),
|
|
||||||
//}
|
|
||||||
//take!(BrowseCommand |state: Pool, iter|Ok(state.browse.as_ref()
|
|
||||||
//.map(|p|Take::take(p, iter))
|
|
||||||
//.transpose()?
|
|
||||||
//.flatten()));
|
|
||||||
|
|
||||||
//fn file_browser_filter (&self) -> Arc<str> {
|
|
||||||
//todo!()
|
|
||||||
//}
|
|
||||||
//fn file_browser_path (&self) -> PathBuf {
|
|
||||||
//todo!();
|
|
||||||
//}
|
|
||||||
///// Immutable reference to sample at cursor.
|
|
||||||
//fn sample_selected (&self) -> Option<Arc<RwLock<Sample>>> {
|
|
||||||
//for (i, sample) in self.mapped.iter().enumerate() {
|
|
||||||
//if i == self.cursor().0 {
|
|
||||||
//return sample.as_ref()
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//for (i, sample) in self.unmapped.iter().enumerate() {
|
|
||||||
//if i + self.mapped.len() == self.cursor().0 {
|
|
||||||
//return Some(sample)
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//None
|
|
||||||
//}
|
|
||||||
//fn sample_gain (&self) -> f32 {
|
|
||||||
//todo!()
|
|
||||||
//}
|
|
||||||
//fn sample_above () -> usize {
|
|
||||||
//self.note_pos().min(119) + 8
|
|
||||||
//}
|
|
||||||
//fn sample_below () -> usize {
|
|
||||||
//self.note_pos().max(8) - 8
|
|
||||||
//}
|
|
||||||
//fn sample_to_left () -> usize {
|
|
||||||
//self.note_pos().min(126) + 1
|
|
||||||
//}
|
|
||||||
//fn sample_to_right () -> usize {
|
|
||||||
//self.note_pos().max(1) - 1
|
|
||||||
//}
|
|
||||||
//fn selected_pitch () -> u7 {
|
|
||||||
//(self.note_pos() as u8).into() // TODO
|
|
||||||
//}
|
|
||||||
|
|
||||||
//select (&self, state: &mut Sampler, i: usize) -> Option<Self> {
|
|
||||||
//Self::Select(state.set_note_pos(i))
|
|
||||||
//}
|
|
||||||
///// Assign sample to slot
|
|
||||||
//set (&self, slot: u7, sample: Option<Arc<RwLock<Sample>>>) -> Option<Self> {
|
|
||||||
//let i = slot.as_int() as usize;
|
|
||||||
//let old = self.mapped[i].clone();
|
|
||||||
//self.mapped[i] = sample;
|
|
||||||
//Some(Self::Set(old))
|
|
||||||
//}
|
|
||||||
//set_start (&self, state: &mut Sampler, slot: u7, frame: usize) -> Option<Self> {
|
|
||||||
//todo!()
|
|
||||||
//}
|
|
||||||
//set_gain (&self, state: &mut Sampler, slot: u7, g: f32) -> Option<Self> {
|
|
||||||
//todo!()
|
|
||||||
//}
|
|
||||||
//note_on (&self, state: &mut Sampler, slot: u7, v: u7) -> Option<Self> {
|
|
||||||
//todo!()
|
|
||||||
//}
|
|
||||||
//note_off (&self, state: &mut Sampler, slot: u7) -> Option<Self> {
|
|
||||||
//todo!()
|
|
||||||
//}
|
|
||||||
//set_sample (&self, state: &mut Sampler, slot: u7, s: Option<Arc<RwLock<Sample>>>) -> Option<Self> {
|
|
||||||
//Some(Self::SetSample(p, state.set_sample(p, s)))
|
|
||||||
//}
|
|
||||||
//import (&self, state: &mut Sampler, c: FileBrowserCommand) -> Option<Self> {
|
|
||||||
//match c {
|
|
||||||
//FileBrowserCommand::Begin => {
|
|
||||||
////let voices = &state.state.voices;
|
|
||||||
////let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![])));
|
|
||||||
//state.mode = Some(SamplerMode::Import(0, FileBrowser::new(None)?));
|
|
||||||
//None
|
|
||||||
//},
|
|
||||||
//_ => {
|
|
||||||
//println!("\n\rtodo: import: filebrowser: {c:?}");
|
|
||||||
//None
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
////(Select [i: usize] Some(Self::Select(state.set_note_pos(i))))
|
|
||||||
////(RecordBegin [p: u7] cmd!(state.begin_recording(p.as_int() as usize)))
|
|
||||||
////(RecordCancel [] cmd!(state.cancel_recording()))
|
|
||||||
////(RecordFinish [] cmd!(state.finish_recording()))
|
|
||||||
////(SetStart [p: u7, frame: usize] cmd_todo!("\n\rtodo: {self:?}"))
|
|
||||||
////(SetGain [p: u7, gain: f32] cmd_todo!("\n\rtodo: {self:?}"))
|
|
||||||
////(NoteOn [p: u7, velocity: u7] cmd_todo!("\n\rtodo: {self:?}"))
|
|
||||||
////(NoteOff [p: u7] cmd_todo!("\n\rtodo: {self:?}"))
|
|
||||||
////(SetSample [p: u7, s: Option<Arc<RwLock<Sample>>>] Some(Self::SetSample(p, state.set_sample(p, s))))
|
|
||||||
////(Import [c: FileBrowserCommand] match c {
|
|
||||||
////FileBrowserCommand::Begin => {
|
|
||||||
//////let voices = &state.state.voices;
|
|
||||||
//////let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![])));
|
|
||||||
////state.mode = Some(SamplerMode::Import(0, FileBrowser::new(None)?));
|
|
||||||
////None
|
|
||||||
////},
|
|
||||||
////_ => {
|
|
||||||
////println!("\n\rtodo: import: filebrowser: {c:?}");
|
|
||||||
////None
|
|
||||||
////}
|
|
||||||
////})));
|
|
||||||
////("import" [,..a]
|
|
||||||
////FileBrowserCommand::try_from_expr(state, a).map(Self::Import))
|
|
||||||
////("select" [i: usize]
|
|
||||||
////Some(Self::Select(i.expect("no index"))))
|
|
||||||
////("record/begin" [i: u7]
|
|
||||||
////Some(Self::RecordBegin(i.expect("no index"))))
|
|
||||||
////("record/cancel" []
|
|
||||||
////Some(Self::RecordCancel))
|
|
||||||
////("record/finish" []
|
|
||||||
////Some(Self::RecordFinish))
|
|
||||||
////("set/sample" [i: u7, s: Option<Arc<RwLock<Sample>>>]
|
|
||||||
////Some(Self::SetSample(i.expect("no index"), s.expect("no sampler"))))
|
|
||||||
////("set/start" [i: u7, s: usize]
|
|
||||||
////Some(Self::SetStart(i.expect("no index"), s.expect("no start"))))
|
|
||||||
////("set/gain" [i: u7, g: f32]
|
|
||||||
////Some(Self::SetGain(i.expect("no index"), g.expect("no gain"))))
|
|
||||||
////("note/on" [p: u7, v: u7]
|
|
||||||
////Some(Self::NoteOn(p.expect("no slot"), v.expect("no velocity"))))
|
|
||||||
////("note/off" [p: u7]
|
|
||||||
////Some(Self::NoteOff(p.expect("no slot"))))));
|
|
||||||
|
|
||||||
// TODO:
|
|
||||||
//for port in midi_in.iter() {
|
|
||||||
//for event in port.iter() {
|
|
||||||
//match event {
|
|
||||||
//(time, Ok(LiveEvent::Midi {message, ..})) => match message {
|
|
||||||
//MidiMessage::NoteOn {ref key, ..} if let Some(editor) = self.editor.as_ref() => {
|
|
||||||
//editor.set_note_pos(key.as_int() as usize);
|
|
||||||
//},
|
|
||||||
//MidiMessage::Controller {controller, value} if let (Some(editor), Some(sampler)) = (
|
|
||||||
//self.editor.as_ref(),
|
|
||||||
//self.sampler.as_ref(),
|
|
||||||
//) => {
|
|
||||||
//// TODO: give sampler its own cursor
|
|
||||||
//if let Some(sample) = &sampler.mapped[editor.note_pos()] {
|
|
||||||
//sample.write().unwrap().handle_cc(*controller, *value)
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//_ =>{}
|
|
||||||
//},
|
|
||||||
//_ =>{}
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
|
|
||||||
|
|
||||||
//scene_scroll: Fill::Y(Fixed::X(1, ScrollbarV {
|
|
||||||
//offset: arrangement.scene_scroll,
|
|
||||||
//length: h_scenes_area as usize,
|
|
||||||
//total: h_scenes as usize,
|
|
||||||
//})),
|
|
||||||
//take!(SceneCommand |state: Arrangement, iter|state.selected_scene().as_ref()
|
|
||||||
//.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten()));
|
|
||||||
|
|
||||||
//pub(crate) fn io_conns <'a, T: PortsSizes<'a>> (
|
|
||||||
//fg: Color, bg: Color, iter: &mut impl Iterator<Item = (usize, &'a Arc<str>, &'a [Connect], usize, usize)>
|
|
||||||
//) -> impl Content<TuiOut> + 'a {
|
|
||||||
//Fill::XY(Thunk::new(move|to: &mut TuiOut|for (_, _, connections, y, y2) in &mut *iter {
|
|
||||||
//to.place(&map_south(y as u16, (y2-y) as u16, Bsp::s(
|
|
||||||
//Fill::Y(Tui::bold(true, wrap(bg, fg, Fill::Y(Align::w(&"▞▞▞▞ ▞▞▞▞"))))),
|
|
||||||
//Thunk::new(|to: &mut TuiOut|for (index, _connection) in connections.iter().enumerate() {
|
|
||||||
//to.place(&map_south(index as u16, 1, Fill::Y(Align::w(Tui::bold(false,
|
|
||||||
//wrap(bg, fg, Fill::Y(&"")))))))
|
|
||||||
//})
|
|
||||||
//)))
|
|
||||||
//}))
|
|
||||||
//}
|
|
||||||
//track_scroll: Fill::Y(Fixed::Y(1, ScrollbarH {
|
|
||||||
//offset: arrangement.track_scroll,
|
|
||||||
//length: h_tracks_area as usize,
|
|
||||||
//total: h_scenes as usize,
|
|
||||||
//})),
|
|
||||||
//take!(TrackCommand |state: Arrangement, iter|state.selected_track().as_ref()
|
|
||||||
//.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten()));
|
|
||||||
|
|
||||||
|
|
||||||
//macro_rules! impl_port {
|
|
||||||
//($Name:ident : $Spec:ident -> $Pair:ident |$jack:ident, $name:ident|$port:expr) => {
|
|
||||||
//#[derive(Debug)] pub struct $Name {
|
|
||||||
///// Handle to JACK client, for receiving reconnect events.
|
|
||||||
//jack: Jack<'static>,
|
|
||||||
///// Port name
|
|
||||||
//name: Arc<str>,
|
|
||||||
///// Port handle.
|
|
||||||
//port: Port<$Spec>,
|
|
||||||
///// List of ports to connect to.
|
|
||||||
//conn: Vec<PortConnect>
|
|
||||||
//}
|
|
||||||
//impl AsRef<Port<$Spec>> for $Name {
|
|
||||||
//fn as_ref (&self) -> &Port<$Spec> { &self.port }
|
|
||||||
//}
|
|
||||||
//impl $Name {
|
|
||||||
//pub fn new ($jack: &Jack, name: impl AsRef<str>, connect: &[PortConnect])
|
|
||||||
//-> Usually<Self>
|
|
||||||
//{
|
|
||||||
//let $name = name.as_ref();
|
|
||||||
//let jack = $jack.clone();
|
|
||||||
//let port = $port?;
|
|
||||||
//let name = $name.into();
|
|
||||||
//let conn = connect.to_vec();
|
|
||||||
//let port = Self { jack, port, name, conn };
|
|
||||||
//port.connect_to_matching()?;
|
|
||||||
//Ok(port)
|
|
||||||
//}
|
|
||||||
//pub fn name (&self) -> &Arc<str> { &self.name }
|
|
||||||
//pub fn port (&self) -> &Port<$Spec> { &self.port }
|
|
||||||
//pub fn port_mut (&mut self) -> &mut Port<$Spec> { &mut self.port }
|
|
||||||
//pub fn into_port (self) -> Port<$Spec> { self.port }
|
|
||||||
//pub fn close (self) -> Usually<()> {
|
|
||||||
//let Self { jack, port, .. } = self;
|
|
||||||
//Ok(jack.with_client(|client|client.unregister_port(port))?)
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//impl HasJack<'static> for $Name {
|
|
||||||
//fn jack (&self) -> &'static Jack<'static> { &self.jack }
|
|
||||||
//}
|
|
||||||
//impl JackPort<'static> for $Name {
|
|
||||||
//type Port = $Spec;
|
|
||||||
//type Pair = $Pair;
|
|
||||||
//fn port (&self) -> &Port<$Spec> { &self.port }
|
|
||||||
//}
|
|
||||||
//impl ConnectTo<'static, &str> for $Name {
|
|
||||||
//fn connect_to (&self, to: &str) -> Usually<PortConnectStatus> {
|
|
||||||
//self.with_client(|c|if let Some(ref port) = c.port_by_name(to.as_ref()) {
|
|
||||||
//self.connect_to(port)
|
|
||||||
//} else {
|
|
||||||
//Ok(Missing)
|
|
||||||
//})
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//impl ConnectTo<'static, &Port<Unowned>> for $Name {
|
|
||||||
//fn connect_to (&self, port: &Port<Unowned>) -> Usually<PortConnectStatus> {
|
|
||||||
//self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(&self.port, port) {
|
|
||||||
//Connected
|
|
||||||
//} else if let Ok(_) = c.connect_ports(port, &self.port) {
|
|
||||||
//Connected
|
|
||||||
//} else {
|
|
||||||
//Mismatch
|
|
||||||
//}))
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//impl ConnectTo<'static, &Port<$Pair>> for $Name {
|
|
||||||
//fn connect_to (&self, port: &Port<$Pair>) -> Usually<PortConnectStatus> {
|
|
||||||
//self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(&self.port, port) {
|
|
||||||
//Connected
|
|
||||||
//} else if let Ok(_) = c.connect_ports(port, &self.port) {
|
|
||||||
//Connected
|
|
||||||
//} else {
|
|
||||||
//Mismatch
|
|
||||||
//}))
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//impl ConnectAuto<'static> for $Name {
|
|
||||||
//fn connections (&self) -> &[PortConnect] {
|
|
||||||
//&self.conn
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//};
|
|
||||||
//}
|
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,10 @@ path = "tek.rs"
|
||||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ansi_term = "0.12.1"
|
tek_device = { path = "../device" }
|
||||||
|
|
||||||
atomic_float = { workspace = true }
|
atomic_float = { workspace = true }
|
||||||
backtrace = { workspace = true }
|
backtrace = { workspace = true }
|
||||||
builder-pattern = "0.4.2"
|
|
||||||
clap = { workspace = true, optional = true }
|
clap = { workspace = true, optional = true }
|
||||||
jack = { workspace = true }
|
jack = { workspace = true }
|
||||||
konst = { workspace = true }
|
konst = { workspace = true }
|
||||||
|
|
@ -32,32 +32,30 @@ uuid = { workspace = true, optional = true }
|
||||||
wavers = { workspace = true, optional = true }
|
wavers = { workspace = true, optional = true }
|
||||||
winit = { workspace = true, optional = true }
|
winit = { workspace = true, optional = true }
|
||||||
xdg = { workspace = true }
|
xdg = { workspace = true }
|
||||||
|
ansi_term = "0.12.1"
|
||||||
|
builder-pattern = "0.4.2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
proptest = { workspace = true }
|
proptest = { workspace = true }
|
||||||
proptest-derive = { workspace = true }
|
proptest-derive = { workspace = true }
|
||||||
|
tek_engine = { path = "../engine" }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
arranger = ["port", "editor", "sequencer", "editor", "track", "scene", "clip", "select"]
|
arranger = ["port", "editor", "sequencer", "editor"]
|
||||||
browse = []
|
browse = []
|
||||||
clap = []
|
clap = []
|
||||||
cli = ["dep:clap"]
|
cli = ["dep:clap"]
|
||||||
clip = []
|
|
||||||
clock = []
|
clock = []
|
||||||
default = ["cli", "arranger", "sampler", "lv2"]
|
default = ["cli", "arranger", "sampler", "lv2"]
|
||||||
editor = []
|
editor = []
|
||||||
host = ["lv2"]
|
host = ["lv2"]
|
||||||
lv2 = ["port", "livi"]
|
lv2 = ["port", "livi", "winit"]
|
||||||
lv2_gui = ["lv2", "winit"]
|
|
||||||
meter = []
|
meter = []
|
||||||
mixer = []
|
mixer = []
|
||||||
pool = []
|
pool = []
|
||||||
port = []
|
port = []
|
||||||
sampler = ["port", "meter", "mixer", "browse", "symphonia", "wavers"]
|
sampler = ["port", "meter", "mixer", "browse", "symphonia", "wavers"]
|
||||||
scene = []
|
|
||||||
select = []
|
|
||||||
sequencer = ["port", "clock", "uuid", "pool"]
|
sequencer = ["port", "clock", "uuid", "pool"]
|
||||||
sf2 = []
|
sf2 = []
|
||||||
track = []
|
|
||||||
vst2 = []
|
vst2 = []
|
||||||
vst3 = []
|
vst3 = []
|
||||||
|
|
|
||||||
1052
app/tek.rs
1052
app/tek.rs
File diff suppressed because it is too large
Load diff
3741
app/tek_impls.rs
3741
app/tek_impls.rs
File diff suppressed because it is too large
Load diff
|
|
@ -2,64 +2,12 @@ use crate::*;
|
||||||
use clap::{self, Parser, Subcommand};
|
use clap::{self, Parser, Subcommand};
|
||||||
use builder_pattern::Builder;
|
use builder_pattern::Builder;
|
||||||
|
|
||||||
/// Wraps [JackState], and through it [jack::Client] when connected.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let jack = tek::Jack::default();
|
|
||||||
/// ```
|
|
||||||
#[derive(Clone, Debug, Default)] pub struct Jack<'j> (
|
|
||||||
pub(crate) Arc<RwLock<JackState<'j>>>
|
|
||||||
);
|
|
||||||
|
|
||||||
/// This is a connection which may be [Inactive], [Activating], or [Active].
|
|
||||||
/// In the [Active] and [Inactive] states, [JackState::client] returns a
|
|
||||||
/// [jack::Client], which you can use to talk to the JACK API.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let state = tek::JackState::default();
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Default)] pub enum JackState<'j> {
|
|
||||||
/// Unused
|
|
||||||
#[default] Inert,
|
|
||||||
/// Before activation.
|
|
||||||
Inactive(Client),
|
|
||||||
/// During activation.
|
|
||||||
Activating,
|
|
||||||
/// After activation. Must not be dropped for JACK thread to persist.
|
|
||||||
Active(DynamicAsyncClient<'j>),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Event enum for JACK events.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let event = tek::JackEvent::XRun;
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Clone, PartialEq)] pub enum JackEvent {
|
|
||||||
ThreadInit,
|
|
||||||
Shutdown(ClientStatus, Arc<str>),
|
|
||||||
Freewheel(bool),
|
|
||||||
SampleRate(Frames),
|
|
||||||
ClientRegistration(Arc<str>, bool),
|
|
||||||
PortRegistration(PortId, bool),
|
|
||||||
PortRename(PortId, Arc<str>, Arc<str>),
|
|
||||||
PortsConnected(PortId, PortId, bool),
|
|
||||||
GraphReorder,
|
|
||||||
XRun,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generic notification handler that emits [JackEvent]
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let notify = tek::JackNotify(|_|{});
|
|
||||||
/// ```
|
|
||||||
pub struct JackNotify<T: Fn(JackEvent) + Send>(pub T);
|
|
||||||
|
|
||||||
/// Total state
|
/// Total state
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use tek::{TracksView, ScenesView, AddScene};
|
/// use tek::tek_device::TracksView;
|
||||||
/// let mut app = tek::App::default();
|
/// let app: tek::App = Default::default();
|
||||||
/// let _ = app.scene_add(None, None).unwrap();
|
/// let _ = app.scene_add(None, None)?;
|
||||||
/// let _ = app.update_clock();
|
/// let _ = app.update_clock();
|
||||||
/// app.project.editor = Some(Default::default());
|
/// app.project.editor = Some(Default::default());
|
||||||
/// //let _: Vec<_> = app.project.inputs_with_sizes().collect();
|
/// //let _: Vec<_> = app.project.inputs_with_sizes().collect();
|
||||||
|
|
@ -105,22 +53,7 @@ pub struct JackNotify<T: Fn(JackEvent) + Send>(pub T);
|
||||||
/// Configuration: mode, view, and bind definitions.
|
/// Configuration: mode, view, and bind definitions.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// let config = tek::Config::default();
|
/// let conf: tek::Config = Default::default();
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// // Some dizzle.
|
|
||||||
/// // What indentation to use here lol?
|
|
||||||
/// let source = stringify!((mode :menu (name Menu)
|
|
||||||
/// (info Mode selector.) (keys :axis/y :confirm)
|
|
||||||
/// (view (bg (g 0) (bsp/s :ports/out
|
|
||||||
/// (bsp/n :ports/in
|
|
||||||
/// (bg (g 30) (bsp/s (fixed/y 7 :logo)
|
|
||||||
/// (fill :dialog/menu)))))))));
|
|
||||||
/// // Add this definition to the config and try to load it.
|
|
||||||
/// // A "mode" is basically a state machine
|
|
||||||
/// // with associated input and output definitions.
|
|
||||||
/// tek::Config::default().add(&source).unwrap().get_mode(":menu").unwrap();
|
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Default, Debug)] pub struct Config {
|
#[derive(Default, Debug)] pub struct Config {
|
||||||
/// XDG base directories of running user.
|
/// XDG base directories of running user.
|
||||||
|
|
@ -136,7 +69,7 @@ pub struct JackNotify<T: Fn(JackEvent) + Send>(pub T);
|
||||||
/// Group of view and keys definitions.
|
/// Group of view and keys definitions.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// let mode = tek::Mode::<std::sync::Arc<str>>::default();
|
/// let mode: tek::Mode<std::sync::Arc<str>> = Default::default();
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Default, Debug)] pub struct Mode<D: Language + Ord> {
|
#[derive(Default, Debug)] pub struct Mode<D: Language + Ord> {
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
|
|
@ -150,7 +83,7 @@ pub struct JackNotify<T: Fn(JackEvent) + Send>(pub T);
|
||||||
/// An input binding.
|
/// An input binding.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// let bind = tek::Bind::<(), ()>::default();
|
/// let bind: tek::Bind<(), ()> = Default::default();
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Debug)] pub struct Bind<E, C>(
|
#[derive(Debug)] pub struct Bind<E, C>(
|
||||||
/// Map of each event (e.g. key combination) to
|
/// Map of each event (e.g. key combination) to
|
||||||
|
|
@ -209,9 +142,9 @@ pub struct JackNotify<T: Fn(JackEvent) + Send>(pub T);
|
||||||
/// use clap::CommandFactory;
|
/// use clap::CommandFactory;
|
||||||
/// tek::Cli::command().debug_assert();
|
/// tek::Cli::command().debug_assert();
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Parser)]
|
#[derive(Debug, Parser, Default)]
|
||||||
#[command(name = "tek", version, about = Some(HEADER), long_about = Some(HEADER))]
|
#[command(name = "tek", version, about = Some(HEADER), long_about = Some(HEADER))]
|
||||||
#[derive(Debug, Default)] pub struct Cli {
|
pub struct Cli {
|
||||||
/// Pre-defined configuration modes.
|
/// Pre-defined configuration modes.
|
||||||
///
|
///
|
||||||
/// TODO: Replace these with scripted configurations.
|
/// TODO: Replace these with scripted configurations.
|
||||||
|
|
@ -280,7 +213,8 @@ pub struct JackNotify<T: Fn(JackEvent) + Send>(pub T);
|
||||||
/// ```
|
/// ```
|
||||||
/// let axis = tek::ControlAxis::X;
|
/// let axis = tek::ControlAxis::X;
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Debug, Copy, Clone)] pub enum ControlAxis {
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub enum ControlAxis {
|
||||||
X, Y, Z, I
|
X, Y, Z, I
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -298,792 +232,3 @@ pub struct JackNotify<T: Fn(JackEvent) + Send>(pub T);
|
||||||
Browse(BrowseTarget, Arc<Browse>),
|
Browse(BrowseTarget, Arc<Browse>),
|
||||||
Options,
|
Options,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Temporal resolutions: sample rate, tempo, MIDI pulses per quaver (beat)
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Clone)] pub struct Timebase {
|
|
||||||
/// Audio samples per second
|
|
||||||
pub sr: SampleRate,
|
|
||||||
/// MIDI beats per minute
|
|
||||||
pub bpm: Bpm,
|
|
||||||
/// MIDI ticks per beat
|
|
||||||
pub ppq: Ppq,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Iterator that emits subsequent ticks within a range.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let iter = tek::TicksIterator::default();
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Default)] pub struct TicksIterator {
|
|
||||||
pub spp: f64,
|
|
||||||
pub sample: usize,
|
|
||||||
pub start: usize,
|
|
||||||
pub end: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use tek::{TimeRange, NoteRange};
|
|
||||||
/// let model = tek::MidiSelection::from((1, false));
|
|
||||||
///
|
|
||||||
/// let _ = model.get_time_len();
|
|
||||||
/// let _ = model.get_time_zoom();
|
|
||||||
/// let _ = model.get_time_lock();
|
|
||||||
/// let _ = model.get_time_start();
|
|
||||||
/// let _ = model.get_time_axis();
|
|
||||||
/// let _ = model.get_time_end();
|
|
||||||
///
|
|
||||||
/// let _ = model.get_note_lo();
|
|
||||||
/// let _ = model.get_note_axis();
|
|
||||||
/// let _ = model.get_note_hi();
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Clone)] pub struct MidiCursor {
|
|
||||||
/// Time coordinate of cursor
|
|
||||||
pub time_pos: Arc<AtomicUsize>,
|
|
||||||
/// Note coordinate of cursor
|
|
||||||
pub note_pos: Arc<AtomicUsize>,
|
|
||||||
/// Length of note that will be inserted, in pulses
|
|
||||||
pub note_len: Arc<AtomicUsize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Clone, Default)] pub struct MidiSelection {
|
|
||||||
pub time_len: Arc<AtomicUsize>,
|
|
||||||
/// Length of visible time axis
|
|
||||||
pub time_axis: Arc<AtomicUsize>,
|
|
||||||
/// Earliest time displayed
|
|
||||||
pub time_start: Arc<AtomicUsize>,
|
|
||||||
/// Time step
|
|
||||||
pub time_zoom: Arc<AtomicUsize>,
|
|
||||||
/// Auto rezoom to fit in time axis
|
|
||||||
pub time_lock: Arc<AtomicBool>,
|
|
||||||
/// Length of visible note axis
|
|
||||||
pub note_axis: Arc<AtomicUsize>,
|
|
||||||
// Lowest note displayed
|
|
||||||
pub note_lo: Arc<AtomicUsize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A point in time in all time scales (microsecond, sample, MIDI pulse)
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Default, Clone)] pub struct Moment {
|
|
||||||
pub timebase: Arc<Timebase>,
|
|
||||||
/// Current time in microseconds
|
|
||||||
pub usec: Microsecond,
|
|
||||||
/// Current time in audio samples
|
|
||||||
pub sample: SampleCount,
|
|
||||||
/// Current time in MIDI pulses
|
|
||||||
pub pulse: Pulse,
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Clone, Default)] pub enum Moment2 {
|
|
||||||
#[default] None,
|
|
||||||
Zero,
|
|
||||||
Usec(Microsecond),
|
|
||||||
Sample(SampleCount),
|
|
||||||
Pulse(Pulse),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// MIDI resolution in PPQ (pulses per quarter note)
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Default)] pub struct Ppq (pub(crate) AtomicF64);
|
|
||||||
|
|
||||||
/// Timestamp in MIDI pulses
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Default)] pub struct Pulse (pub(crate) AtomicF64);
|
|
||||||
|
|
||||||
/// Tempo in beats per minute
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Default)] pub struct Bpm (pub(crate) AtomicF64);
|
|
||||||
|
|
||||||
/// Quantization setting for launching clips
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Default)] pub struct LaunchSync (pub(crate) AtomicF64);
|
|
||||||
|
|
||||||
/// Quantization setting for notes
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Default)] pub struct Quantize (pub(crate) AtomicF64);
|
|
||||||
|
|
||||||
/// Timestamp in audio samples
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Default)] pub struct SampleCount (pub(crate) AtomicF64);
|
|
||||||
|
|
||||||
/// Audio sample rate in Hz (samples per second)
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Default)] pub struct SampleRate (pub(crate) AtomicF64);
|
|
||||||
|
|
||||||
/// Timestamp in microseconds
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Default)] pub struct Microsecond (pub(crate) AtomicF64);
|
|
||||||
|
|
||||||
/// The source of time.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let clock = tek::Clock::default();
|
|
||||||
/// ```
|
|
||||||
#[derive(Clone, Default)] pub struct Clock {
|
|
||||||
/// JACK transport handle.
|
|
||||||
pub transport: Arc<Option<Transport>>,
|
|
||||||
/// Global temporal resolution (shared by [Moment] fields)
|
|
||||||
pub timebase: Arc<Timebase>,
|
|
||||||
/// Current global sample and usec (monotonic from JACK clock)
|
|
||||||
pub global: Arc<Moment>,
|
|
||||||
/// Global sample and usec at which playback started
|
|
||||||
pub started: Arc<RwLock<Option<Moment>>>,
|
|
||||||
/// Playback offset (when playing not from start)
|
|
||||||
pub offset: Arc<Moment>,
|
|
||||||
/// Current playhead position
|
|
||||||
pub playhead: Arc<Moment>,
|
|
||||||
/// Note quantization factor
|
|
||||||
pub quant: Arc<Quantize>,
|
|
||||||
/// Launch quantization factor
|
|
||||||
pub sync: Arc<LaunchSync>,
|
|
||||||
/// Size of buffer in samples
|
|
||||||
pub chunk: Arc<AtomicUsize>,
|
|
||||||
// Cache of formatted strings
|
|
||||||
pub view_cache: Arc<RwLock<ClockView>>,
|
|
||||||
/// For syncing the clock to an external source
|
|
||||||
#[cfg(feature = "port")] pub midi_in: Arc<RwLock<Option<MidiInput>>>,
|
|
||||||
/// For syncing other devices to this clock
|
|
||||||
#[cfg(feature = "port")] pub midi_out: Arc<RwLock<Option<MidiOutput>>>,
|
|
||||||
/// For emitting a metronome
|
|
||||||
#[cfg(feature = "port")] pub click_out: Arc<RwLock<Option<AudioOutput>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Contains memoized renders of clock values.
|
|
||||||
///
|
|
||||||
/// Performance optimization.
|
|
||||||
#[derive(Debug)] pub struct ClockView {
|
|
||||||
pub sr: Memo<Option<(bool, f64)>, String>,
|
|
||||||
pub buf: Memo<Option<f64>, String>,
|
|
||||||
pub lat: Memo<Option<f64>, String>,
|
|
||||||
pub bpm: Memo<Option<f64>, String>,
|
|
||||||
pub beat: Memo<Option<f64>, String>,
|
|
||||||
pub time: Memo<Option<f64>, String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Arranger.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let arranger = tek::Arrangement::default();
|
|
||||||
/// ```
|
|
||||||
#[derive(Default, Debug)] pub struct Arrangement {
|
|
||||||
/// Project name.
|
|
||||||
pub name: Arc<str>,
|
|
||||||
/// Base color.
|
|
||||||
pub color: ItemTheme,
|
|
||||||
/// JACK client handle.
|
|
||||||
pub jack: Jack<'static>,
|
|
||||||
/// FIXME a render of the project arrangement, redrawn on update.
|
|
||||||
/// TODO rename to "render_cache" or smth
|
|
||||||
pub arranger: Arc<RwLock<Buffer>>,
|
|
||||||
/// Display size
|
|
||||||
pub size: Measure<TuiOut>,
|
|
||||||
/// Display size of clips area
|
|
||||||
pub size_inner: Measure<TuiOut>,
|
|
||||||
/// Source of time
|
|
||||||
#[cfg(feature = "clock")] pub clock: Clock,
|
|
||||||
/// Allows one MIDI clip to be edited
|
|
||||||
#[cfg(feature = "editor")] pub editor: Option<MidiEditor>,
|
|
||||||
/// List of global midi inputs
|
|
||||||
#[cfg(feature = "port")] pub midi_ins: Vec<MidiInput>,
|
|
||||||
/// List of global midi outputs
|
|
||||||
#[cfg(feature = "port")] pub midi_outs: Vec<MidiOutput>,
|
|
||||||
/// List of global audio inputs
|
|
||||||
#[cfg(feature = "port")] pub audio_ins: Vec<AudioInput>,
|
|
||||||
/// List of global audio outputs
|
|
||||||
#[cfg(feature = "port")] pub audio_outs: Vec<AudioOutput>,
|
|
||||||
/// Selected UI element
|
|
||||||
#[cfg(feature = "select")] pub selection: Selection,
|
|
||||||
/// Last track number (to avoid duplicate port names)
|
|
||||||
#[cfg(feature = "track")] pub track_last: usize,
|
|
||||||
/// List of tracks
|
|
||||||
#[cfg(feature = "track")] pub tracks: Vec<Track>,
|
|
||||||
/// Scroll offset of tracks
|
|
||||||
#[cfg(feature = "track")] pub track_scroll: usize,
|
|
||||||
/// List of scenes
|
|
||||||
#[cfg(feature = "scene")] pub scenes: Vec<Scene>,
|
|
||||||
/// Scroll offset of scenes
|
|
||||||
#[cfg(feature = "scene")] pub scene_scroll: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Browses for files to load/save.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let browse = tek::Browse::default();
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Clone, Default, PartialEq)] pub struct Browse {
|
|
||||||
pub cwd: PathBuf,
|
|
||||||
pub dirs: Vec<(OsString, String)>,
|
|
||||||
pub files: Vec<(OsString, String)>,
|
|
||||||
pub filter: String,
|
|
||||||
pub index: usize,
|
|
||||||
pub scroll: usize,
|
|
||||||
pub size: Measure<TuiOut>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct EntriesIterator<'a> {
|
|
||||||
pub browser: &'a Browse,
|
|
||||||
pub offset: usize,
|
|
||||||
pub length: usize,
|
|
||||||
pub index: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)] pub enum BrowseTarget {
|
|
||||||
SaveProject,
|
|
||||||
LoadProject,
|
|
||||||
ImportSample(Arc<RwLock<Option<Sample>>>),
|
|
||||||
ExportSample(Arc<RwLock<Option<Sample>>>),
|
|
||||||
ImportClip(Arc<RwLock<Option<MidiClip>>>),
|
|
||||||
ExportClip(Arc<RwLock<Option<MidiClip>>>),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A MIDI sequence.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let clip = tek::MidiClip::default();
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Clone, Default)] pub struct MidiClip {
|
|
||||||
pub uuid: uuid::Uuid,
|
|
||||||
/// Name of clip
|
|
||||||
pub name: Arc<str>,
|
|
||||||
/// Temporal resolution in pulses per quarter note
|
|
||||||
pub ppq: usize,
|
|
||||||
/// Length of clip in pulses
|
|
||||||
pub length: usize,
|
|
||||||
/// Notes in clip
|
|
||||||
pub notes: MidiData,
|
|
||||||
/// Whether to loop the clip or play it once
|
|
||||||
pub looped: bool,
|
|
||||||
/// Start of loop
|
|
||||||
pub loop_start: usize,
|
|
||||||
/// Length of loop
|
|
||||||
pub loop_length: usize,
|
|
||||||
/// All notes are displayed with minimum length
|
|
||||||
pub percussive: bool,
|
|
||||||
/// Identifying color of clip
|
|
||||||
pub color: ItemTheme,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A device that can be plugged into the chain.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let device = tek::Device::default();
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Default)] pub enum Device {
|
|
||||||
#[default]
|
|
||||||
Bypass,
|
|
||||||
Mute,
|
|
||||||
#[cfg(feature = "sampler")]
|
|
||||||
Sampler(Sampler),
|
|
||||||
#[cfg(feature = "lv2")] // TODO
|
|
||||||
Lv2(Lv2),
|
|
||||||
#[cfg(feature = "vst2")] // TODO
|
|
||||||
Vst2,
|
|
||||||
#[cfg(feature = "vst3")] // TODO
|
|
||||||
Vst3,
|
|
||||||
#[cfg(feature = "clap")] // TODO
|
|
||||||
Clap,
|
|
||||||
#[cfg(feature = "sf2")] // TODO
|
|
||||||
Sf2,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Some sort of wrapper?
|
|
||||||
pub struct DeviceAudio<'a>(pub &'a mut Device);
|
|
||||||
|
|
||||||
/// Contains state for viewing and editing a clip.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use std::sync::{Arc, RwLock};
|
|
||||||
/// let clip = tek::MidiClip::stop_all();
|
|
||||||
/// let mut editor = tek::MidiEditor {
|
|
||||||
/// mode: tek::PianoHorizontal::new(Some(&Arc::new(RwLock::new(clip)))),
|
|
||||||
/// size: Default::default(),
|
|
||||||
/// //keys: Default::default(),
|
|
||||||
/// };
|
|
||||||
/// let _ = editor.put_note(true);
|
|
||||||
/// let _ = editor.put_note(false);
|
|
||||||
/// let _ = editor.clip_status();
|
|
||||||
/// let _ = editor.edit_status();
|
|
||||||
/// ```
|
|
||||||
pub struct MidiEditor {
|
|
||||||
/// Size of editor on screen
|
|
||||||
pub size: Measure<TuiOut>,
|
|
||||||
/// View mode and state of editor
|
|
||||||
pub mode: PianoHorizontal,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A clip, rendered as a horizontal piano roll.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let piano = tek::PianoHorizontal::default();
|
|
||||||
/// ```
|
|
||||||
#[derive(Clone, Default)] pub struct PianoHorizontal {
|
|
||||||
pub clip: Option<Arc<RwLock<MidiClip>>>,
|
|
||||||
/// Buffer where the whole clip is rerendered on change
|
|
||||||
pub buffer: Arc<RwLock<BigBuffer>>,
|
|
||||||
/// Size of actual notes area
|
|
||||||
pub size: Measure<TuiOut>,
|
|
||||||
/// The display window
|
|
||||||
pub range: MidiSelection,
|
|
||||||
/// The note cursor
|
|
||||||
pub point: MidiCursor,
|
|
||||||
/// The highlight color palette
|
|
||||||
pub color: ItemTheme,
|
|
||||||
/// Width of the keyboard
|
|
||||||
pub keys_width: u16,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 12 piano keys, some highlighted.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let keys = tek::OctaveVertical::default();
|
|
||||||
/// ```
|
|
||||||
#[derive(Copy, Clone)] pub struct OctaveVertical {
|
|
||||||
pub on: [bool; 12],
|
|
||||||
pub colors: [Color; 3]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A LV2 plugin.
|
|
||||||
#[derive(Debug)] #[cfg(feature = "lv2")] pub struct Lv2 {
|
|
||||||
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
|
||||||
pub jack: Jack<'static>,
|
|
||||||
pub name: Arc<str>,
|
|
||||||
pub path: Option<Arc<str>>,
|
|
||||||
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>>,
|
|
||||||
|
|
||||||
pub lv2_world: livi::World,
|
|
||||||
pub lv2_instance: livi::Instance,
|
|
||||||
pub lv2_plugin: livi::Plugin,
|
|
||||||
pub lv2_features: Arc<livi::Features>,
|
|
||||||
pub lv2_port_list: Vec<livi::Port>,
|
|
||||||
pub lv2_input_buffer: Vec<livi::event::LV2AtomSequence>,
|
|
||||||
pub lv2_ui_thread: Option<JoinHandle<()>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A LV2 plugin's X11 UI.
|
|
||||||
#[cfg(feature = "lv2_gui")] pub struct LV2PluginUI {
|
|
||||||
pub window: Option<Window>
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)] pub enum MeteringMode {
|
|
||||||
#[default] Rms,
|
|
||||||
Log10,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone)] pub struct Log10Meter(pub f32);
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone)] pub struct RmsMeter(pub f32);
|
|
||||||
|
|
||||||
#[derive(Debug, Default)] pub enum MixingMode {
|
|
||||||
#[default] Summing,
|
|
||||||
Average,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A clip pool.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let pool = tek::Pool::default();
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug)] pub struct Pool {
|
|
||||||
pub visible: bool,
|
|
||||||
/// Selected clip
|
|
||||||
pub clip: AtomicUsize,
|
|
||||||
/// Mode switch
|
|
||||||
pub mode: Option<PoolMode>,
|
|
||||||
/// Embedded file browse
|
|
||||||
#[cfg(feature = "browse")] pub browse: Option<Browse>,
|
|
||||||
/// Collection of MIDI clips.
|
|
||||||
#[cfg(feature = "clip")] pub clips: Arc<RwLock<Vec<Arc<RwLock<MidiClip>>>>>,
|
|
||||||
/// Collection of sound samples.
|
|
||||||
#[cfg(feature = "sampler")] pub samples: Arc<RwLock<Vec<Arc<RwLock<Sample>>>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Displays and edits clip length.
|
|
||||||
#[derive(Clone, Debug, Default)] pub struct ClipLength {
|
|
||||||
/// Pulses per beat (quaver)
|
|
||||||
pub ppq: usize,
|
|
||||||
/// Beats per bar
|
|
||||||
pub bpb: usize,
|
|
||||||
/// Length of clip in pulses
|
|
||||||
pub pulses: usize,
|
|
||||||
/// Selected subdivision
|
|
||||||
pub focus: Option<ClipLengthFocus>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Some sort of wrapper again?
|
|
||||||
pub struct PoolView<'a>(pub &'a Pool);
|
|
||||||
|
|
||||||
/// Audio input port.
|
|
||||||
#[derive(Debug)] pub struct AudioInput {
|
|
||||||
/// Handle to JACK client, for receiving reconnect events.
|
|
||||||
pub jack: Jack<'static>,
|
|
||||||
/// Port name
|
|
||||||
pub name: Arc<str>,
|
|
||||||
/// Port handle.
|
|
||||||
pub port: Port<AudioIn>,
|
|
||||||
/// List of ports to connect to.
|
|
||||||
pub connections: Vec<Connect>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Audio output port.
|
|
||||||
#[derive(Debug)] pub struct AudioOutput {
|
|
||||||
/// Handle to JACK client, for receiving reconnect events.
|
|
||||||
pub jack: Jack<'static>,
|
|
||||||
/// Port name
|
|
||||||
pub name: Arc<str>,
|
|
||||||
/// Port handle.
|
|
||||||
pub port: Port<AudioOut>,
|
|
||||||
/// List of ports to connect to.
|
|
||||||
pub connections: Vec<Connect>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// MIDI input port.
|
|
||||||
#[derive(Debug)] pub struct MidiInput {
|
|
||||||
/// Handle to JACK client, for receiving reconnect events.
|
|
||||||
pub jack: Jack<'static>,
|
|
||||||
/// Port name
|
|
||||||
pub name: Arc<str>,
|
|
||||||
/// Port handle.
|
|
||||||
pub port: Port<MidiIn>,
|
|
||||||
/// List of currently held notes.
|
|
||||||
pub held: Arc<RwLock<[bool;128]>>,
|
|
||||||
/// List of ports to connect to.
|
|
||||||
pub connections: Vec<Connect>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// MIDI output port.
|
|
||||||
#[derive(Debug)] pub struct MidiOutput {
|
|
||||||
/// Handle to JACK client, for receiving reconnect events.
|
|
||||||
pub jack: Jack<'static>,
|
|
||||||
/// Port name
|
|
||||||
pub name: Arc<str>,
|
|
||||||
/// Port handle.
|
|
||||||
pub port: Port<MidiOut>,
|
|
||||||
/// List of ports to connect to.
|
|
||||||
pub connections: Vec<Connect>,
|
|
||||||
/// List of currently held notes.
|
|
||||||
pub held: Arc<RwLock<[bool;128]>>,
|
|
||||||
/// Buffer
|
|
||||||
pub note_buffer: Vec<u8>,
|
|
||||||
/// Buffer
|
|
||||||
pub output_buffer: Vec<Vec<Vec<u8>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Port connection manager.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let connect = tek::Connect::default();
|
|
||||||
/// ```
|
|
||||||
#[derive(Clone, Debug, Default)] pub struct Connect {
|
|
||||||
pub name: Option<ConnectName>,
|
|
||||||
pub scope: Option<ConnectScope>,
|
|
||||||
pub status: Arc<RwLock<Vec<(Port<Unowned>, Arc<str>, ConnectStatus)>>>,
|
|
||||||
pub info: Arc<str>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Plays [Voice]s from [Sample]s.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let sampler = tek::Sampler::default();
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Default)] pub struct Sampler {
|
|
||||||
/// Name of sampler.
|
|
||||||
pub name: Arc<str>,
|
|
||||||
/// Device color.
|
|
||||||
pub color: ItemTheme,
|
|
||||||
/// Audio input ports. Samples get recorded here.
|
|
||||||
#[cfg(feature = "port")] pub audio_ins: Vec<AudioInput>,
|
|
||||||
/// Audio input meters.
|
|
||||||
#[cfg(feature = "meter")] pub input_meters: Vec<f32>,
|
|
||||||
/// Sample currently being recorded.
|
|
||||||
pub recording: Option<(usize, Option<Arc<RwLock<Sample>>>)>,
|
|
||||||
/// Recording buffer.
|
|
||||||
pub buffer: Vec<Vec<f32>>,
|
|
||||||
/// Samples mapped to MIDI notes.
|
|
||||||
pub samples: SampleKit<128>,
|
|
||||||
/// Samples that are not mapped to MIDI notes.
|
|
||||||
pub unmapped: Vec<Arc<RwLock<Sample>>>,
|
|
||||||
/// Sample currently being edited.
|
|
||||||
pub editing: Option<Arc<RwLock<Sample>>>,
|
|
||||||
/// MIDI input port. Triggers sample playback.
|
|
||||||
#[cfg(feature = "port")] pub midi_in: Option<MidiInput>,
|
|
||||||
/// Collection of currently playing instances of samples.
|
|
||||||
pub voices: Arc<RwLock<Vec<Voice>>>,
|
|
||||||
/// Audio output ports. Voices get played here.
|
|
||||||
#[cfg(feature = "port")] pub audio_outs: Vec<AudioOutput>,
|
|
||||||
/// Audio output meters.
|
|
||||||
#[cfg(feature = "meter")] pub output_meters: Vec<f32>,
|
|
||||||
/// How to mix the voices.
|
|
||||||
pub mixing_mode: MixingMode,
|
|
||||||
/// How to meter the inputs and outputs.
|
|
||||||
pub metering_mode: MeteringMode,
|
|
||||||
/// Fixed gain applied to all output.
|
|
||||||
pub output_gain: f32,
|
|
||||||
/// Currently active modal, if any.
|
|
||||||
pub mode: Option<SamplerMode>,
|
|
||||||
/// Size of rendered sampler.
|
|
||||||
pub size: Measure<TuiOut>,
|
|
||||||
/// Lowest note displayed.
|
|
||||||
pub note_lo: AtomicUsize,
|
|
||||||
/// Currently selected note.
|
|
||||||
pub note_pt: AtomicUsize,
|
|
||||||
/// Selected note as row/col.
|
|
||||||
pub cursor: (AtomicUsize, AtomicUsize),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Collection of samples, one per slot, fixed number of slots.
|
|
||||||
///
|
|
||||||
/// History: Separated to cleanly implement [Default].
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let samples = tek::SampleKit([None, None, None, None]);
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug)] pub struct SampleKit<const N: usize>(
|
|
||||||
pub [Option<Arc<RwLock<Sample>>>;N]
|
|
||||||
);
|
|
||||||
|
|
||||||
/// A sound cut.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let sample = tek::Sample::default();
|
|
||||||
/// let sample = tek::Sample::new("test", 0, 0, vec![]);
|
|
||||||
/// ```
|
|
||||||
#[derive(Default, Debug)] pub struct Sample {
|
|
||||||
pub name: Arc<str>,
|
|
||||||
pub start: usize,
|
|
||||||
pub end: usize,
|
|
||||||
pub channels: Vec<Vec<f32>>,
|
|
||||||
pub rate: Option<usize>,
|
|
||||||
pub gain: f32,
|
|
||||||
pub color: ItemTheme,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A currently playing instance of a sample.
|
|
||||||
#[derive(Default, Debug, Clone)] pub struct Voice {
|
|
||||||
pub sample: Arc<RwLock<Sample>>,
|
|
||||||
pub after: usize,
|
|
||||||
pub position: usize,
|
|
||||||
pub velocity: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct AddSampleModal {
|
|
||||||
pub exited: bool,
|
|
||||||
pub dir: PathBuf,
|
|
||||||
pub subdirs: Vec<OsString>,
|
|
||||||
pub files: Vec<OsString>,
|
|
||||||
pub cursor: usize,
|
|
||||||
pub offset: usize,
|
|
||||||
pub sample: Arc<RwLock<Sample>>,
|
|
||||||
pub voices: Arc<RwLock<Vec<Voice>>>,
|
|
||||||
pub _search: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A scene consists of a set of clips to play together.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let scene: tek::Scene = Default::default();
|
|
||||||
/// let _ = scene.pulses();
|
|
||||||
/// let _ = scene.is_playing(&[]);
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Default)] pub struct Scene {
|
|
||||||
/// Name of scene
|
|
||||||
pub name: Arc<str>,
|
|
||||||
/// Identifying color of scene
|
|
||||||
pub color: ItemTheme,
|
|
||||||
/// Clips in scene, one per track
|
|
||||||
pub clips: Vec<Option<Arc<RwLock<MidiClip>>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the current user selection in the arranger
|
|
||||||
#[derive(PartialEq, Clone, Copy, Debug, Default)] pub enum Selection {
|
|
||||||
#[default]
|
|
||||||
/// Nothing is selected
|
|
||||||
Nothing,
|
|
||||||
/// The whole mix is selected
|
|
||||||
Mix,
|
|
||||||
/// A MIDI input is selected.
|
|
||||||
Input(usize),
|
|
||||||
/// A MIDI output is selected.
|
|
||||||
Output(usize),
|
|
||||||
/// A scene is selected.
|
|
||||||
#[cfg(feature = "scene")] Scene(usize),
|
|
||||||
/// A track is selected.
|
|
||||||
#[cfg(feature = "track")] Track(usize),
|
|
||||||
/// A clip (track × scene) is selected.
|
|
||||||
#[cfg(feature = "track")] TrackClip { track: usize, scene: usize },
|
|
||||||
/// A track's MIDI input connection is selected.
|
|
||||||
#[cfg(feature = "track")] TrackInput { track: usize, port: usize },
|
|
||||||
/// A track's MIDI output connection is selected.
|
|
||||||
#[cfg(feature = "track")] TrackOutput { track: usize, port: usize },
|
|
||||||
/// A track device slot is selected.
|
|
||||||
#[cfg(feature = "track")] TrackDevice { track: usize, device: usize },
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Contains state for playing a clip
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let clip = tek::MidiClip::default();
|
|
||||||
/// println!("Empty clip: {clip:?}");
|
|
||||||
///
|
|
||||||
/// let clip = tek::MidiClip::stop_all();
|
|
||||||
/// println!("Panic clip: {clip:?}");
|
|
||||||
///
|
|
||||||
/// let mut clip = tek::MidiClip::new("clip", true, 1, None, None);
|
|
||||||
/// clip.set_length(96);
|
|
||||||
/// clip.toggle_loop();
|
|
||||||
/// clip.record_event(12, midly::MidiMessage::NoteOn { key: 36.into(), vel: 100.into() });
|
|
||||||
/// assert!(clip.contains_note_on(36.into(), 6, 18));
|
|
||||||
/// assert_eq!(&clip.notes, &clip.duplicate().notes);
|
|
||||||
///
|
|
||||||
/// let clip = std::sync::Arc::new(clip);
|
|
||||||
/// assert_eq!(clip.clone(), clip);
|
|
||||||
///
|
|
||||||
/// let sequencer = tek::Sequencer::default();
|
|
||||||
/// println!("{sequencer:?}");
|
|
||||||
/// ```
|
|
||||||
pub struct Sequencer {
|
|
||||||
/// State of clock and playhead
|
|
||||||
#[cfg(feature = "clock")] pub clock: Clock,
|
|
||||||
/// Start time and clip being played
|
|
||||||
#[cfg(feature = "clip")] pub play_clip: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
|
|
||||||
/// Start time and next clip
|
|
||||||
#[cfg(feature = "clip")] pub next_clip: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
|
|
||||||
/// Record from MIDI ports to current sequence.
|
|
||||||
#[cfg(feature = "port")] pub midi_ins: Vec<MidiInput>,
|
|
||||||
/// Play from current sequence to MIDI ports
|
|
||||||
#[cfg(feature = "port")] pub midi_outs: Vec<MidiOutput>,
|
|
||||||
/// 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)
|
|
||||||
/// 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>,
|
|
||||||
/// MIDI output buffer
|
|
||||||
pub midi_buf: Vec<Vec<Vec<u8>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A track consists of a sequencer and zero or more devices chained after it.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let track: tek::Track = Default::default();
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Default)] pub struct Track {
|
|
||||||
/// Name of track
|
|
||||||
pub name: Arc<str>,
|
|
||||||
/// Identifying color of track
|
|
||||||
pub color: ItemTheme,
|
|
||||||
/// Preferred width of track column
|
|
||||||
pub width: usize,
|
|
||||||
/// MIDI sequencer state
|
|
||||||
pub sequencer: Sequencer,
|
|
||||||
/// Device chain
|
|
||||||
pub devices: Vec<Device>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commands supported by [Browse]
|
|
||||||
//#[derive(Debug, Clone, PartialEq)]
|
|
||||||
//pub enum BrowseCommand {
|
|
||||||
//Begin,
|
|
||||||
//Cancel,
|
|
||||||
//Confirm,
|
|
||||||
//Select(usize),
|
|
||||||
//Chdir(PathBuf),
|
|
||||||
//Filter(Arc<str>),
|
|
||||||
//}
|
|
||||||
|
|
||||||
/// 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, Browse),
|
|
||||||
/// Save clip to disk
|
|
||||||
Export(usize, Browse),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)] pub enum ConnectName {
|
|
||||||
/** Exact match */
|
|
||||||
Exact(Arc<str>),
|
|
||||||
/** Match regular expression */
|
|
||||||
RegExp(Arc<str>),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectScope {
|
|
||||||
One,
|
|
||||||
All
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectStatus {
|
|
||||||
Missing,
|
|
||||||
Disconnected,
|
|
||||||
Connected,
|
|
||||||
Mismatch,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)] pub enum SamplerMode {
|
|
||||||
// Load sample from path
|
|
||||||
Import(usize, Browse),
|
|
||||||
}
|
|
||||||
|
|
|
||||||
62
app/tek_test.rs
Normal file
62
app/tek_test.rs
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
#[cfg(test)] #[test] fn test_menu () -> Usually<()> {
|
||||||
|
|
||||||
|
// Some dizzle.
|
||||||
|
// What indentation to use here lol?
|
||||||
|
let source = stringify!((mode :menu
|
||||||
|
(name Menu)
|
||||||
|
(info Mode selector.)
|
||||||
|
(keys :axis/y :confirm)
|
||||||
|
(view (bg (g 0) (bsp/s
|
||||||
|
:ports/out
|
||||||
|
(bsp/n
|
||||||
|
:ports/in
|
||||||
|
(bg (g 30) (bsp/s
|
||||||
|
(fixed/y 7 :logo)
|
||||||
|
(fill :dialog/menu)))))))));
|
||||||
|
|
||||||
|
// Load this definition into the config.
|
||||||
|
// A "mode" is basically a state machine
|
||||||
|
// with associated input and output definitions.
|
||||||
|
let mode = Config::new(None).add(&source)?.get_mode(":menu");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)] #[test] fn test_view_layout () {
|
||||||
|
let _ = button_play_pause(true);
|
||||||
|
let _ = button_2("", "", true);
|
||||||
|
let _ = button_2("", "", false);
|
||||||
|
let _ = button_3("", "", "", true);
|
||||||
|
let _ = button_3("", "", "", false);
|
||||||
|
//let _ = heading("", "", 0, "", true);
|
||||||
|
//let _ = heading("", "", 0, "", false);
|
||||||
|
let _ = wrap(Reset, Reset, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)] mod test_view_meter {
|
||||||
|
use super::*;
|
||||||
|
use proptest::prelude::*;
|
||||||
|
#[test] fn test_view_meter () {
|
||||||
|
let _ = view_meter("", 0.0);
|
||||||
|
let _ = view_meters(&[0.0, 0.0]);
|
||||||
|
}
|
||||||
|
proptest! {
|
||||||
|
#[test] fn proptest_view_meter (
|
||||||
|
label in "\\PC*", value in f32::MIN..f32::MAX
|
||||||
|
) {
|
||||||
|
let _ = view_meter(&label, value);
|
||||||
|
}
|
||||||
|
#[test] fn proptest_view_meters (
|
||||||
|
value1 in f32::MIN..f32::MAX,
|
||||||
|
value2 in f32::MIN..f32::MAX
|
||||||
|
) {
|
||||||
|
let _ = view_meters(&[value1, value2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)] #[test] fn test_midi_edit () {
|
||||||
|
}
|
||||||
16
bacon.toml
16
bacon.toml
|
|
@ -10,25 +10,25 @@ l = "job:clippy"
|
||||||
[jobs.check]
|
[jobs.check]
|
||||||
command = ["cargo", "check"]
|
command = ["cargo", "check"]
|
||||||
need_stdout = false
|
need_stdout = false
|
||||||
watch = ["dizzle", "tengri", "app"]
|
watch = ["deps", "engine", "device", "app"]
|
||||||
[jobs.check-all]
|
[jobs.check-all]
|
||||||
command = ["cargo", "check", "--all-targets"]
|
command = ["cargo", "check", "--all-targets"]
|
||||||
need_stdout = false
|
need_stdout = false
|
||||||
watch = ["dizzle", "tengri", "app"]
|
watch = ["deps", "engine", "device", "app"]
|
||||||
[jobs.clippy]
|
[jobs.clippy]
|
||||||
command = ["cargo", "clippy"]
|
command = ["cargo", "clippy"]
|
||||||
need_stdout = false
|
need_stdout = false
|
||||||
watch = ["dizzle", "tengri", "app"]
|
watch = ["deps", "engine", "device", "app"]
|
||||||
[jobs.clippy-all]
|
[jobs.clippy-all]
|
||||||
command = ["cargo", "clippy", "--all-targets"]
|
command = ["cargo", "clippy", "--all-targets"]
|
||||||
need_stdout = false
|
need_stdout = false
|
||||||
watch = ["dizzle", "tengri", "app"]
|
watch = ["deps", "engine", "device", "app"]
|
||||||
[jobs.test]
|
[jobs.test]
|
||||||
command = ["cargo", "test", "--workspace", "--exclude", "jack"]
|
command = ["cargo", "test", "--workspace", "--exclude", "jack"]
|
||||||
need_stdout = true
|
need_stdout = true
|
||||||
watch = ["dizzle", "tengri", "app"]
|
watch = ["deps", "engine", "device", "app"]
|
||||||
[jobs.nextest]
|
[jobs.nextest]
|
||||||
watch = ["dizzle", "tengri", "app"]
|
watch = ["deps", "engine", "device", "app"]
|
||||||
command = [
|
command = [
|
||||||
"cargo", "nextest", "run",
|
"cargo", "nextest", "run",
|
||||||
"--hide-progress-bar", "--failure-output", "final"
|
"--hide-progress-bar", "--failure-output", "final"
|
||||||
|
|
@ -51,14 +51,14 @@ need_stdout = true
|
||||||
allow_warnings = true
|
allow_warnings = true
|
||||||
background = true
|
background = true
|
||||||
[jobs.run-long]
|
[jobs.run-long]
|
||||||
watch = ["dizzle", "tengri", "app"]
|
watch = ["deps", "engine", "device", "app"]
|
||||||
command = [ "cargo", "run", ]
|
command = [ "cargo", "run", ]
|
||||||
need_stdout = true
|
need_stdout = true
|
||||||
allow_warnings = true
|
allow_warnings = true
|
||||||
background = false
|
background = false
|
||||||
on_change_strategy = "kill_then_restart"
|
on_change_strategy = "kill_then_restart"
|
||||||
[jobs.ex]
|
[jobs.ex]
|
||||||
watch = ["dizzle", "tengri", "app"]
|
watch = ["deps", "engine", "device", "app"]
|
||||||
command = ["cargo", "run", "--example"]
|
command = ["cargo", "run", "--example"]
|
||||||
need_stdout = true
|
need_stdout = true
|
||||||
allow_warnings = true
|
allow_warnings = true
|
||||||
|
|
|
||||||
614
device/.scratch.rs
Normal file
614
device/.scratch.rs
Normal file
|
|
@ -0,0 +1,614 @@
|
||||||
|
//fn begin (browse: &mut Browse) => {
|
||||||
|
//unreachable!();
|
||||||
|
//}
|
||||||
|
//fn cancel (browse: &mut Browse) => {
|
||||||
|
//todo!()
|
||||||
|
////browse.mode = None;
|
||||||
|
////Ok(None)
|
||||||
|
//}
|
||||||
|
//fn confirm (browse: &mut Browse) => {
|
||||||
|
//todo!()
|
||||||
|
////Ok(match browse.mode {
|
||||||
|
////Some(PoolMode::Import(index, ref mut browse)) => {
|
||||||
|
////if browse.is_file() {
|
||||||
|
////let path = browse.path();
|
||||||
|
////browse.mode = None;
|
||||||
|
////let _undo = PoolClipCommand::import(browse, index, path)?;
|
||||||
|
////None
|
||||||
|
////} else if browse.is_dir() {
|
||||||
|
////browse.mode = Some(PoolMode::Import(index, browse.chdir()?));
|
||||||
|
////None
|
||||||
|
////} else {
|
||||||
|
////None
|
||||||
|
////}
|
||||||
|
////},
|
||||||
|
////Some(PoolMode::Export(index, ref mut browse)) => {
|
||||||
|
////todo!()
|
||||||
|
////},
|
||||||
|
////_ => unreachable!(),
|
||||||
|
////})
|
||||||
|
//}
|
||||||
|
//fn select (browse: &mut Browse, index: usize) => {
|
||||||
|
//todo!()
|
||||||
|
////Ok(match browse.mode {
|
||||||
|
////Some(PoolMode::Import(index, ref mut browse)) => {
|
||||||
|
////browse.index = index;
|
||||||
|
////None
|
||||||
|
////},
|
||||||
|
////Some(PoolMode::Export(index, ref mut browse)) => {
|
||||||
|
////browse.index = index;
|
||||||
|
////None
|
||||||
|
////},
|
||||||
|
////_ => unreachable!(),
|
||||||
|
////})
|
||||||
|
//}
|
||||||
|
//fn chdir (browse: &mut Browse, dir: PathBuf) => {
|
||||||
|
//todo!()
|
||||||
|
////Ok(match browse.mode {
|
||||||
|
////Some(PoolMode::Import(index, ref mut browse)) => {
|
||||||
|
////browse.mode = Some(PoolMode::Import(index, Browse::new(Some(dir))?));
|
||||||
|
////None
|
||||||
|
////},
|
||||||
|
////Some(PoolMode::Export(index, ref mut browse)) => {
|
||||||
|
////browse.mode = Some(PoolMode::Export(index, Browse::new(Some(dir))?));
|
||||||
|
////None
|
||||||
|
////},
|
||||||
|
////_ => unreachable!(),
|
||||||
|
////})
|
||||||
|
//}
|
||||||
|
//fn filter (browse: &mut Browse, filter: Arc<str>) => {
|
||||||
|
//todo!()
|
||||||
|
//}
|
||||||
|
|
||||||
|
|
||||||
|
//def_command!(ArrangementCommand: |arranger: Arrangement| {
|
||||||
|
|
||||||
|
//Home => { arranger.editor = None; Ok(None) },
|
||||||
|
|
||||||
|
//Edit => {
|
||||||
|
//let selection = arranger.selection().clone();
|
||||||
|
//arranger.editor = if arranger.editor.is_some() {
|
||||||
|
//None
|
||||||
|
//} else {
|
||||||
|
//match selection {
|
||||||
|
//Selection::TrackClip { track, scene } => {
|
||||||
|
//let clip = &mut arranger.scenes_mut()[scene].clips[track];
|
||||||
|
//if clip.is_none() {
|
||||||
|
////app.clip_auto_create();
|
||||||
|
//*clip = Some(Arc::new(RwLock::new(MidiClip::new(
|
||||||
|
//&format!("t{track:02}s{scene:02}"),
|
||||||
|
//false, 384, None, Some(ItemTheme::random())
|
||||||
|
//))));
|
||||||
|
//}
|
||||||
|
//clip.as_ref().map(|c|c.into())
|
||||||
|
//}
|
||||||
|
//_ => {
|
||||||
|
//None
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//};
|
||||||
|
//if let Some(editor) = arranger.editor.as_mut() {
|
||||||
|
//if let Some(clip) = editor.clip() {
|
||||||
|
//let length = clip.read().unwrap().length.max(1);
|
||||||
|
//let width = arranger.size_inner.w().saturating_sub(20).max(1);
|
||||||
|
//editor.set_time_zoom(length / width);
|
||||||
|
//editor.redraw();
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//Ok(None)
|
||||||
|
//},
|
||||||
|
|
||||||
|
////// Set the selection
|
||||||
|
//Select { selection: Selection } => { *arranger.selection_mut() = *selection; Ok(None) },
|
||||||
|
|
||||||
|
////// Launch the selected clip or scene
|
||||||
|
//Launch => {
|
||||||
|
//match *arranger.selection() {
|
||||||
|
//Selection::Track(t) => {
|
||||||
|
//arranger.tracks[t].sequencer.enqueue_next(None)
|
||||||
|
//},
|
||||||
|
//Selection::TrackClip { track, scene } => {
|
||||||
|
//arranger.tracks[track].sequencer.enqueue_next(arranger.scenes[scene].clips[track].as_ref())
|
||||||
|
//},
|
||||||
|
//Selection::Scene(s) => {
|
||||||
|
//for t in 0..arranger.tracks.len() {
|
||||||
|
//arranger.tracks[t].sequencer.enqueue_next(arranger.scenes[s].clips[t].as_ref())
|
||||||
|
//}
|
||||||
|
//},
|
||||||
|
//_ => {}
|
||||||
|
//};
|
||||||
|
//Ok(None)
|
||||||
|
//},
|
||||||
|
|
||||||
|
////// Set the color of the selected entity
|
||||||
|
//SetColor { palette: Option<ItemTheme> } => {
|
||||||
|
//let mut palette = palette.unwrap_or_else(||ItemTheme::random());
|
||||||
|
//let selection = *arranger.selection();
|
||||||
|
//Ok(Some(Self::SetColor { palette: Some(match selection {
|
||||||
|
//Selection::Mix => {
|
||||||
|
//std::mem::swap(&mut palette, &mut arranger.color);
|
||||||
|
//palette
|
||||||
|
//},
|
||||||
|
//Selection::Scene(s) => {
|
||||||
|
//std::mem::swap(&mut palette, &mut arranger.scenes[s].color);
|
||||||
|
//palette
|
||||||
|
//}
|
||||||
|
//Selection::Track(t) => {
|
||||||
|
//std::mem::swap(&mut palette, &mut arranger.tracks[t].color);
|
||||||
|
//palette
|
||||||
|
//}
|
||||||
|
//Selection::TrackClip { track, scene } => {
|
||||||
|
//if let Some(ref clip) = arranger.scenes[scene].clips[track] {
|
||||||
|
//let mut clip = clip.write().unwrap();
|
||||||
|
//std::mem::swap(&mut palette, &mut clip.color);
|
||||||
|
//palette
|
||||||
|
//} else {
|
||||||
|
//return Ok(None)
|
||||||
|
//}
|
||||||
|
//},
|
||||||
|
//_ => todo!()
|
||||||
|
//}) }))
|
||||||
|
//},
|
||||||
|
|
||||||
|
//Track { track: TrackCommand } => { todo!("delegate") },
|
||||||
|
|
||||||
|
//TrackAdd => {
|
||||||
|
//let index = arranger.track_add(None, None, &[], &[])?.0;
|
||||||
|
//*arranger.selection_mut() = match arranger.selection() {
|
||||||
|
//Selection::Track(_) => Selection::Track(index),
|
||||||
|
//Selection::TrackClip { track: _, scene } => Selection::TrackClip {
|
||||||
|
//track: index, scene: *scene
|
||||||
|
//},
|
||||||
|
//_ => *arranger.selection()
|
||||||
|
//};
|
||||||
|
//Ok(Some(Self::TrackDelete { index }))
|
||||||
|
//},
|
||||||
|
|
||||||
|
//TrackSwap { index: usize, other: usize } => {
|
||||||
|
//let index = *index;
|
||||||
|
//let other = *other;
|
||||||
|
//Ok(Some(Self::TrackSwap { index, other }))
|
||||||
|
//},
|
||||||
|
|
||||||
|
//TrackDelete { index: usize } => {
|
||||||
|
//let index = *index;
|
||||||
|
//let exists = arranger.tracks().get(index).is_some();
|
||||||
|
//if exists {
|
||||||
|
//let track = arranger.tracks_mut().remove(index);
|
||||||
|
//let Track { sequencer: Sequencer { midi_ins, midi_outs, .. }, .. } = track;
|
||||||
|
//for port in midi_ins.into_iter() {
|
||||||
|
//port.close()?;
|
||||||
|
//}
|
||||||
|
//for port in midi_outs.into_iter() {
|
||||||
|
//port.close()?;
|
||||||
|
//}
|
||||||
|
//for scene in arranger.scenes_mut().iter_mut() {
|
||||||
|
//scene.clips.remove(index);
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//Ok(None)
|
||||||
|
////TODO:Ok(Some(Self::TrackAdd ( index, track: Some(deleted_track) })
|
||||||
|
//},
|
||||||
|
|
||||||
|
//MidiIn { input: MidiInputCommand } => {
|
||||||
|
//todo!("delegate"); Ok(None)
|
||||||
|
//},
|
||||||
|
|
||||||
|
//MidiInAdd => {
|
||||||
|
//arranger.midi_in_add()?;
|
||||||
|
//Ok(None)
|
||||||
|
//},
|
||||||
|
|
||||||
|
//MidiOut { output: MidiOutputCommand } => {
|
||||||
|
//todo!("delegate");
|
||||||
|
//Ok(None)
|
||||||
|
//},
|
||||||
|
|
||||||
|
//MidiOutAdd => {
|
||||||
|
//arranger.midi_out_add()?;
|
||||||
|
//Ok(None)
|
||||||
|
//},
|
||||||
|
|
||||||
|
//Device { command: DeviceCommand } => {
|
||||||
|
//todo!("delegate");
|
||||||
|
//Ok(None)
|
||||||
|
//},
|
||||||
|
|
||||||
|
//DeviceAdd { index: usize } => {
|
||||||
|
//todo!("delegate");
|
||||||
|
//Ok(None)
|
||||||
|
//},
|
||||||
|
|
||||||
|
//Scene { scene: SceneCommand } => {
|
||||||
|
//todo!("delegate");
|
||||||
|
//Ok(None)
|
||||||
|
//},
|
||||||
|
|
||||||
|
//OutputAdd => {
|
||||||
|
//arranger.midi_outs.push(MidiOutput::new(
|
||||||
|
//arranger.jack(),
|
||||||
|
//&format!("/M{}", arranger.midi_outs.len() + 1),
|
||||||
|
//&[]
|
||||||
|
//)?);
|
||||||
|
//Ok(None)
|
||||||
|
//},
|
||||||
|
|
||||||
|
//InputAdd => {
|
||||||
|
//arranger.midi_ins.push(MidiInput::new(
|
||||||
|
//arranger.jack(),
|
||||||
|
//&format!("M{}/", arranger.midi_ins.len() + 1),
|
||||||
|
//&[]
|
||||||
|
//)?);
|
||||||
|
//Ok(None)
|
||||||
|
//},
|
||||||
|
|
||||||
|
//SceneAdd => {
|
||||||
|
//let index = arranger.scene_add(None, None)?.0;
|
||||||
|
//*arranger.selection_mut() = match arranger.selection() {
|
||||||
|
//Selection::Scene(_) => Selection::Scene(index),
|
||||||
|
//Selection::TrackClip { track, scene } => Selection::TrackClip {
|
||||||
|
//track: *track,
|
||||||
|
//scene: index
|
||||||
|
//},
|
||||||
|
//_ => *arranger.selection()
|
||||||
|
//};
|
||||||
|
//Ok(None) // TODO
|
||||||
|
//},
|
||||||
|
|
||||||
|
//SceneSwap { index: usize, other: usize } => {
|
||||||
|
//let index = *index;
|
||||||
|
//let other = *other;
|
||||||
|
//Ok(Some(Self::SceneSwap { index, other }))
|
||||||
|
//},
|
||||||
|
|
||||||
|
//SceneDelete { index: usize } => {
|
||||||
|
//let index = *index;
|
||||||
|
//let scenes = arranger.scenes_mut();
|
||||||
|
//Ok(if scenes.get(index).is_some() {
|
||||||
|
//let _scene = scenes.remove(index);
|
||||||
|
//None
|
||||||
|
//} else {
|
||||||
|
//None
|
||||||
|
//})
|
||||||
|
//},
|
||||||
|
|
||||||
|
//SceneLaunch { index: usize } => {
|
||||||
|
//let index = *index;
|
||||||
|
//for track in 0..arranger.tracks.len() {
|
||||||
|
//let clip = arranger.scenes[index].clips[track].as_ref();
|
||||||
|
//arranger.tracks[track].sequencer.enqueue_next(clip);
|
||||||
|
//}
|
||||||
|
//Ok(None)
|
||||||
|
//},
|
||||||
|
|
||||||
|
//Clip { scene: ClipCommand } => {
|
||||||
|
//todo!("delegate")
|
||||||
|
//},
|
||||||
|
|
||||||
|
//ClipGet { a: usize, b: usize } => {
|
||||||
|
////(Get [a: usize, b: usize] cmd_todo!("\n\rtodo: clip: get: {a} {b}"))
|
||||||
|
////("get" [a: usize, b: usize] Some(Self::Get(a.unwrap(), b.unwrap())))
|
||||||
|
//todo!()
|
||||||
|
//},
|
||||||
|
|
||||||
|
//ClipPut { a: usize, b: usize } => {
|
||||||
|
////(Put [t: usize, s: usize, c: MaybeClip]
|
||||||
|
////Some(Self::Put(t, s, arranger.clip_put(t, s, c))))
|
||||||
|
////("put" [a: usize, b: usize, c: MaybeClip] Some(Self::Put(a.unwrap(), b.unwrap(), c.unwrap())))
|
||||||
|
//todo!()
|
||||||
|
//},
|
||||||
|
|
||||||
|
//ClipDel { a: usize, b: usize } => {
|
||||||
|
////("delete" [a: usize, b: usize] Some(Self::Put(a.unwrap(), b.unwrap(), None))))
|
||||||
|
//todo!()
|
||||||
|
//},
|
||||||
|
|
||||||
|
//ClipEnqueue { a: usize, b: usize } => {
|
||||||
|
////(Enqueue [t: usize, s: usize]
|
||||||
|
////cmd!(arranger.tracks[t].sequencer.enqueue_next(arranger.scenes[s].clips[t].as_ref())))
|
||||||
|
////("enqueue" [a: usize, b: usize] Some(Self::Enqueue(a.unwrap(), b.unwrap())))
|
||||||
|
//todo!()
|
||||||
|
//},
|
||||||
|
|
||||||
|
//ClipSwap { a: usize, b: usize }=> {
|
||||||
|
////(Edit [clip: MaybeClip] cmd_todo!("\n\rtodo: clip: edit: {clip:?}"))
|
||||||
|
////("edit" [a: MaybeClip] Some(Self::Edit(a.unwrap())))
|
||||||
|
//todo!()
|
||||||
|
//},
|
||||||
|
|
||||||
|
//});
|
||||||
|
// Update sequencer playhead indicator
|
||||||
|
//self.now().set(0.);
|
||||||
|
//if let Some((ref started_at, Some(ref playing))) = self.sequencer.play_clip {
|
||||||
|
//let clip = clip.read().unwrap();
|
||||||
|
//if *playing.read().unwrap() == *clip {
|
||||||
|
//let pulse = self.current().pulse.get();
|
||||||
|
//let start = started_at.pulse.get();
|
||||||
|
//let now = (pulse - start) % clip.length as f64;
|
||||||
|
//self.now().set(now);
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//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)
|
||||||
|
//}
|
||||||
|
//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)
|
||||||
|
//});
|
||||||
|
|
||||||
|
//pub struct LV2PluginUI {
|
||||||
|
//write: (),
|
||||||
|
//controller: (),
|
||||||
|
//widget: (),
|
||||||
|
//features: (),
|
||||||
|
//transfer: (),
|
||||||
|
//}
|
||||||
|
//take!(BrowseCommand |state: Pool, iter|Ok(state.browse.as_ref()
|
||||||
|
//.map(|p|Take::take(p, iter))
|
||||||
|
//.transpose()?
|
||||||
|
//.flatten()));
|
||||||
|
|
||||||
|
//fn file_browser_filter (&self) -> Arc<str> {
|
||||||
|
//todo!()
|
||||||
|
//}
|
||||||
|
//fn file_browser_path (&self) -> PathBuf {
|
||||||
|
//todo!();
|
||||||
|
//}
|
||||||
|
///// Immutable reference to sample at cursor.
|
||||||
|
//fn sample_selected (&self) -> Option<Arc<RwLock<Sample>>> {
|
||||||
|
//for (i, sample) in self.mapped.iter().enumerate() {
|
||||||
|
//if i == self.cursor().0 {
|
||||||
|
//return sample.as_ref()
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//for (i, sample) in self.unmapped.iter().enumerate() {
|
||||||
|
//if i + self.mapped.len() == self.cursor().0 {
|
||||||
|
//return Some(sample)
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//None
|
||||||
|
//}
|
||||||
|
//fn sample_gain (&self) -> f32 {
|
||||||
|
//todo!()
|
||||||
|
//}
|
||||||
|
//fn sample_above () -> usize {
|
||||||
|
//self.note_pos().min(119) + 8
|
||||||
|
//}
|
||||||
|
//fn sample_below () -> usize {
|
||||||
|
//self.note_pos().max(8) - 8
|
||||||
|
//}
|
||||||
|
//fn sample_to_left () -> usize {
|
||||||
|
//self.note_pos().min(126) + 1
|
||||||
|
//}
|
||||||
|
//fn sample_to_right () -> usize {
|
||||||
|
//self.note_pos().max(1) - 1
|
||||||
|
//}
|
||||||
|
//fn selected_pitch () -> u7 {
|
||||||
|
//(self.note_pos() as u8).into() // TODO
|
||||||
|
//}
|
||||||
|
|
||||||
|
//select (&self, state: &mut Sampler, i: usize) -> Option<Self> {
|
||||||
|
//Self::Select(state.set_note_pos(i))
|
||||||
|
//}
|
||||||
|
///// Assign sample to slot
|
||||||
|
//set (&self, slot: u7, sample: Option<Arc<RwLock<Sample>>>) -> Option<Self> {
|
||||||
|
//let i = slot.as_int() as usize;
|
||||||
|
//let old = self.mapped[i].clone();
|
||||||
|
//self.mapped[i] = sample;
|
||||||
|
//Some(Self::Set(old))
|
||||||
|
//}
|
||||||
|
//set_start (&self, state: &mut Sampler, slot: u7, frame: usize) -> Option<Self> {
|
||||||
|
//todo!()
|
||||||
|
//}
|
||||||
|
//set_gain (&self, state: &mut Sampler, slot: u7, g: f32) -> Option<Self> {
|
||||||
|
//todo!()
|
||||||
|
//}
|
||||||
|
//note_on (&self, state: &mut Sampler, slot: u7, v: u7) -> Option<Self> {
|
||||||
|
//todo!()
|
||||||
|
//}
|
||||||
|
//note_off (&self, state: &mut Sampler, slot: u7) -> Option<Self> {
|
||||||
|
//todo!()
|
||||||
|
//}
|
||||||
|
//set_sample (&self, state: &mut Sampler, slot: u7, s: Option<Arc<RwLock<Sample>>>) -> Option<Self> {
|
||||||
|
//Some(Self::SetSample(p, state.set_sample(p, s)))
|
||||||
|
//}
|
||||||
|
//import (&self, state: &mut Sampler, c: FileBrowserCommand) -> Option<Self> {
|
||||||
|
//match c {
|
||||||
|
//FileBrowserCommand::Begin => {
|
||||||
|
////let voices = &state.state.voices;
|
||||||
|
////let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![])));
|
||||||
|
//state.mode = Some(SamplerMode::Import(0, FileBrowser::new(None)?));
|
||||||
|
//None
|
||||||
|
//},
|
||||||
|
//_ => {
|
||||||
|
//println!("\n\rtodo: import: filebrowser: {c:?}");
|
||||||
|
//None
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
////(Select [i: usize] Some(Self::Select(state.set_note_pos(i))))
|
||||||
|
////(RecordBegin [p: u7] cmd!(state.begin_recording(p.as_int() as usize)))
|
||||||
|
////(RecordCancel [] cmd!(state.cancel_recording()))
|
||||||
|
////(RecordFinish [] cmd!(state.finish_recording()))
|
||||||
|
////(SetStart [p: u7, frame: usize] cmd_todo!("\n\rtodo: {self:?}"))
|
||||||
|
////(SetGain [p: u7, gain: f32] cmd_todo!("\n\rtodo: {self:?}"))
|
||||||
|
////(NoteOn [p: u7, velocity: u7] cmd_todo!("\n\rtodo: {self:?}"))
|
||||||
|
////(NoteOff [p: u7] cmd_todo!("\n\rtodo: {self:?}"))
|
||||||
|
////(SetSample [p: u7, s: Option<Arc<RwLock<Sample>>>] Some(Self::SetSample(p, state.set_sample(p, s))))
|
||||||
|
////(Import [c: FileBrowserCommand] match c {
|
||||||
|
////FileBrowserCommand::Begin => {
|
||||||
|
//////let voices = &state.state.voices;
|
||||||
|
//////let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![])));
|
||||||
|
////state.mode = Some(SamplerMode::Import(0, FileBrowser::new(None)?));
|
||||||
|
////None
|
||||||
|
////},
|
||||||
|
////_ => {
|
||||||
|
////println!("\n\rtodo: import: filebrowser: {c:?}");
|
||||||
|
////None
|
||||||
|
////}
|
||||||
|
////})));
|
||||||
|
////("import" [,..a]
|
||||||
|
////FileBrowserCommand::try_from_expr(state, a).map(Self::Import))
|
||||||
|
////("select" [i: usize]
|
||||||
|
////Some(Self::Select(i.expect("no index"))))
|
||||||
|
////("record/begin" [i: u7]
|
||||||
|
////Some(Self::RecordBegin(i.expect("no index"))))
|
||||||
|
////("record/cancel" []
|
||||||
|
////Some(Self::RecordCancel))
|
||||||
|
////("record/finish" []
|
||||||
|
////Some(Self::RecordFinish))
|
||||||
|
////("set/sample" [i: u7, s: Option<Arc<RwLock<Sample>>>]
|
||||||
|
////Some(Self::SetSample(i.expect("no index"), s.expect("no sampler"))))
|
||||||
|
////("set/start" [i: u7, s: usize]
|
||||||
|
////Some(Self::SetStart(i.expect("no index"), s.expect("no start"))))
|
||||||
|
////("set/gain" [i: u7, g: f32]
|
||||||
|
////Some(Self::SetGain(i.expect("no index"), g.expect("no gain"))))
|
||||||
|
////("note/on" [p: u7, v: u7]
|
||||||
|
////Some(Self::NoteOn(p.expect("no slot"), v.expect("no velocity"))))
|
||||||
|
////("note/off" [p: u7]
|
||||||
|
////Some(Self::NoteOff(p.expect("no slot"))))));
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
//for port in midi_in.iter() {
|
||||||
|
//for event in port.iter() {
|
||||||
|
//match event {
|
||||||
|
//(time, Ok(LiveEvent::Midi {message, ..})) => match message {
|
||||||
|
//MidiMessage::NoteOn {ref key, ..} if let Some(editor) = self.editor.as_ref() => {
|
||||||
|
//editor.set_note_pos(key.as_int() as usize);
|
||||||
|
//},
|
||||||
|
//MidiMessage::Controller {controller, value} if let (Some(editor), Some(sampler)) = (
|
||||||
|
//self.editor.as_ref(),
|
||||||
|
//self.sampler.as_ref(),
|
||||||
|
//) => {
|
||||||
|
//// TODO: give sampler its own cursor
|
||||||
|
//if let Some(sample) = &sampler.mapped[editor.note_pos()] {
|
||||||
|
//sample.write().unwrap().handle_cc(*controller, *value)
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//_ =>{}
|
||||||
|
//},
|
||||||
|
//_ =>{}
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
|
||||||
|
|
||||||
|
//scene_scroll: Fill::Y(Fixed::X(1, ScrollbarV {
|
||||||
|
//offset: arrangement.scene_scroll,
|
||||||
|
//length: h_scenes_area as usize,
|
||||||
|
//total: h_scenes as usize,
|
||||||
|
//})),
|
||||||
|
//take!(SceneCommand |state: Arrangement, iter|state.selected_scene().as_ref()
|
||||||
|
//.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten()));
|
||||||
|
|
||||||
|
//pub(crate) fn io_conns <'a, T: PortsSizes<'a>> (
|
||||||
|
//fg: Color, bg: Color, iter: &mut impl Iterator<Item = (usize, &'a Arc<str>, &'a [Connect], usize, usize)>
|
||||||
|
//) -> impl Content<TuiOut> + 'a {
|
||||||
|
//Fill::XY(Thunk::new(move|to: &mut TuiOut|for (_, _, connections, y, y2) in &mut *iter {
|
||||||
|
//to.place(&map_south(y as u16, (y2-y) as u16, Bsp::s(
|
||||||
|
//Fill::Y(Tui::bold(true, wrap(bg, fg, Fill::Y(Align::w(&"▞▞▞▞ ▞▞▞▞"))))),
|
||||||
|
//Thunk::new(|to: &mut TuiOut|for (index, _connection) in connections.iter().enumerate() {
|
||||||
|
//to.place(&map_south(index as u16, 1, Fill::Y(Align::w(Tui::bold(false,
|
||||||
|
//wrap(bg, fg, Fill::Y(&"")))))))
|
||||||
|
//})
|
||||||
|
//)))
|
||||||
|
//}))
|
||||||
|
//}
|
||||||
|
//track_scroll: Fill::Y(Fixed::Y(1, ScrollbarH {
|
||||||
|
//offset: arrangement.track_scroll,
|
||||||
|
//length: h_tracks_area as usize,
|
||||||
|
//total: h_scenes as usize,
|
||||||
|
//})),
|
||||||
|
//take!(TrackCommand |state: Arrangement, iter|state.selected_track().as_ref()
|
||||||
|
//.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten()));
|
||||||
59
device/Cargo.toml
Normal file
59
device/Cargo.toml
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
[package]
|
||||||
|
name = "tek_device"
|
||||||
|
edition = { workspace = true }
|
||||||
|
version = { workspace = true }
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "device.rs"
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "linux")']
|
||||||
|
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tek_engine = { path = "../engine" }
|
||||||
|
|
||||||
|
atomic_float = { workspace = true }
|
||||||
|
backtrace = { workspace = true }
|
||||||
|
clap = { workspace = true, optional = true }
|
||||||
|
jack = { workspace = true }
|
||||||
|
konst = { workspace = true }
|
||||||
|
livi = { workspace = true, optional = true }
|
||||||
|
midly = { workspace = true }
|
||||||
|
palette = { workspace = true }
|
||||||
|
rand = { workspace = true }
|
||||||
|
symphonia = { workspace = true, optional = true }
|
||||||
|
tengri = { workspace = true }
|
||||||
|
toml = { workspace = true }
|
||||||
|
uuid = { workspace = true, optional = true }
|
||||||
|
wavers = { workspace = true, optional = true }
|
||||||
|
winit = { workspace = true, optional = true }
|
||||||
|
xdg = { workspace = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
proptest = { workspace = true }
|
||||||
|
proptest-derive = { workspace = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
arranger = ["port", "editor", "sequencer", "editor", "track", "scene", "clip", "select"]
|
||||||
|
browse = []
|
||||||
|
clap = []
|
||||||
|
cli = ["dep:clap"]
|
||||||
|
clip = []
|
||||||
|
clock = []
|
||||||
|
default = ["cli", "arranger", "sampler", "track", "lv2"]
|
||||||
|
editor = []
|
||||||
|
host = ["lv2"]
|
||||||
|
lv2 = ["port", "livi"]
|
||||||
|
lv2_gui = ["lv2", "winit"]
|
||||||
|
meter = []
|
||||||
|
mixer = []
|
||||||
|
pool = []
|
||||||
|
port = []
|
||||||
|
sampler = ["port", "meter", "mixer", "browse", "symphonia", "wavers"]
|
||||||
|
select = []
|
||||||
|
scene = []
|
||||||
|
sequencer = ["port", "clock", "uuid", "pool"]
|
||||||
|
sf2 = []
|
||||||
|
track = []
|
||||||
|
vst2 = []
|
||||||
|
vst3 = []
|
||||||
820
device/device.rs
Normal file
820
device/device.rs
Normal file
|
|
@ -0,0 +1,820 @@
|
||||||
|
#![feature(trait_alias)]
|
||||||
|
|
||||||
|
pub use tek_engine;
|
||||||
|
pub(crate) use ConnectName::*;
|
||||||
|
pub(crate) use ConnectScope::*;
|
||||||
|
pub(crate) use ConnectStatus::*;
|
||||||
|
|
||||||
|
mod device_struct; pub use self::device_struct::*;
|
||||||
|
mod device_trait; pub use self::device_trait::*;
|
||||||
|
mod device_type; pub use self::device_type::*;
|
||||||
|
mod device_impl;
|
||||||
|
|
||||||
|
pub(crate) use ::{
|
||||||
|
tek_engine::*,
|
||||||
|
tek_engine::tengri::{
|
||||||
|
Usually, Perhaps, Has, MaybeHas, has, maybe_has, from,
|
||||||
|
input::*,
|
||||||
|
output::*,
|
||||||
|
tui::*,
|
||||||
|
tui::ratatui,
|
||||||
|
tui::ratatui::widgets::{Widget, canvas::{Canvas, Line}},
|
||||||
|
tui::ratatui::prelude::{Rect, Style, Stylize, Buffer, Color::{self, *}},
|
||||||
|
},
|
||||||
|
std::{
|
||||||
|
cmp::Ord,
|
||||||
|
ffi::OsString,
|
||||||
|
fmt::{Debug, Formatter},
|
||||||
|
fs::File,
|
||||||
|
path::PathBuf,
|
||||||
|
sync::{Arc, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "sampler")] pub(crate) use symphonia::{
|
||||||
|
core::{
|
||||||
|
formats::Packet,
|
||||||
|
codecs::{Decoder, CODEC_TYPE_NULL},
|
||||||
|
//errors::Error as SymphoniaError,
|
||||||
|
io::MediaSourceStream,
|
||||||
|
probe::Hint,
|
||||||
|
audio::SampleBuffer,
|
||||||
|
},
|
||||||
|
default::get_codecs,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "lv2")] use std::thread::{spawn, JoinHandle};
|
||||||
|
|
||||||
|
#[cfg(feature = "lv2_gui")] use ::winit::{
|
||||||
|
application::ApplicationHandler,
|
||||||
|
event::WindowEvent,
|
||||||
|
event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
|
||||||
|
window::{Window, WindowId},
|
||||||
|
platform::x11::EventLoopBuilderExtX11
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Define a type alias for iterators of sized items (columns).
|
||||||
|
macro_rules! def_sizes_iter {
|
||||||
|
($Type:ident => $($Item:ty),+) => {
|
||||||
|
pub trait $Type<'a> =
|
||||||
|
Iterator<Item=(usize, $(&'a $Item,)+ usize, usize)> + Send + Sync + 'a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn view_transport (
|
||||||
|
play: bool,
|
||||||
|
bpm: Arc<RwLock<String>>,
|
||||||
|
beat: Arc<RwLock<String>>,
|
||||||
|
time: Arc<RwLock<String>>,
|
||||||
|
) -> impl Content<TuiOut> {
|
||||||
|
let theme = ItemTheme::G[96];
|
||||||
|
Tui::bg(Black, row!(Bsp::a(
|
||||||
|
Fill::XY(Align::w(button_play_pause(play))),
|
||||||
|
Fill::XY(Align::e(row!(
|
||||||
|
FieldH(theme, "BPM", bpm),
|
||||||
|
FieldH(theme, "Beat", beat),
|
||||||
|
FieldH(theme, "Time", time),
|
||||||
|
)))
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn view_status (
|
||||||
|
sel: Option<Arc<str>>,
|
||||||
|
sr: Arc<RwLock<String>>,
|
||||||
|
buf: Arc<RwLock<String>>,
|
||||||
|
lat: Arc<RwLock<String>>,
|
||||||
|
) -> impl Content<TuiOut> {
|
||||||
|
let theme = ItemTheme::G[96];
|
||||||
|
Tui::bg(Black, row!(Bsp::a(
|
||||||
|
Fill::XY(Align::w(sel.map(|sel|FieldH(theme, "Selected", sel)))),
|
||||||
|
Fill::XY(Align::e(row!(
|
||||||
|
FieldH(theme, "SR", sr),
|
||||||
|
FieldH(theme, "Buf", buf),
|
||||||
|
FieldH(theme, "Lat", lat),
|
||||||
|
)))
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn button_play_pause (playing: bool) -> impl Content<TuiOut> {
|
||||||
|
let compact = true;//self.is_editing();
|
||||||
|
Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) },
|
||||||
|
Either::new(compact,
|
||||||
|
Thunk::new(move|to: &mut TuiOut|to.place(&Fixed::X(9, Either::new(playing,
|
||||||
|
Tui::fg(Rgb(0, 255, 0), " PLAYING "),
|
||||||
|
Tui::fg(Rgb(255, 128, 0), " STOPPED ")))
|
||||||
|
)),
|
||||||
|
Thunk::new(move|to: &mut TuiOut|to.place(&Fixed::X(5, Either::new(playing,
|
||||||
|
Tui::fg(Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)),
|
||||||
|
Tui::fg(Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",))))
|
||||||
|
))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn swap_value <T: Clone + PartialEq, U> (
|
||||||
|
target: &mut T, value: &T, returned: impl Fn(T)->U
|
||||||
|
) -> Perhaps<U> {
|
||||||
|
if *target == *value {
|
||||||
|
Ok(None)
|
||||||
|
} else {
|
||||||
|
let mut value = value.clone();
|
||||||
|
std::mem::swap(target, &mut value);
|
||||||
|
Ok(Some(returned(value)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle_bool <U> (
|
||||||
|
target: &mut bool, value: &Option<bool>, returned: impl Fn(Option<bool>)->U
|
||||||
|
) -> Perhaps<U> {
|
||||||
|
let mut value = value.unwrap_or(!*target);
|
||||||
|
if value == *target {
|
||||||
|
Ok(None)
|
||||||
|
} else {
|
||||||
|
std::mem::swap(target, &mut value);
|
||||||
|
Ok(Some(returned(Some(value))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn device_kinds () -> &'static [&'static str] {
|
||||||
|
&[
|
||||||
|
#[cfg(feature = "sampler")] "Sampler",
|
||||||
|
#[cfg(feature = "lv2")] "Plugin (LV2)",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Has<Vec<Device>>> HasDevices for T {
|
||||||
|
fn devices (&self) -> &Vec<Device> {
|
||||||
|
self.get()
|
||||||
|
}
|
||||||
|
fn devices_mut (&mut self) -> &mut Vec<Device> {
|
||||||
|
self.get_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Device {
|
||||||
|
pub fn name (&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::Sampler(sampler) => sampler.name.as_ref(),
|
||||||
|
_ => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn midi_ins (&self) -> &[MidiInput] {
|
||||||
|
match self {
|
||||||
|
//Self::Sampler(Sampler { midi_in, .. }) => &[midi_in],
|
||||||
|
_ => todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn midi_outs (&self) -> &[MidiOutput] {
|
||||||
|
match self {
|
||||||
|
Self::Sampler(_) => &[],
|
||||||
|
_ => todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn audio_ins (&self) -> &[AudioInput] {
|
||||||
|
match self {
|
||||||
|
Self::Sampler(Sampler { audio_ins, .. }) => audio_ins.as_slice(),
|
||||||
|
_ => todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn audio_outs (&self) -> &[AudioOutput] {
|
||||||
|
match self {
|
||||||
|
Self::Sampler(Sampler { audio_outs, .. }) => audio_outs.as_slice(),
|
||||||
|
_ => todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
audio!(|self: DeviceAudio<'a>, client, scope|{
|
||||||
|
use Device::*;
|
||||||
|
match self.0 {
|
||||||
|
Mute => { Control::Continue },
|
||||||
|
Bypass => { /*TODO*/ Control::Continue },
|
||||||
|
#[cfg(feature = "sampler")] Sampler(sampler) => sampler.process(client, scope),
|
||||||
|
#[cfg(feature = "lv2")] Lv2(lv2) => lv2.process(client, scope),
|
||||||
|
#[cfg(feature = "vst2")] Vst2 => { todo!() }, // TODO
|
||||||
|
#[cfg(feature = "vst3")] Vst3 => { todo!() }, // TODO
|
||||||
|
#[cfg(feature = "clap")] Clap => { todo!() }, // TODO
|
||||||
|
#[cfg(feature = "sf2")] Sf2 => { todo!() }, // TODO
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
def_command!(ClockCommand: |clock: Clock| {
|
||||||
|
SeekUsec { usec: f64 } => {
|
||||||
|
clock.playhead.update_from_usec(*usec); Ok(None) },
|
||||||
|
SeekSample { sample: f64 } => {
|
||||||
|
clock.playhead.update_from_sample(*sample); Ok(None) },
|
||||||
|
SeekPulse { pulse: f64 } => {
|
||||||
|
clock.playhead.update_from_pulse(*pulse); Ok(None) },
|
||||||
|
SetBpm { bpm: f64 } => Ok(Some(
|
||||||
|
Self::SetBpm { bpm: clock.timebase().bpm.set(*bpm) })),
|
||||||
|
SetQuant { quant: f64 } => Ok(Some(
|
||||||
|
Self::SetQuant { quant: clock.quant.set(*quant) })),
|
||||||
|
SetSync { sync: f64 } => Ok(Some(
|
||||||
|
Self::SetSync { sync: clock.sync.set(*sync) })),
|
||||||
|
|
||||||
|
Play { position: Option<u32> } => {
|
||||||
|
clock.play_from(*position)?; Ok(None) /* TODO Some(Pause(previousPosition)) */ },
|
||||||
|
Pause { position: Option<u32> } => {
|
||||||
|
clock.pause_at(*position)?; Ok(None) },
|
||||||
|
|
||||||
|
TogglePlayback { position: u32 } => Ok(if clock.is_rolling() {
|
||||||
|
clock.pause_at(Some(*position))?; None
|
||||||
|
} else {
|
||||||
|
clock.play_from(Some(*position))?; None
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
def_command!(DeviceCommand: |device: Device| {});
|
||||||
|
def_command!(ClipCommand: |clip: MidiClip| {
|
||||||
|
SetColor { color: Option<ItemTheme> } => {
|
||||||
|
//(SetColor [t: usize, s: usize, c: ItemTheme]
|
||||||
|
//clip.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o)))));
|
||||||
|
//("color" [a: usize, b: usize] Some(Self::SetColor(a.unwrap(), b.unwrap(), ItemTheme::random())))
|
||||||
|
todo!()
|
||||||
|
},
|
||||||
|
SetLoop { looping: Option<bool> } => {
|
||||||
|
//(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}"))
|
||||||
|
//("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap())))
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
def_command!(BrowseCommand: |browse: Browse| {
|
||||||
|
SetVisible => Ok(None),
|
||||||
|
SetPath { address: PathBuf } => Ok(None),
|
||||||
|
SetSearch { filter: Arc<str> } => Ok(None),
|
||||||
|
SetCursor { cursor: usize } => Ok(None),
|
||||||
|
});
|
||||||
|
//take!(DeviceCommand|state: Arrangement, iter|state.selected_device().as_ref()
|
||||||
|
//.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten()));
|
||||||
|
|
||||||
|
#[cfg(feature = "track")]
|
||||||
|
pub fn view_track_row_section (
|
||||||
|
_theme: ItemTheme,
|
||||||
|
button: impl Content<TuiOut>,
|
||||||
|
button_add: impl Content<TuiOut>,
|
||||||
|
content: impl Content<TuiOut>,
|
||||||
|
) -> impl Content<TuiOut> {
|
||||||
|
Bsp::w(Fill::Y(Fixed::X(4, Align::nw(button_add))),
|
||||||
|
Bsp::e(Fixed::X(20, Fill::Y(Align::nw(button))), Fill::XY(Align::c(content))))
|
||||||
|
}
|
||||||
|
pub fn wrap (bg: Color, fg: Color, content: impl Content<TuiOut>) -> impl Content<TuiOut> {
|
||||||
|
let left = Tui::fg_bg(bg, Reset, Fixed::X(1, Repeat::Y("▐")));
|
||||||
|
let right = Tui::fg_bg(bg, Reset, Fixed::X(1, Repeat::Y("▌")));
|
||||||
|
Bsp::e(left, Bsp::w(right, Tui::fg_bg(fg, bg, content)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export] macro_rules! has_clip {
|
||||||
|
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||||||
|
impl $(<$($L),*$($T $(: $U)?),*>)? HasMidiClip for $Struct $(<$($L),*$($T),*>)? {
|
||||||
|
fn clip (&$self) -> Option<Arc<RwLock<MidiClip>>> { $cb }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def_command!(MidiEditCommand: |editor: MidiEditor| {
|
||||||
|
Show { clip: Option<Arc<RwLock<MidiClip>>> } => {
|
||||||
|
editor.set_clip(clip.as_ref()); editor.redraw(); Ok(None) },
|
||||||
|
DeleteNote => {
|
||||||
|
editor.redraw(); todo!() },
|
||||||
|
AppendNote { advance: bool } => {
|
||||||
|
editor.put_note(*advance); editor.redraw(); Ok(None) },
|
||||||
|
SetNotePos { pos: usize } => {
|
||||||
|
editor.set_note_pos((*pos).min(127)); editor.redraw(); Ok(None) },
|
||||||
|
SetNoteLen { len: usize } => {
|
||||||
|
editor.set_note_len(*len); editor.redraw(); Ok(None) },
|
||||||
|
SetNoteScroll { scroll: usize } => {
|
||||||
|
editor.set_note_lo((*scroll).min(127)); editor.redraw(); Ok(None) },
|
||||||
|
SetTimePos { pos: usize } => {
|
||||||
|
editor.set_time_pos(*pos); editor.redraw(); Ok(None) },
|
||||||
|
SetTimeScroll { scroll: usize } => {
|
||||||
|
editor.set_time_start(*scroll); editor.redraw(); Ok(None) },
|
||||||
|
SetTimeZoom { zoom: usize } => {
|
||||||
|
editor.set_time_zoom(*zoom); editor.redraw(); Ok(None) },
|
||||||
|
SetTimeLock { lock: bool } => {
|
||||||
|
editor.set_time_lock(*lock); editor.redraw(); Ok(None) },
|
||||||
|
// TODO: 1-9 seek markers that by default start every 8th of the clip
|
||||||
|
});
|
||||||
|
|
||||||
|
fn to_key (note: usize) -> &'static str {
|
||||||
|
match note % 12 {
|
||||||
|
11 | 9 | 7 | 5 | 4 | 2 | 0 => "████▌",
|
||||||
|
10 | 8 | 6 | 3 | 1 => " ",
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn note_y_iter (note_lo: usize, note_hi: usize, y0: u16)
|
||||||
|
-> impl Iterator<Item=(usize, u16, usize)>
|
||||||
|
{
|
||||||
|
(note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_header (state: &Lv2, 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 })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(feature = "lv2_gui")]
|
||||||
|
pub fn run_lv2_ui (mut ui: LV2PluginUI) -> Usually<JoinHandle<()>> {
|
||||||
|
Ok(spawn(move||{
|
||||||
|
let event_loop = EventLoop::builder().with_x11().with_any_thread(true).build().unwrap();
|
||||||
|
event_loop.set_control_flow(ControlFlow::Wait);
|
||||||
|
event_loop.run_app(&mut ui).unwrap()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "lv2_gui")]
|
||||||
|
fn lv2_ui_instantiate (kind: &str) {
|
||||||
|
//let host = Suil
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mix_summing <const N: usize> (
|
||||||
|
buffer: &mut [Vec<f32>], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>,
|
||||||
|
) -> bool {
|
||||||
|
let channels = buffer.len();
|
||||||
|
for index in 0..frames {
|
||||||
|
if let Some(frame) = next() {
|
||||||
|
for (channel, sample) in frame.iter().enumerate() {
|
||||||
|
let channel = channel % channels;
|
||||||
|
buffer[channel][index] += sample * gain;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mix_average <const N: usize> (
|
||||||
|
buffer: &mut [Vec<f32>], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>,
|
||||||
|
) -> bool {
|
||||||
|
let channels = buffer.len();
|
||||||
|
for index in 0..frames {
|
||||||
|
if let Some(frame) = next() {
|
||||||
|
for (channel, sample) in frame.iter().enumerate() {
|
||||||
|
let channel = channel % channels;
|
||||||
|
let value = buffer[channel][index];
|
||||||
|
buffer[channel][index] = (value + sample * gain) / 2.0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_log10 (samples: &[f32]) -> f32 {
|
||||||
|
let total: f32 = samples.iter().map(|x|x.abs()).sum();
|
||||||
|
let count = samples.len() as f32;
|
||||||
|
10. * (total / count).log10()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn to_rms (samples: &[f32]) -> f32 {
|
||||||
|
let sum = samples.iter()
|
||||||
|
.map(|s|*s)
|
||||||
|
.reduce(|sum, sample|sum + sample.abs())
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
(sum / samples.len() as f32).sqrt()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn view_meter <'a> (label: &'a str, value: f32) -> impl Content<TuiOut> + 'a {
|
||||||
|
col!(
|
||||||
|
FieldH(ItemTheme::G[128], label, format!("{:>+9.3}", value)),
|
||||||
|
Fixed::XY(if value >= 0.0 { 13 }
|
||||||
|
else if value >= -1.0 { 12 }
|
||||||
|
else if value >= -2.0 { 11 }
|
||||||
|
else if value >= -3.0 { 10 }
|
||||||
|
else if value >= -4.0 { 9 }
|
||||||
|
else if value >= -6.0 { 8 }
|
||||||
|
else if value >= -9.0 { 7 }
|
||||||
|
else if value >= -12.0 { 6 }
|
||||||
|
else if value >= -15.0 { 5 }
|
||||||
|
else if value >= -20.0 { 4 }
|
||||||
|
else if value >= -25.0 { 3 }
|
||||||
|
else if value >= -30.0 { 2 }
|
||||||
|
else if value >= -40.0 { 1 }
|
||||||
|
else { 0 }, 1, Tui::bg(if value >= 0.0 { Red }
|
||||||
|
else if value >= -3.0 { Yellow }
|
||||||
|
else { Green }, ())))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn view_meters (values: &[f32;2]) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
let left = format!("L/{:>+9.3}", values[0]);
|
||||||
|
let right = format!("R/{:>+9.3}", values[1]);
|
||||||
|
Bsp::s(left, right)
|
||||||
|
}
|
||||||
|
def_command!(PoolCommand: |pool: Pool| {
|
||||||
|
// Toggle visibility of pool
|
||||||
|
Show { visible: bool } => { pool.visible = *visible; Ok(Some(Self::Show { visible: !visible })) },
|
||||||
|
// Select a clip from the clip pool
|
||||||
|
Select { index: usize } => { pool.set_clip_index(*index); Ok(None) },
|
||||||
|
// Update the contents of the clip pool
|
||||||
|
Clip { command: PoolClipCommand } => Ok(command.execute(pool)?.map(|command|Self::Clip{command})),
|
||||||
|
// Rename a clip
|
||||||
|
Rename { command: RenameCommand } => Ok(command.delegate(pool, |command|Self::Rename{command})?),
|
||||||
|
// Change the length of a clip
|
||||||
|
Length { command: CropCommand } => Ok(command.delegate(pool, |command|Self::Length{command})?),
|
||||||
|
// Import from file
|
||||||
|
Import { command: BrowseCommand } => Ok(if let Some(browse) = pool.browse.as_mut() {
|
||||||
|
command.delegate(browse, |command|Self::Import{command})?
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}),
|
||||||
|
// Export to file
|
||||||
|
Export { command: BrowseCommand } => Ok(if let Some(browse) = pool.browse.as_mut() {
|
||||||
|
command.delegate(browse, |command|Self::Export{command})?
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
def_command!(PoolClipCommand: |pool: Pool| {
|
||||||
|
Delete { index: usize } => {
|
||||||
|
let index = *index;
|
||||||
|
let clip = pool.clips_mut().remove(index).read().unwrap().clone();
|
||||||
|
Ok(Some(Self::Add { index, clip }))
|
||||||
|
},
|
||||||
|
Swap { index: usize, other: usize } => {
|
||||||
|
let index = *index;
|
||||||
|
let other = *other;
|
||||||
|
pool.clips_mut().swap(index, other);
|
||||||
|
Ok(Some(Self::Swap { index, other }))
|
||||||
|
},
|
||||||
|
Export { index: usize, path: PathBuf } => {
|
||||||
|
todo!("export clip to midi file");
|
||||||
|
},
|
||||||
|
Add { index: usize, clip: MidiClip } => {
|
||||||
|
let index = *index;
|
||||||
|
let mut index = index;
|
||||||
|
let clip = Arc::new(RwLock::new(clip.clone()));
|
||||||
|
let mut clips = pool.clips_mut();
|
||||||
|
if index >= clips.len() {
|
||||||
|
index = clips.len();
|
||||||
|
clips.push(clip)
|
||||||
|
} else {
|
||||||
|
clips.insert(index, clip);
|
||||||
|
}
|
||||||
|
Ok(Some(Self::Delete { index }))
|
||||||
|
},
|
||||||
|
Import { index: usize, path: PathBuf } => {
|
||||||
|
let index = *index;
|
||||||
|
let bytes = std::fs::read(&path)?;
|
||||||
|
let smf = Smf::parse(bytes.as_slice())?;
|
||||||
|
let mut t = 0u32;
|
||||||
|
let mut events = vec![];
|
||||||
|
for track in smf.tracks.iter() {
|
||||||
|
for event in track.iter() {
|
||||||
|
t += event.delta.as_int();
|
||||||
|
if let TrackEventKind::Midi { channel, message } = event.kind {
|
||||||
|
events.push((t, channel.as_int(), message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut clip = MidiClip::new("imported", true, t as usize + 1, None, None);
|
||||||
|
for event in events.iter() {
|
||||||
|
clip.notes[event.0 as usize].push(event.2);
|
||||||
|
}
|
||||||
|
Ok(Self::Add { index, clip }.execute(pool)?)
|
||||||
|
},
|
||||||
|
SetName { index: usize, name: Arc<str> } => {
|
||||||
|
let index = *index;
|
||||||
|
let clip = &mut pool.clips_mut()[index];
|
||||||
|
let old_name = clip.read().unwrap().name.clone();
|
||||||
|
clip.write().unwrap().name = name.clone();
|
||||||
|
Ok(Some(Self::SetName { index, name: old_name }))
|
||||||
|
},
|
||||||
|
SetLength { index: usize, length: usize } => {
|
||||||
|
let index = *index;
|
||||||
|
let clip = &mut pool.clips_mut()[index];
|
||||||
|
let old_len = clip.read().unwrap().length;
|
||||||
|
clip.write().unwrap().length = *length;
|
||||||
|
Ok(Some(Self::SetLength { index, length: old_len }))
|
||||||
|
},
|
||||||
|
SetColor { index: usize, color: ItemColor } => {
|
||||||
|
let index = *index;
|
||||||
|
let mut color = ItemTheme::from(*color);
|
||||||
|
std::mem::swap(&mut color, &mut pool.clips()[index].write().unwrap().color);
|
||||||
|
Ok(Some(Self::SetColor { index, color: color.base }))
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
def_command!(RenameCommand: |pool: Pool| {
|
||||||
|
Begin => unreachable!(),
|
||||||
|
Cancel => {
|
||||||
|
if let Some(PoolMode::Rename(clip, ref mut old_name)) = pool.mode_mut().clone() {
|
||||||
|
pool.clips()[clip].write().unwrap().name = old_name.clone().into();
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
},
|
||||||
|
Confirm => {
|
||||||
|
if let Some(PoolMode::Rename(_clip, ref mut old_name)) = pool.mode_mut().clone() {
|
||||||
|
let old_name = old_name.clone(); *pool.mode_mut() = None; return Ok(Some(Self::Set { value: old_name }))
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
},
|
||||||
|
Set { value: Arc<str> } => {
|
||||||
|
if let Some(PoolMode::Rename(clip, ref mut _old_name)) = pool.mode_mut().clone() {
|
||||||
|
pool.clips()[clip].write().unwrap().name = value.clone();
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
},
|
||||||
|
});
|
||||||
|
def_command!(CropCommand: |pool: Pool| {
|
||||||
|
Begin => unreachable!(),
|
||||||
|
Cancel => { if let Some(PoolMode::Length(..)) = pool.mode_mut().clone() { *pool.mode_mut() = None; } Ok(None) },
|
||||||
|
Set { length: usize } => {
|
||||||
|
if let Some(PoolMode::Length(clip, ref mut length, ref mut _focus))
|
||||||
|
= pool.mode_mut().clone()
|
||||||
|
{
|
||||||
|
let old_length;
|
||||||
|
{
|
||||||
|
let clip = pool.clips()[clip].clone();//.write().unwrap();
|
||||||
|
old_length = Some(clip.read().unwrap().length);
|
||||||
|
clip.write().unwrap().length = *length;
|
||||||
|
}
|
||||||
|
*pool.mode_mut() = None;
|
||||||
|
return Ok(old_length.map(|length|Self::Set { length }))
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
},
|
||||||
|
Next => {
|
||||||
|
if let Some(PoolMode::Length(_clip, ref mut _length, ref mut focus)) = pool.mode_mut().clone() { focus.next() }; Ok(None)
|
||||||
|
},
|
||||||
|
Prev => {
|
||||||
|
if let Some(PoolMode::Length(_clip, ref mut _length, ref mut focus)) = pool.mode_mut().clone() { focus.prev() }; Ok(None)
|
||||||
|
},
|
||||||
|
Inc => {
|
||||||
|
if let Some(PoolMode::Length(_clip, ref mut length, ref mut focus)) = pool.mode_mut().clone() {
|
||||||
|
match focus {
|
||||||
|
ClipLengthFocus::Bar => { *length += 4 * PPQ },
|
||||||
|
ClipLengthFocus::Beat => { *length += PPQ },
|
||||||
|
ClipLengthFocus::Tick => { *length += 1 },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
},
|
||||||
|
Dec => {
|
||||||
|
if let Some(PoolMode::Length(_clip, ref mut length, ref mut focus)) = pool.mode_mut().clone() {
|
||||||
|
match focus {
|
||||||
|
ClipLengthFocus::Bar => { *length = length.saturating_sub(4 * PPQ) },
|
||||||
|
ClipLengthFocus::Beat => { *length = length.saturating_sub(PPQ) },
|
||||||
|
ClipLengthFocus::Tick => { *length = length.saturating_sub(1) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
def_command!(MidiInputCommand: |port: MidiInput| {
|
||||||
|
Close => todo!(),
|
||||||
|
Connect { midi_out: Arc<str> } => todo!(),
|
||||||
|
});
|
||||||
|
def_command!(MidiOutputCommand: |port: MidiOutput| {
|
||||||
|
Close => todo!(),
|
||||||
|
Connect { midi_in: Arc<str> } => todo!(),
|
||||||
|
});
|
||||||
|
def_command!(AudioInputCommand: |port: AudioInput| {
|
||||||
|
Close => todo!(),
|
||||||
|
Connect { audio_out: Arc<str> } => todo!(),
|
||||||
|
});
|
||||||
|
def_command!(AudioOutputCommand: |port: AudioOutput| {
|
||||||
|
Close => todo!(),
|
||||||
|
Connect { audio_in: Arc<str> } => todo!(),
|
||||||
|
});
|
||||||
|
def_sizes_iter!(InputsSizes => MidiInput);
|
||||||
|
def_sizes_iter!(OutputsSizes => MidiOutput);
|
||||||
|
def_sizes_iter!(PortsSizes => Arc<str>, [Connect]);
|
||||||
|
def_sizes_iter!(ScenesSizes => Scene);
|
||||||
|
|
||||||
|
fn draw_info (sample: Option<&Arc<RwLock<Sample>>>) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
When::new(sample.is_some(), Thunk::new(move|to: &mut TuiOut|{
|
||||||
|
let sample = sample.unwrap().read().unwrap();
|
||||||
|
let theme = sample.color;
|
||||||
|
to.place(&row!(
|
||||||
|
FieldH(theme, "Name", format!("{:<10}", sample.name.clone())),
|
||||||
|
FieldH(theme, "Length", format!("{:<8}", sample.channels[0].len())),
|
||||||
|
FieldH(theme, "Start", format!("{:<8}", sample.start)),
|
||||||
|
FieldH(theme, "End", format!("{:<8}", sample.end)),
|
||||||
|
FieldH(theme, "Trans", "0"),
|
||||||
|
FieldH(theme, "Gain", format!("{}", sample.gain)),
|
||||||
|
))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_info_v (sample: Option<&Arc<RwLock<Sample>>>) -> impl Content<TuiOut> + use<'_> {
|
||||||
|
Either::new(sample.is_some(), Thunk::new(move|to: &mut TuiOut|{
|
||||||
|
let sample = sample.unwrap().read().unwrap();
|
||||||
|
let theme = sample.color;
|
||||||
|
to.place(&Fixed::X(20, col!(
|
||||||
|
Fill::X(Align::w(FieldH(theme, "Name ", format!("{:<10}", sample.name.clone())))),
|
||||||
|
Fill::X(Align::w(FieldH(theme, "Length", format!("{:<8}", sample.channels[0].len())))),
|
||||||
|
Fill::X(Align::w(FieldH(theme, "Start ", format!("{:<8}", sample.start)))),
|
||||||
|
Fill::X(Align::w(FieldH(theme, "End ", format!("{:<8}", sample.end)))),
|
||||||
|
Fill::X(Align::w(FieldH(theme, "Trans ", "0"))),
|
||||||
|
Fill::X(Align::w(FieldH(theme, "Gain ", format!("{}", sample.gain)))),
|
||||||
|
)))
|
||||||
|
}), Thunk::new(|to: &mut TuiOut|to.place(&Tui::fg(Red, col!(
|
||||||
|
Tui::bold(true, "× No sample."),
|
||||||
|
"[r] record",
|
||||||
|
"[Shift-F9] import",
|
||||||
|
)))))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_status (sample: Option<&Arc<RwLock<Sample>>>) -> impl Content<TuiOut> {
|
||||||
|
Tui::bold(true, Tui::fg(Tui::g(224), sample
|
||||||
|
.map(|sample|{
|
||||||
|
let sample = sample.read().unwrap();
|
||||||
|
format!("Sample {}-{}", sample.start, sample.end)
|
||||||
|
})
|
||||||
|
.unwrap_or_else(||"No sample".to_string())))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_sample (
|
||||||
|
to: &mut TuiOut, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool
|
||||||
|
) -> Usually<usize> {
|
||||||
|
let style = if focus { Style::default().green() } else { Style::default() };
|
||||||
|
if focus {
|
||||||
|
to.blit(&"🬴", x+1, y, Some(style.bold()));
|
||||||
|
}
|
||||||
|
let label1 = format!("{:3} {:12}",
|
||||||
|
note.map(|n|n.to_string()).unwrap_or(String::default()),
|
||||||
|
sample.name);
|
||||||
|
let label2 = format!("{:>6} {:>6} +0.0",
|
||||||
|
sample.start,
|
||||||
|
sample.end);
|
||||||
|
to.blit(&label1, x+2, y, Some(style.bold()));
|
||||||
|
to.blit(&label2, x+3+label1.len()as u16, y, Some(style));
|
||||||
|
Ok(label1.len() + label2.len() + 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_sample_data (_: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scan (dir: &PathBuf) -> Usually<(Vec<OsString>, Vec<OsString>)> {
|
||||||
|
let (mut subdirs, mut files) = std::fs::read_dir(dir)?
|
||||||
|
.fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{
|
||||||
|
let entry = entry.expect("failed to read drectory entry");
|
||||||
|
let meta = entry.metadata().expect("failed to read entry metadata");
|
||||||
|
if meta.is_file() {
|
||||||
|
files.push(entry.file_name());
|
||||||
|
} else if meta.is_dir() {
|
||||||
|
subdirs.push(entry.file_name());
|
||||||
|
}
|
||||||
|
(subdirs, files)
|
||||||
|
});
|
||||||
|
subdirs.sort();
|
||||||
|
files.sort();
|
||||||
|
Ok((subdirs, files))
|
||||||
|
}
|
||||||
|
|
||||||
|
def_command!(SamplerCommand: |sampler: Sampler| {
|
||||||
|
RecordToggle { slot: usize } => {
|
||||||
|
let slot = *slot;
|
||||||
|
let recording = sampler.recording.as_ref().map(|x|x.0);
|
||||||
|
let _ = Self::RecordFinish.execute(sampler)?;
|
||||||
|
// autoslice: continue recording at next slot
|
||||||
|
if recording != Some(slot) {
|
||||||
|
Self::RecordBegin { slot }.execute(sampler)
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
RecordBegin { slot: usize } => {
|
||||||
|
let slot = *slot;
|
||||||
|
sampler.recording = Some((
|
||||||
|
slot,
|
||||||
|
Some(Arc::new(RwLock::new(Sample::new(
|
||||||
|
"Sample", 0, 0, vec![vec![];sampler.audio_ins.len()]
|
||||||
|
))))
|
||||||
|
));
|
||||||
|
Ok(None)
|
||||||
|
},
|
||||||
|
RecordFinish => {
|
||||||
|
let _prev_sample = sampler.recording.as_mut().map(|(index, sample)|{
|
||||||
|
std::mem::swap(sample, &mut sampler.mapped[*index]);
|
||||||
|
sample
|
||||||
|
}); // TODO: undo
|
||||||
|
Ok(None)
|
||||||
|
},
|
||||||
|
RecordCancel => {
|
||||||
|
sampler.recording = None;
|
||||||
|
Ok(None)
|
||||||
|
},
|
||||||
|
PlaySample { slot: usize } => {
|
||||||
|
let slot = *slot;
|
||||||
|
if let Some(ref sample) = sampler.mapped[slot] {
|
||||||
|
sampler.voices.write().unwrap().push(Sample::play(sample, 0, &u7::from(128)));
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
},
|
||||||
|
StopSample { slot: usize } => {
|
||||||
|
let slot = *slot;
|
||||||
|
todo!();
|
||||||
|
Ok(None)
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
def_command!(FileBrowserCommand: |sampler: Sampler|{
|
||||||
|
//("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")))))
|
||||||
|
});
|
||||||
|
def_command!(SceneCommand: |scene: Scene| {
|
||||||
|
SetSize { size: usize } => { todo!() },
|
||||||
|
SetZoom { size: usize } => { todo!() },
|
||||||
|
SetName { name: Arc<str> } =>
|
||||||
|
swap_value(&mut scene.name, name, |name|Self::SetName{name}),
|
||||||
|
SetColor { color: ItemTheme } =>
|
||||||
|
swap_value(&mut scene.color, color, |color|Self::SetColor{color}),
|
||||||
|
});
|
||||||
|
def_sizes_iter!(TracksSizes => Track);
|
||||||
|
def_command!(TrackCommand: |track: Track| {
|
||||||
|
Stop => { track.sequencer.enqueue_next(None); Ok(None) },
|
||||||
|
SetMute { mute: Option<bool> } => todo!(),
|
||||||
|
SetSolo { solo: Option<bool> } => todo!(),
|
||||||
|
SetSize { size: usize } => todo!(),
|
||||||
|
SetZoom { zoom: usize } => todo!(),
|
||||||
|
SetName { name: Arc<str> } =>
|
||||||
|
swap_value(&mut track.name, name, |name|Self::SetName { name }),
|
||||||
|
SetColor { color: ItemTheme } =>
|
||||||
|
swap_value(&mut track.color, color, |color|Self::SetColor { color }),
|
||||||
|
SetRec { rec: Option<bool> } =>
|
||||||
|
toggle_bool(&mut track.sequencer.recording, rec, |rec|Self::SetRec { rec }),
|
||||||
|
SetMon { mon: Option<bool> } =>
|
||||||
|
toggle_bool(&mut track.sequencer.monitoring, mon, |mon|Self::SetMon { mon }),
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
pub(crate) fn track_width (_index: usize, track: &Track) -> u16 {
|
||||||
|
track.width as u16
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_track_header (theme: ItemTheme, content: impl Content<TuiOut>) -> impl Content<TuiOut> {
|
||||||
|
Fixed::X(12, Tui::bg(theme.darker.rgb, Fill::X(Align::e(content))))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, ports: &'a [T])
|
||||||
|
-> impl Content<TuiOut> + use<'a, T>
|
||||||
|
{
|
||||||
|
let ins = ports.len() as u16;
|
||||||
|
let frame = Outer(true, Style::default().fg(Tui::g(96)));
|
||||||
|
let iter = move||ports.iter();
|
||||||
|
let names = Map::south(1, iter, move|port, index|Fill::Y(Align::w(format!(" {index} {}", port.port_name()))));
|
||||||
|
let field = FieldV(theme, title, names);
|
||||||
|
Fixed::XY(20, 1 + ins, frame.enclose(Fixed::XY(20, 1 + ins, field)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn per_track_top <'a, T: Content<TuiOut> + 'a, U: TracksSizes<'a>> (
|
||||||
|
tracks: impl Fn() -> U + Send + Sync + 'a,
|
||||||
|
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
|
||||||
|
) -> impl Content<TuiOut> + 'a {
|
||||||
|
Align::x(Tui::bg(Reset, Map::new(tracks,
|
||||||
|
move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{
|
||||||
|
let width = (x2 - x1) as u16;
|
||||||
|
map_east(x1 as u16, width, Fixed::X(width, Tui::fg_bg(
|
||||||
|
track.color.lightest.rgb,
|
||||||
|
track.color.base.rgb,
|
||||||
|
callback(index, track))))})))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn per_track <'a, T: Content<TuiOut> + 'a, U: TracksSizes<'a>> (
|
||||||
|
tracks: impl Fn() -> U + Send + Sync + 'a,
|
||||||
|
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
|
||||||
|
) -> impl Content<TuiOut> + 'a {
|
||||||
|
per_track_top(tracks, move|index, track|Fill::Y(Align::y(callback(index, track))))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn io_ports <'a, T: PortsSizes<'a>> (
|
||||||
|
fg: Color, bg: Color, iter: impl Fn()->T + Send + Sync + 'a
|
||||||
|
) -> impl Content<TuiOut> + 'a {
|
||||||
|
Map::new(iter, move|(
|
||||||
|
_index, name, connections, y, y2
|
||||||
|
): (usize, &'a Arc<str>, &'a [Connect], usize, usize), _|
|
||||||
|
map_south(y as u16, (y2-y) as u16, Bsp::s(
|
||||||
|
Fill::Y(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(Bsp::e(&" ", name))))),
|
||||||
|
Map::new(||connections.iter(), move|connect: &'a Connect, index|map_south(index as u16, 1,
|
||||||
|
Fill::Y(Align::w(Tui::bold(false, Tui::fg_bg(fg, bg,
|
||||||
|
&connect.info)))))))))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "vst2")]
|
||||||
|
fn set_vst_plugin <E: Engine> (
|
||||||
|
host: &Arc<Mutex<Plugin<E>>>, _path: &str
|
||||||
|
) -> Usually<PluginKind> {
|
||||||
|
let mut loader = ::vst::host::PluginLoader::load(
|
||||||
|
&std::path::Path::new("/nix/store/ij3sz7nqg5l7v2dygdvzy3w6cj62bd6r-helm-0.9.0/lib/lxvst/helm.so"),
|
||||||
|
host.clone()
|
||||||
|
)?;
|
||||||
|
Ok(PluginKind::VST2 {
|
||||||
|
instance: loader.instance()?
|
||||||
|
})
|
||||||
|
}
|
||||||
3233
device/device_impl.rs
Normal file
3233
device/device_impl.rs
Normal file
File diff suppressed because it is too large
Load diff
633
device/device_struct.rs
Normal file
633
device/device_struct.rs
Normal file
|
|
@ -0,0 +1,633 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// The source of time.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let clock = tek_device::Clock::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Clone, Default)] pub struct Clock {
|
||||||
|
/// JACK transport handle.
|
||||||
|
pub transport: Arc<Option<Transport>>,
|
||||||
|
/// Global temporal resolution (shared by [Moment] fields)
|
||||||
|
pub timebase: Arc<Timebase>,
|
||||||
|
/// Current global sample and usec (monotonic from JACK clock)
|
||||||
|
pub global: Arc<Moment>,
|
||||||
|
/// Global sample and usec at which playback started
|
||||||
|
pub started: Arc<RwLock<Option<Moment>>>,
|
||||||
|
/// Playback offset (when playing not from start)
|
||||||
|
pub offset: Arc<Moment>,
|
||||||
|
/// Current playhead position
|
||||||
|
pub playhead: Arc<Moment>,
|
||||||
|
/// Note quantization factor
|
||||||
|
pub quant: Arc<Quantize>,
|
||||||
|
/// Launch quantization factor
|
||||||
|
pub sync: Arc<LaunchSync>,
|
||||||
|
/// Size of buffer in samples
|
||||||
|
pub chunk: Arc<AtomicUsize>,
|
||||||
|
// Cache of formatted strings
|
||||||
|
pub view_cache: Arc<RwLock<ClockView>>,
|
||||||
|
/// For syncing the clock to an external source
|
||||||
|
#[cfg(feature = "port")] pub midi_in: Arc<RwLock<Option<MidiInput>>>,
|
||||||
|
/// For syncing other devices to this clock
|
||||||
|
#[cfg(feature = "port")] pub midi_out: Arc<RwLock<Option<MidiOutput>>>,
|
||||||
|
/// For emitting a metronome
|
||||||
|
#[cfg(feature = "port")] pub click_out: Arc<RwLock<Option<AudioOutput>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Contains memoized renders of clock values.
|
||||||
|
///
|
||||||
|
/// Performance optimization.
|
||||||
|
#[derive(Debug)] pub struct ClockView {
|
||||||
|
pub sr: Memo<Option<(bool, f64)>, String>,
|
||||||
|
pub buf: Memo<Option<f64>, String>,
|
||||||
|
pub lat: Memo<Option<f64>, String>,
|
||||||
|
pub bpm: Memo<Option<f64>, String>,
|
||||||
|
pub beat: Memo<Option<f64>, String>,
|
||||||
|
pub time: Memo<Option<f64>, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Arranger.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let arranger = tek_device::Arrangement::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Default, Debug)] pub struct Arrangement {
|
||||||
|
/// Project name.
|
||||||
|
pub name: Arc<str>,
|
||||||
|
/// Base color.
|
||||||
|
pub color: ItemTheme,
|
||||||
|
/// JACK client handle.
|
||||||
|
pub jack: Jack<'static>,
|
||||||
|
/// FIXME a render of the project arrangement, redrawn on update.
|
||||||
|
/// TODO rename to "render_cache" or smth
|
||||||
|
pub arranger: Arc<RwLock<Buffer>>,
|
||||||
|
/// Display size
|
||||||
|
pub size: Measure<TuiOut>,
|
||||||
|
/// Display size of clips area
|
||||||
|
pub size_inner: Measure<TuiOut>,
|
||||||
|
/// Source of time
|
||||||
|
#[cfg(feature = "clock")] pub clock: Clock,
|
||||||
|
/// Allows one MIDI clip to be edited
|
||||||
|
#[cfg(feature = "editor")] pub editor: Option<MidiEditor>,
|
||||||
|
/// List of global midi inputs
|
||||||
|
#[cfg(feature = "port")] pub midi_ins: Vec<MidiInput>,
|
||||||
|
/// List of global midi outputs
|
||||||
|
#[cfg(feature = "port")] pub midi_outs: Vec<MidiOutput>,
|
||||||
|
/// List of global audio inputs
|
||||||
|
#[cfg(feature = "port")] pub audio_ins: Vec<AudioInput>,
|
||||||
|
/// List of global audio outputs
|
||||||
|
#[cfg(feature = "port")] pub audio_outs: Vec<AudioOutput>,
|
||||||
|
/// Selected UI element
|
||||||
|
#[cfg(feature = "select")] pub selection: Selection,
|
||||||
|
/// Last track number (to avoid duplicate port names)
|
||||||
|
#[cfg(feature = "track")] pub track_last: usize,
|
||||||
|
/// List of tracks
|
||||||
|
#[cfg(feature = "track")] pub tracks: Vec<Track>,
|
||||||
|
/// Scroll offset of tracks
|
||||||
|
#[cfg(feature = "track")] pub track_scroll: usize,
|
||||||
|
/// List of scenes
|
||||||
|
#[cfg(feature = "scene")] pub scenes: Vec<Scene>,
|
||||||
|
/// Scroll offset of scenes
|
||||||
|
#[cfg(feature = "scene")] pub scene_scroll: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Browses for phrase to import/export
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let browser = tek_device::Browser::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq)]
|
||||||
|
pub struct Browse {
|
||||||
|
pub cwd: PathBuf,
|
||||||
|
pub dirs: Vec<(OsString, String)>,
|
||||||
|
pub files: Vec<(OsString, String)>,
|
||||||
|
pub filter: String,
|
||||||
|
pub index: usize,
|
||||||
|
pub scroll: usize,
|
||||||
|
pub size: Measure<TuiOut>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)] pub(crate) struct EntriesIterator<'a> {
|
||||||
|
pub browser: &'a Browse,
|
||||||
|
pub offset: usize,
|
||||||
|
pub length: usize,
|
||||||
|
pub index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)] pub enum BrowseTarget {
|
||||||
|
SaveProject,
|
||||||
|
LoadProject,
|
||||||
|
ImportSample(Arc<RwLock<Option<Sample>>>),
|
||||||
|
ExportSample(Arc<RwLock<Option<Sample>>>),
|
||||||
|
ImportClip(Arc<RwLock<Option<MidiClip>>>),
|
||||||
|
ExportClip(Arc<RwLock<Option<MidiClip>>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A MIDI sequence.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let clip = tek_device::MidiClip::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default)] pub struct MidiClip {
|
||||||
|
pub uuid: uuid::Uuid,
|
||||||
|
/// Name of clip
|
||||||
|
pub name: Arc<str>,
|
||||||
|
/// Temporal resolution in pulses per quarter note
|
||||||
|
pub ppq: usize,
|
||||||
|
/// Length of clip in pulses
|
||||||
|
pub length: usize,
|
||||||
|
/// Notes in clip
|
||||||
|
pub notes: MidiData,
|
||||||
|
/// Whether to loop the clip or play it once
|
||||||
|
pub looped: bool,
|
||||||
|
/// Start of loop
|
||||||
|
pub loop_start: usize,
|
||||||
|
/// Length of loop
|
||||||
|
pub loop_length: usize,
|
||||||
|
/// All notes are displayed with minimum length
|
||||||
|
pub percussive: bool,
|
||||||
|
/// Identifying color of clip
|
||||||
|
pub color: ItemTheme,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A device that can be plugged into the chain.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let device = tek_engie::Device::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Default)] pub enum Device {
|
||||||
|
#[default]
|
||||||
|
Bypass,
|
||||||
|
Mute,
|
||||||
|
#[cfg(feature = "sampler")]
|
||||||
|
Sampler(Sampler),
|
||||||
|
#[cfg(feature = "lv2")] // TODO
|
||||||
|
Lv2(Lv2),
|
||||||
|
#[cfg(feature = "vst2")] // TODO
|
||||||
|
Vst2,
|
||||||
|
#[cfg(feature = "vst3")] // TODO
|
||||||
|
Vst3,
|
||||||
|
#[cfg(feature = "clap")] // TODO
|
||||||
|
Clap,
|
||||||
|
#[cfg(feature = "sf2")] // TODO
|
||||||
|
Sf2,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Some sort of wrapper?
|
||||||
|
pub struct DeviceAudio<'a>(pub &'a mut Device);
|
||||||
|
|
||||||
|
/// Contains state for viewing and editing a clip.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let mut editor = tek_device::MidiEditor {
|
||||||
|
/// mode: PianoHorizontal::new(Some(&Arc::new(RwLock::new(MidiClip::stop_all())))),
|
||||||
|
/// size: Default::default(),
|
||||||
|
/// //keys: Default::default(),
|
||||||
|
/// };
|
||||||
|
/// let _ = editor.put_note(true);
|
||||||
|
/// let _ = editor.put_note(false);
|
||||||
|
/// let _ = editor.clip_status();
|
||||||
|
/// let _ = editor.edit_status();
|
||||||
|
/// ```
|
||||||
|
pub struct MidiEditor {
|
||||||
|
/// Size of editor on screen
|
||||||
|
pub size: Measure<TuiOut>,
|
||||||
|
/// View mode and state of editor
|
||||||
|
pub mode: PianoHorizontal,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A clip, rendered as a horizontal piano roll.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let piano = tek_device::PianoHorizontal::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Clone, Default)] pub struct PianoHorizontal {
|
||||||
|
pub clip: Option<Arc<RwLock<MidiClip>>>,
|
||||||
|
/// Buffer where the whole clip is rerendered on change
|
||||||
|
pub buffer: Arc<RwLock<BigBuffer>>,
|
||||||
|
/// Size of actual notes area
|
||||||
|
pub size: Measure<TuiOut>,
|
||||||
|
/// The display window
|
||||||
|
pub range: MidiSelection,
|
||||||
|
/// The note cursor
|
||||||
|
pub point: MidiCursor,
|
||||||
|
/// The highlight color palette
|
||||||
|
pub color: ItemTheme,
|
||||||
|
/// Width of the keyboard
|
||||||
|
pub keys_width: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 12 piano keys, some highlighted.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let keys = tek_device::OctaveVertical::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Copy, Clone)] pub struct OctaveVertical {
|
||||||
|
pub on: [bool; 12],
|
||||||
|
pub colors: [Color; 3]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A LV2 plugin.
|
||||||
|
#[derive(Debug)] #[cfg(feature = "lv2")] pub struct Lv2 {
|
||||||
|
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
||||||
|
pub jack: Jack<'static>,
|
||||||
|
pub name: Arc<str>,
|
||||||
|
pub path: Option<Arc<str>>,
|
||||||
|
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>>,
|
||||||
|
|
||||||
|
pub lv2_world: livi::World,
|
||||||
|
pub lv2_instance: livi::Instance,
|
||||||
|
pub lv2_plugin: livi::Plugin,
|
||||||
|
pub lv2_features: Arc<livi::Features>,
|
||||||
|
pub lv2_port_list: Vec<livi::Port>,
|
||||||
|
pub lv2_input_buffer: Vec<livi::event::LV2AtomSequence>,
|
||||||
|
pub lv2_ui_thread: Option<JoinHandle<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A LV2 plugin's X11 UI.
|
||||||
|
#[cfg(feature = "lv2_gui")] pub struct LV2PluginUI {
|
||||||
|
pub window: Option<Window>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)] pub enum MeteringMode {
|
||||||
|
#[default] Rms,
|
||||||
|
Log10,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct Log10Meter(pub f32);
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct RmsMeter(pub f32);
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub enum MixingMode {
|
||||||
|
#[default] Summing,
|
||||||
|
Average,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A clip pool.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let pool = tek_device::Pool::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug)] pub struct Pool {
|
||||||
|
pub visible: bool,
|
||||||
|
/// Selected clip
|
||||||
|
pub clip: AtomicUsize,
|
||||||
|
/// Mode switch
|
||||||
|
pub mode: Option<PoolMode>,
|
||||||
|
/// Embedded file browse
|
||||||
|
#[cfg(feature = "browse")] pub browse: Option<Browse>,
|
||||||
|
/// Collection of MIDI clips.
|
||||||
|
#[cfg(feature = "clip")] pub clips: Arc<RwLock<Vec<Arc<RwLock<MidiClip>>>>>,
|
||||||
|
/// Collection of sound samples.
|
||||||
|
#[cfg(feature = "sampler")] pub samples: Arc<RwLock<Vec<Arc<RwLock<Sample>>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Displays and edits clip length.
|
||||||
|
#[derive(Clone, Debug, Default)] pub struct ClipLength {
|
||||||
|
/// Pulses per beat (quaver)
|
||||||
|
pub ppq: usize,
|
||||||
|
/// Beats per bar
|
||||||
|
pub bpb: usize,
|
||||||
|
/// Length of clip in pulses
|
||||||
|
pub pulses: usize,
|
||||||
|
/// Selected subdivision
|
||||||
|
pub focus: Option<ClipLengthFocus>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Some sort of wrapper again?
|
||||||
|
pub struct PoolView<'a>(pub &'a Pool);
|
||||||
|
|
||||||
|
/// Audio input port.
|
||||||
|
#[derive(Debug)] pub struct AudioInput {
|
||||||
|
/// Handle to JACK client, for receiving reconnect events.
|
||||||
|
pub jack: Jack<'static>,
|
||||||
|
/// Port name
|
||||||
|
pub name: Arc<str>,
|
||||||
|
/// Port handle.
|
||||||
|
pub port: Port<AudioIn>,
|
||||||
|
/// List of ports to connect to.
|
||||||
|
pub connections: Vec<Connect>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Audio output port.
|
||||||
|
#[derive(Debug)] pub struct AudioOutput {
|
||||||
|
/// Handle to JACK client, for receiving reconnect events.
|
||||||
|
pub jack: Jack<'static>,
|
||||||
|
/// Port name
|
||||||
|
pub name: Arc<str>,
|
||||||
|
/// Port handle.
|
||||||
|
pub port: Port<AudioOut>,
|
||||||
|
/// List of ports to connect to.
|
||||||
|
pub connections: Vec<Connect>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MIDI input port.
|
||||||
|
#[derive(Debug)] pub struct MidiInput {
|
||||||
|
/// Handle to JACK client, for receiving reconnect events.
|
||||||
|
pub jack: Jack<'static>,
|
||||||
|
/// Port name
|
||||||
|
pub name: Arc<str>,
|
||||||
|
/// Port handle.
|
||||||
|
pub port: Port<MidiIn>,
|
||||||
|
/// List of currently held notes.
|
||||||
|
pub held: Arc<RwLock<[bool;128]>>,
|
||||||
|
/// List of ports to connect to.
|
||||||
|
pub connections: Vec<Connect>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MIDI output port.
|
||||||
|
#[derive(Debug)] pub struct MidiOutput {
|
||||||
|
/// Handle to JACK client, for receiving reconnect events.
|
||||||
|
pub jack: Jack<'static>,
|
||||||
|
/// Port name
|
||||||
|
pub name: Arc<str>,
|
||||||
|
/// Port handle.
|
||||||
|
pub port: Port<MidiOut>,
|
||||||
|
/// List of ports to connect to.
|
||||||
|
pub connections: Vec<Connect>,
|
||||||
|
/// List of currently held notes.
|
||||||
|
pub held: Arc<RwLock<[bool;128]>>,
|
||||||
|
/// Buffer
|
||||||
|
pub note_buffer: Vec<u8>,
|
||||||
|
/// Buffer
|
||||||
|
pub output_buffer: Vec<Vec<Vec<u8>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Port connection manager.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let connect = tek_engine::Connect::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Clone, Debug, Default)] pub struct Connect {
|
||||||
|
pub name: Option<ConnectName>,
|
||||||
|
pub scope: Option<ConnectScope>,
|
||||||
|
pub status: Arc<RwLock<Vec<(Port<Unowned>, Arc<str>, ConnectStatus)>>>,
|
||||||
|
pub info: Arc<str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plays [Voice]s from [Sample]s.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let sampler = tek_device::Sampler::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Default)] pub struct Sampler {
|
||||||
|
/// Name of sampler.
|
||||||
|
pub name: Arc<str>,
|
||||||
|
/// Device color.
|
||||||
|
pub color: ItemTheme,
|
||||||
|
/// Audio input ports. Samples get recorded here.
|
||||||
|
#[cfg(feature = "port")] pub audio_ins: Vec<AudioInput>,
|
||||||
|
/// Audio input meters.
|
||||||
|
#[cfg(feature = "meter")] pub input_meters: Vec<f32>,
|
||||||
|
/// Sample currently being recorded.
|
||||||
|
pub recording: Option<(usize, Option<Arc<RwLock<Sample>>>)>,
|
||||||
|
/// Recording buffer.
|
||||||
|
pub buffer: Vec<Vec<f32>>,
|
||||||
|
/// Samples mapped to MIDI notes.
|
||||||
|
pub mapped: [Option<Arc<RwLock<Sample>>>;128],
|
||||||
|
/// Samples that are not mapped to MIDI notes.
|
||||||
|
pub unmapped: Vec<Arc<RwLock<Sample>>>,
|
||||||
|
/// Sample currently being edited.
|
||||||
|
pub editing: Option<Arc<RwLock<Sample>>>,
|
||||||
|
/// MIDI input port. Triggers sample playback.
|
||||||
|
#[cfg(feature = "port")] pub midi_in: Option<MidiInput>,
|
||||||
|
/// Collection of currently playing instances of samples.
|
||||||
|
pub voices: Arc<RwLock<Vec<Voice>>>,
|
||||||
|
/// Audio output ports. Voices get played here.
|
||||||
|
#[cfg(feature = "port")] pub audio_outs: Vec<AudioOutput>,
|
||||||
|
/// Audio output meters.
|
||||||
|
#[cfg(feature = "meter")] pub output_meters: Vec<f32>,
|
||||||
|
/// How to mix the voices.
|
||||||
|
pub mixing_mode: MixingMode,
|
||||||
|
/// How to meter the inputs and outputs.
|
||||||
|
pub metering_mode: MeteringMode,
|
||||||
|
/// Fixed gain applied to all output.
|
||||||
|
pub output_gain: f32,
|
||||||
|
/// Currently active modal, if any.
|
||||||
|
pub mode: Option<SamplerMode>,
|
||||||
|
/// Size of rendered sampler.
|
||||||
|
pub size: Measure<TuiOut>,
|
||||||
|
/// Lowest note displayed.
|
||||||
|
pub note_lo: AtomicUsize,
|
||||||
|
/// Currently selected note.
|
||||||
|
pub note_pt: AtomicUsize,
|
||||||
|
/// Selected note as row/col.
|
||||||
|
pub cursor: (AtomicUsize, AtomicUsize),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A sound cut.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let sample = tek_device::Sample::default();
|
||||||
|
/// let sample = tek_device::Sample::new("test", 0, 0, vec![]);
|
||||||
|
/// ```
|
||||||
|
#[derive(Default, Debug)] pub struct Sample {
|
||||||
|
pub name: Arc<str>,
|
||||||
|
pub start: usize,
|
||||||
|
pub end: usize,
|
||||||
|
pub channels: Vec<Vec<f32>>,
|
||||||
|
pub rate: Option<usize>,
|
||||||
|
pub gain: f32,
|
||||||
|
pub color: ItemTheme,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A currently playing instance of a sample.
|
||||||
|
#[derive(Default, Debug, Clone)]
|
||||||
|
pub struct Voice {
|
||||||
|
pub sample: Arc<RwLock<Sample>>,
|
||||||
|
pub after: usize,
|
||||||
|
pub position: usize,
|
||||||
|
pub velocity: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AddSampleModal {
|
||||||
|
pub exited: bool,
|
||||||
|
pub dir: PathBuf,
|
||||||
|
pub subdirs: Vec<OsString>,
|
||||||
|
pub files: Vec<OsString>,
|
||||||
|
pub cursor: usize,
|
||||||
|
pub offset: usize,
|
||||||
|
pub sample: Arc<RwLock<Sample>>,
|
||||||
|
pub voices: Arc<RwLock<Vec<Voice>>>,
|
||||||
|
pub _search: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A scene consists of a set of clips to play together.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let scene: tek_device::Scene = Default::default();
|
||||||
|
/// let _ = scene.pulses();
|
||||||
|
/// let _ = scene.is_playing(&[]);
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Default)] pub struct Scene {
|
||||||
|
/// Name of scene
|
||||||
|
pub name: Arc<str>,
|
||||||
|
/// Identifying color of scene
|
||||||
|
pub color: ItemTheme,
|
||||||
|
/// Clips in scene, one per track
|
||||||
|
pub clips: Vec<Option<Arc<RwLock<MidiClip>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents the current user selection in the arranger
|
||||||
|
#[derive(PartialEq, Clone, Copy, Debug, Default)]
|
||||||
|
pub enum Selection {
|
||||||
|
#[default]
|
||||||
|
/// Nothing is selected
|
||||||
|
Nothing,
|
||||||
|
/// The whole mix is selected
|
||||||
|
Mix,
|
||||||
|
/// A MIDI input is selected.
|
||||||
|
Input(usize),
|
||||||
|
/// A MIDI output is selected.
|
||||||
|
Output(usize),
|
||||||
|
/// A scene is selected.
|
||||||
|
#[cfg(feature = "scene")] Scene(usize),
|
||||||
|
/// A track is selected.
|
||||||
|
#[cfg(feature = "track")] Track(usize),
|
||||||
|
/// A clip (track × scene) is selected.
|
||||||
|
#[cfg(feature = "track")] TrackClip { track: usize, scene: usize },
|
||||||
|
/// A track's MIDI input connection is selected.
|
||||||
|
#[cfg(feature = "track")] TrackInput { track: usize, port: usize },
|
||||||
|
/// A track's MIDI output connection is selected.
|
||||||
|
#[cfg(feature = "track")] TrackOutput { track: usize, port: usize },
|
||||||
|
/// A track device slot is selected.
|
||||||
|
#[cfg(feature = "track")] TrackDevice { track: usize, device: usize },
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Contains state for playing a clip
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let clip = tek_device::MidiClip::default();
|
||||||
|
/// println!("Empty clip: {clip:?}");
|
||||||
|
///
|
||||||
|
/// let clip = tek_device::MidiClip::stop_all();
|
||||||
|
/// println!("Panic clip: {clip:?}");
|
||||||
|
///
|
||||||
|
/// let mut clip = tek_device::MidiClip::new("clip", true, 1, None, None);
|
||||||
|
/// clip.set_length(96);
|
||||||
|
/// clip.toggle_loop();
|
||||||
|
/// clip.record_event(12, midly::MidiMessage::NoteOn { key: 36.into(), vel: 100.into() });
|
||||||
|
/// assert!(clip.contains_note_on(36.into(), 6, 18));
|
||||||
|
/// assert_eq!(&clip.notes, &clip.duplicate().notes);
|
||||||
|
///
|
||||||
|
/// let clip = std::sync::Arc::new(clip);
|
||||||
|
/// assert_eq!(clip.clone(), clip);
|
||||||
|
///
|
||||||
|
/// let sequencer = tek_device::Sequencer::default();
|
||||||
|
/// println!("{sequencer:?}");
|
||||||
|
/// ```
|
||||||
|
pub struct Sequencer {
|
||||||
|
/// State of clock and playhead
|
||||||
|
#[cfg(feature = "clock")] pub clock: Clock,
|
||||||
|
/// Start time and clip being played
|
||||||
|
#[cfg(feature = "clip")] pub play_clip: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
|
||||||
|
/// Start time and next clip
|
||||||
|
#[cfg(feature = "clip")] pub next_clip: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
|
||||||
|
/// Record from MIDI ports to current sequence.
|
||||||
|
#[cfg(feature = "port")] pub midi_ins: Vec<MidiInput>,
|
||||||
|
/// Play from current sequence to MIDI ports
|
||||||
|
#[cfg(feature = "port")] pub midi_outs: Vec<MidiOutput>,
|
||||||
|
/// 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)
|
||||||
|
/// 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>,
|
||||||
|
/// MIDI output buffer
|
||||||
|
pub midi_buf: Vec<Vec<Vec<u8>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A track consists of a sequencer and zero or more devices chained after it.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let track: tek_device::Track = Default::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Default)] pub struct Track {
|
||||||
|
/// Name of track
|
||||||
|
pub name: Arc<str>,
|
||||||
|
/// Identifying color of track
|
||||||
|
pub color: ItemTheme,
|
||||||
|
/// Preferred width of track column
|
||||||
|
pub width: usize,
|
||||||
|
/// MIDI sequencer state
|
||||||
|
pub sequencer: Sequencer,
|
||||||
|
/// Device chain
|
||||||
|
pub devices: Vec<Device>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commands supported by [Browse]
|
||||||
|
//#[derive(Debug, Clone, PartialEq)]
|
||||||
|
//pub enum BrowseCommand {
|
||||||
|
//Begin,
|
||||||
|
//Cancel,
|
||||||
|
//Confirm,
|
||||||
|
//Select(usize),
|
||||||
|
//Chdir(PathBuf),
|
||||||
|
//Filter(Arc<str>),
|
||||||
|
//}
|
||||||
|
|
||||||
|
/// 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, Browse),
|
||||||
|
/// Save clip to disk
|
||||||
|
Export(usize, Browse),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum ConnectName {
|
||||||
|
/** Exact match */
|
||||||
|
Exact(Arc<str>),
|
||||||
|
/** Match regular expression */
|
||||||
|
RegExp(Arc<str>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectScope {
|
||||||
|
One,
|
||||||
|
All
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectStatus {
|
||||||
|
Missing,
|
||||||
|
Disconnected,
|
||||||
|
Connected,
|
||||||
|
Mismatch,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum SamplerMode {
|
||||||
|
// Load sample from path
|
||||||
|
Import(usize, Browse),
|
||||||
|
}
|
||||||
|
|
@ -1,246 +1,4 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
use std::sync::atomic::Ordering;
|
|
||||||
|
|
||||||
/// Things that can provide a [jack::Client] reference.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use tek::{Jack, HasJack};
|
|
||||||
///
|
|
||||||
/// let jack: &Jack = Jacked::default().jack();
|
|
||||||
///
|
|
||||||
/// #[derive(Default)] struct Jacked<'j>(Jack<'j>);
|
|
||||||
///
|
|
||||||
/// impl<'j> tek::HasJack<'j> for Jacked<'j> {
|
|
||||||
/// fn jack (&self) -> &Jack<'j> { &self.0 }
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
pub trait HasJack<'j>: Send + Sync {
|
|
||||||
|
|
||||||
/// Return the internal [jack::Client] handle
|
|
||||||
/// that lets you call the JACK API.
|
|
||||||
fn jack (&self) -> &Jack<'j>;
|
|
||||||
|
|
||||||
fn with_client <T> (&self, op: impl FnOnce(&Client)->T) -> T {
|
|
||||||
self.jack().with_client(op)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn port_by_name (&self, name: &str) -> Option<Port<Unowned>> {
|
|
||||||
self.with_client(|client|client.port_by_name(name))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn port_by_id (&self, id: u32) -> Option<Port<Unowned>> {
|
|
||||||
self.with_client(|c|c.port_by_id(id))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn register_port <PS: PortSpec + Default> (&self, name: impl AsRef<str>) -> Usually<Port<PS>> {
|
|
||||||
self.with_client(|client|Ok(client.register_port(name.as_ref(), PS::default())?))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sync_lead (&self, enable: bool, callback: impl Fn(TimebaseInfo)->Position) -> Usually<()> {
|
|
||||||
if enable {
|
|
||||||
self.with_client(|client|match client.register_timebase_callback(false, callback) {
|
|
||||||
Ok(_) => Ok(()),
|
|
||||||
Err(e) => Err(e)
|
|
||||||
})?
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sync_follow (&self, _enable: bool) -> Usually<()> {
|
|
||||||
// TODO: sync follow
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Trait for thing that has a JACK process callback.
|
|
||||||
pub trait Audio {
|
|
||||||
|
|
||||||
/// Handle a JACK event.
|
|
||||||
fn handle (&mut self, _event: JackEvent) {}
|
|
||||||
|
|
||||||
/// Projecss a JACK chunk.
|
|
||||||
fn process (&mut self, _: &Client, _: &ProcessScope) -> Control {
|
|
||||||
Control::Continue
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The JACK process callback function passed to the server.
|
|
||||||
fn callback (
|
|
||||||
state: &Arc<RwLock<Self>>, client: &Client, scope: &ProcessScope
|
|
||||||
) -> Control where Self: Sized {
|
|
||||||
if let Ok(mut state) = state.write() {
|
|
||||||
state.process(client, scope)
|
|
||||||
} else {
|
|
||||||
Control::Quit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Implement [Audio]: provide JACK callbacks.
|
|
||||||
#[macro_export] macro_rules! audio {
|
|
||||||
|
|
||||||
(|
|
|
||||||
$self1:ident:
|
|
||||||
$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?,$c:ident,$s:ident
|
|
||||||
|$cb:expr$(;|$self2:ident,$e:ident|$cb2:expr)?) => {
|
|
||||||
impl $(<$($L),*$($T $(: $U)?),*>)? Audio for $Struct $(<$($L),*$($T),*>)? {
|
|
||||||
#[inline] fn process (&mut $self1, $c: &Client, $s: &ProcessScope) -> Control { $cb }
|
|
||||||
$(#[inline] fn handle (&mut $self2, $e: JackEvent) { $cb2 })?
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
($Struct:ident: $process:ident, $handle:ident) => {
|
|
||||||
impl Audio for $Struct {
|
|
||||||
#[inline] fn process (&mut self, c: &Client, s: &ProcessScope) -> Control {
|
|
||||||
$process(self, c, s)
|
|
||||||
}
|
|
||||||
#[inline] fn handle (&mut self, e: JackEvent) {
|
|
||||||
$handle(self, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
($Struct:ident: $process:ident) => {
|
|
||||||
impl Audio for $Struct {
|
|
||||||
#[inline] fn process (&mut self, c: &Client, s: &ProcessScope) -> Control {
|
|
||||||
$process(self, c, s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait JackPerfModel {
|
|
||||||
fn update_from_jack_scope (&self, t0: Option<u64>, scope: &ProcessScope);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//pub trait MaybeHas<T>: Send + Sync {
|
|
||||||
//fn get (&self) -> Option<&T>;
|
|
||||||
//}
|
|
||||||
|
|
||||||
pub trait HasN<T>: Send + Sync {
|
|
||||||
fn get_nth (&self, key: usize) -> &T;
|
|
||||||
fn get_nth_mut (&mut self, key: usize) -> &mut T;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait NotePoint {
|
|
||||||
fn note_len (&self) -> &AtomicUsize;
|
|
||||||
/// Get the current length of the note cursor.
|
|
||||||
fn get_note_len (&self) -> usize {
|
|
||||||
self.note_len().load(Relaxed)
|
|
||||||
}
|
|
||||||
/// Set the length of the note cursor, returning the previous value.
|
|
||||||
fn set_note_len (&self, x: usize) -> usize {
|
|
||||||
self.note_len().swap(x, Relaxed)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn note_pos (&self) -> &AtomicUsize;
|
|
||||||
/// Get the current pitch of the note cursor.
|
|
||||||
fn get_note_pos (&self) -> usize {
|
|
||||||
self.note_pos().load(Relaxed).min(127)
|
|
||||||
}
|
|
||||||
/// Set the current pitch fo the note cursor, returning the previous value.
|
|
||||||
fn set_note_pos (&self, x: usize) -> usize {
|
|
||||||
self.note_pos().swap(x.min(127), Relaxed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait TimePoint {
|
|
||||||
fn time_pos (&self) -> &AtomicUsize;
|
|
||||||
/// Get the current time position of the note cursor.
|
|
||||||
fn get_time_pos (&self) -> usize {
|
|
||||||
self.time_pos().load(Relaxed)
|
|
||||||
}
|
|
||||||
/// Set the current time position of the note cursor, returning the previous value.
|
|
||||||
fn set_time_pos (&self, x: usize) -> usize {
|
|
||||||
self.time_pos().swap(x, Relaxed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait MidiPoint: NotePoint + TimePoint {
|
|
||||||
/// Get the current end of the note cursor.
|
|
||||||
fn get_note_end (&self) -> usize {
|
|
||||||
self.get_time_pos() + self.get_note_len()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait TimeRange {
|
|
||||||
fn time_len (&self) -> &AtomicUsize;
|
|
||||||
fn get_time_len (&self) -> usize {
|
|
||||||
self.time_len().load(Ordering::Relaxed)
|
|
||||||
}
|
|
||||||
fn time_zoom (&self) -> &AtomicUsize;
|
|
||||||
fn get_time_zoom (&self) -> usize {
|
|
||||||
self.time_zoom().load(Ordering::Relaxed)
|
|
||||||
}
|
|
||||||
fn set_time_zoom (&self, value: usize) -> usize {
|
|
||||||
self.time_zoom().swap(value, Ordering::Relaxed)
|
|
||||||
}
|
|
||||||
fn time_lock (&self) -> &AtomicBool;
|
|
||||||
fn get_time_lock (&self) -> bool {
|
|
||||||
self.time_lock().load(Ordering::Relaxed)
|
|
||||||
}
|
|
||||||
fn set_time_lock (&self, value: bool) -> bool {
|
|
||||||
self.time_lock().swap(value, Ordering::Relaxed)
|
|
||||||
}
|
|
||||||
fn time_start (&self) -> &AtomicUsize;
|
|
||||||
fn get_time_start (&self) -> usize {
|
|
||||||
self.time_start().load(Ordering::Relaxed)
|
|
||||||
}
|
|
||||||
fn set_time_start (&self, value: usize) -> usize {
|
|
||||||
self.time_start().swap(value, Ordering::Relaxed)
|
|
||||||
}
|
|
||||||
fn time_axis (&self) -> &AtomicUsize;
|
|
||||||
fn get_time_axis (&self) -> usize {
|
|
||||||
self.time_axis().load(Ordering::Relaxed)
|
|
||||||
}
|
|
||||||
fn get_time_end (&self) -> usize {
|
|
||||||
self.time_start().get() + self.time_axis().get() * self.time_zoom().get()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait NoteRange {
|
|
||||||
fn note_lo (&self) -> &AtomicUsize;
|
|
||||||
fn get_note_lo (&self) -> usize {
|
|
||||||
self.note_lo().load(Ordering::Relaxed)
|
|
||||||
}
|
|
||||||
fn set_note_lo (&self, x: usize) -> usize {
|
|
||||||
self.note_lo().swap(x, Ordering::Relaxed)
|
|
||||||
}
|
|
||||||
fn note_axis (&self) -> &AtomicUsize;
|
|
||||||
fn get_note_axis (&self) -> usize {
|
|
||||||
self.note_axis().load(Ordering::Relaxed)
|
|
||||||
}
|
|
||||||
fn get_note_hi (&self) -> usize {
|
|
||||||
(self.note_lo().get() + self.note_axis().get().saturating_sub(1)).min(127)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait MidiRange: TimeRange + NoteRange {}
|
|
||||||
|
|
||||||
/// A unit of time, represented as an atomic 64-bit float.
|
|
||||||
///
|
|
||||||
/// According to https://stackoverflow.com/a/873367, as per IEEE754,
|
|
||||||
/// every integer between 1 and 2^53 can be represented exactly.
|
|
||||||
/// This should mean that, even at 192kHz sampling rate, over 1 year of audio
|
|
||||||
/// can be clocked in microseconds with f64 without losing precision.
|
|
||||||
pub trait TimeUnit: InteriorMutable<f64> {}
|
|
||||||
|
|
||||||
pub trait HasSceneScroll: HasScenes {
|
pub trait HasSceneScroll: HasScenes {
|
||||||
fn scene_scroll (&self) -> usize;
|
fn scene_scroll (&self) -> usize;
|
||||||
|
|
@ -317,9 +75,8 @@ pub trait HasScenes: Has<Vec<Scene>> + Send + Sync {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ```
|
/// ```
|
||||||
/// use tek::{MidiEditor, HasEditor, tengri::Has};
|
|
||||||
/// struct TestEditorHost(Option<MidiEditor>);
|
/// struct TestEditorHost(Option<MidiEditor>);
|
||||||
/// tek::tengri::has!(Option<MidiEditor>: |self: TestEditorHost|self.0);
|
/// has!(Option<MidiEditor>: |self: TestEditorHost|self.0);
|
||||||
/// let mut host = TestEditorHost(Some(MidiEditor::default()));
|
/// let mut host = TestEditorHost(Some(MidiEditor::default()));
|
||||||
/// let _ = host.editor();
|
/// let _ = host.editor();
|
||||||
/// let _ = host.editor_mut();
|
/// let _ = host.editor_mut();
|
||||||
|
|
@ -849,13 +606,19 @@ pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll + M
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + Send + Sync {
|
pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + Send + Sync {
|
||||||
|
|
||||||
/// Default scene height.
|
/// Default scene height.
|
||||||
const H_SCENE: usize = 2;
|
const H_SCENE: usize = 2;
|
||||||
|
|
||||||
/// Default editor height.
|
/// Default editor height.
|
||||||
const H_EDITOR: usize = 15;
|
const H_EDITOR: usize = 15;
|
||||||
|
|
||||||
fn h_scenes (&self) -> u16;
|
fn h_scenes (&self) -> u16;
|
||||||
|
|
||||||
fn w_side (&self) -> u16;
|
fn w_side (&self) -> u16;
|
||||||
|
|
||||||
fn w_mid (&self) -> u16;
|
fn w_mid (&self) -> u16;
|
||||||
|
|
||||||
fn scenes_with_sizes (&self) -> impl ScenesSizes<'_> {
|
fn scenes_with_sizes (&self) -> impl ScenesSizes<'_> {
|
||||||
let mut y = 0;
|
let mut y = 0;
|
||||||
self.scenes().iter().enumerate().skip(self.scene_scroll()).map_while(move|(s, scene)|{
|
self.scenes().iter().enumerate().skip(self.scene_scroll()).map_while(move|(s, scene)|{
|
||||||
|
|
@ -924,61 +687,43 @@ pub trait RegisterPorts: HasJack<'static> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait JackPort: HasJack<'static> {
|
pub trait JackPort: HasJack<'static> {
|
||||||
|
|
||||||
type Port: PortSpec + Default;
|
type Port: PortSpec + Default;
|
||||||
|
|
||||||
type Pair: PortSpec + Default;
|
type Pair: PortSpec + Default;
|
||||||
|
|
||||||
fn new (jack: &Jack<'static>, name: &impl AsRef<str>, connect: &[Connect])
|
fn new (jack: &Jack<'static>, name: &impl AsRef<str>, connect: &[Connect])
|
||||||
-> Usually<Self> where Self: Sized;
|
-> Usually<Self> where Self: Sized;
|
||||||
|
|
||||||
fn register (jack: &Jack<'static>, name: &impl AsRef<str>) -> Usually<Port<Self::Port>> {
|
fn register (jack: &Jack<'static>, name: &impl AsRef<str>) -> Usually<Port<Self::Port>> {
|
||||||
jack.with_client(|c|c.register_port::<Self::Port>(name.as_ref(), Default::default()))
|
jack.with_client(|c|c.register_port::<Self::Port>(name.as_ref(), Default::default()))
|
||||||
.map_err(|e|e.into())
|
.map_err(|e|e.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn port_name (&self) -> &Arc<str>;
|
fn port_name (&self) -> &Arc<str>;
|
||||||
|
|
||||||
fn connections (&self) -> &[Connect];
|
fn connections (&self) -> &[Connect];
|
||||||
|
|
||||||
fn port (&self) -> &Port<Self::Port>;
|
fn port (&self) -> &Port<Self::Port>;
|
||||||
|
|
||||||
fn port_mut (&mut self) -> &mut Port<Self::Port>;
|
fn port_mut (&mut self) -> &mut Port<Self::Port>;
|
||||||
|
|
||||||
fn into_port (self) -> Port<Self::Port> where Self: Sized;
|
fn into_port (self) -> Port<Self::Port> where Self: Sized;
|
||||||
|
|
||||||
fn close (self) -> Usually<()> where Self: Sized {
|
fn close (self) -> Usually<()> where Self: Sized {
|
||||||
let jack = self.jack().clone();
|
let jack = self.jack().clone();
|
||||||
Ok(jack.with_client(|c|c.unregister_port(self.into_port()))?)
|
Ok(jack.with_client(|c|c.unregister_port(self.into_port()))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ports (&self, re_name: Option<&str>, re_type: Option<&str>, flags: PortFlags) -> Vec<String> {
|
fn ports (&self, re_name: Option<&str>, re_type: Option<&str>, flags: PortFlags) -> Vec<String> {
|
||||||
self.with_client(|c|c.ports(re_name, re_type, flags))
|
self.with_client(|c|c.ports(re_name, re_type, flags))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn port_by_id (&self, id: u32) -> Option<Port<Unowned>> {
|
fn port_by_id (&self, id: u32) -> Option<Port<Unowned>> {
|
||||||
self.with_client(|c|c.port_by_id(id))
|
self.with_client(|c|c.port_by_id(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn port_by_name (&self, name: impl AsRef<str>) -> Option<Port<Unowned>> {
|
fn port_by_name (&self, name: impl AsRef<str>) -> Option<Port<Unowned>> {
|
||||||
self.with_client(|c|c.port_by_name(name.as_ref()))
|
self.with_client(|c|c.port_by_name(name.as_ref()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn connect_to_matching <'k> (&'k self) -> Usually<()> {
|
fn connect_to_matching <'k> (&'k self) -> Usually<()> {
|
||||||
for connect in self.connections().iter() {
|
for connect in self.connections().iter() {
|
||||||
match &connect.name {
|
//panic!("{connect:?}");
|
||||||
Some(Exact(name)) => {
|
let status = match &connect.name {
|
||||||
*connect.status.write().unwrap() = self.connect_exact(name)?;
|
Exact(name) => self.connect_exact(name),
|
||||||
},
|
RegExp(re) => self.connect_regexp(re, connect.scope),
|
||||||
Some(RegExp(re)) => {
|
}?;
|
||||||
*connect.status.write().unwrap() = self.connect_regexp(re, connect.scope)?;
|
*connect.status.write().unwrap() = status;
|
||||||
},
|
|
||||||
_ => {},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn connect_exact <'k> (&'k self, name: &str) ->
|
fn connect_exact <'k> (&'k self, name: &str) ->
|
||||||
Usually<Vec<(Port<Unowned>, Arc<str>, ConnectStatus)>>
|
Usually<Vec<(Port<Unowned>, Arc<str>, ConnectStatus)>>
|
||||||
{
|
{
|
||||||
|
|
@ -999,9 +744,8 @@ pub trait JackPort: HasJack<'static> {
|
||||||
Ok(status)
|
Ok(status)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn connect_regexp <'k> (
|
fn connect_regexp <'k> (
|
||||||
&'k self, re: &str, scope: Option<ConnectScope>
|
&'k self, re: &str, scope: ConnectScope
|
||||||
) -> Usually<Vec<(Port<Unowned>, Arc<str>, ConnectStatus)>> {
|
) -> Usually<Vec<(Port<Unowned>, Arc<str>, ConnectStatus)>> {
|
||||||
self.with_client(move|c|{
|
self.with_client(move|c|{
|
||||||
let mut status = vec![];
|
let mut status = vec![];
|
||||||
|
|
@ -1011,7 +755,7 @@ pub trait JackPort: HasJack<'static> {
|
||||||
let port_status = self.connect_to_unowned(&port)?;
|
let port_status = self.connect_to_unowned(&port)?;
|
||||||
let name = port.name()?.into();
|
let name = port.name()?.into();
|
||||||
status.push((port, name, port_status));
|
status.push((port, name, port_status));
|
||||||
if port_status == Connected && scope == Some(One) {
|
if port_status == Connected && scope == One {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1019,7 +763,6 @@ pub trait JackPort: HasJack<'static> {
|
||||||
Ok(status)
|
Ok(status)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Connect to a matching port by name. */
|
/** Connect to a matching port by name. */
|
||||||
fn connect_to_name (&self, name: impl AsRef<str>) -> Usually<ConnectStatus> {
|
fn connect_to_name (&self, name: impl AsRef<str>) -> Usually<ConnectStatus> {
|
||||||
self.with_client(|c|if let Some(ref port) = c.port_by_name(name.as_ref()) {
|
self.with_client(|c|if let Some(ref port) = c.port_by_name(name.as_ref()) {
|
||||||
|
|
@ -1028,7 +771,6 @@ pub trait JackPort: HasJack<'static> {
|
||||||
Ok(Missing)
|
Ok(Missing)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Connect to a matching port by reference. */
|
/** Connect to a matching port by reference. */
|
||||||
fn connect_to_unowned (&self, port: &Port<Unowned>) -> Usually<ConnectStatus> {
|
fn connect_to_unowned (&self, port: &Port<Unowned>) -> Usually<ConnectStatus> {
|
||||||
self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) {
|
self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) {
|
||||||
|
|
@ -1039,7 +781,6 @@ pub trait JackPort: HasJack<'static> {
|
||||||
Mismatch
|
Mismatch
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Connect to an owned matching port by reference. */
|
/** Connect to an owned matching port by reference. */
|
||||||
fn connect_to_owned (&self, port: &Port<Self::Pair>) -> Usually<ConnectStatus> {
|
fn connect_to_owned (&self, port: &Port<Self::Pair>) -> Usually<ConnectStatus> {
|
||||||
self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) {
|
self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) {
|
||||||
|
|
@ -1050,5 +791,4 @@ pub trait JackPort: HasJack<'static> {
|
||||||
Mismatch
|
Mismatch
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
11
device/device_type.rs
Normal file
11
device/device_type.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
pub type MidiData = Vec<Vec<MidiMessage>>;
|
||||||
|
|
||||||
|
pub type ClipPool = Vec<Arc<RwLock<MidiClip>>>;
|
||||||
|
|
||||||
|
pub type CollectedMidiInput<'a> = Vec<Vec<(u32, Result<LiveEvent<'a>, MidiError>)>>;
|
||||||
|
|
||||||
|
pub type SceneWith<'a, T> = (usize, &'a Scene, usize, usize, T);
|
||||||
|
|
||||||
|
pub type MidiSample = (Option<u7>, Arc<RwLock<crate::Sample>>);
|
||||||
22
engine/Cargo.toml
Normal file
22
engine/Cargo.toml
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
[package]
|
||||||
|
name = "tek_engine"
|
||||||
|
edition = { workspace = true }
|
||||||
|
version = { workspace = true }
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "engine.rs"
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "linux")']
|
||||||
|
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
atomic_float = { workspace = true }
|
||||||
|
jack = { workspace = true }
|
||||||
|
midly = { workspace = true }
|
||||||
|
tengri = { workspace = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
proptest = { workspace = true }
|
||||||
|
proptest-derive = { workspace = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
196
engine/engine.rs
Normal file
196
engine/engine.rs
Normal file
|
|
@ -0,0 +1,196 @@
|
||||||
|
pub extern crate jack;
|
||||||
|
pub extern crate midly;
|
||||||
|
pub extern crate tengri;
|
||||||
|
mod engine_structs; pub use self::engine_structs::*;
|
||||||
|
mod engine_traits; pub use self::engine_traits::*;
|
||||||
|
mod engine_impls;
|
||||||
|
mod jack_structs; pub use self::jack_structs::*;
|
||||||
|
mod jack_traits; pub use self::jack_traits::*;
|
||||||
|
mod jack_types; pub use self::jack_types::*;
|
||||||
|
mod jack_impls;
|
||||||
|
pub(crate) use ::{
|
||||||
|
tengri::{Usually, from, output::PerfModel},
|
||||||
|
atomic_float::AtomicF64,
|
||||||
|
std::{
|
||||||
|
fmt::Debug, ops::{Add, Sub, Mul, Div, Rem},
|
||||||
|
sync::{Arc, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
pub use ::{
|
||||||
|
jack::{*, contrib::{*, ClosureProcessHandler}},
|
||||||
|
midly::{Smf, TrackEventKind, MidiMessage, Error as MidiError, num::*, live::*,}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const DEFAULT_PPQ: f64 = 96.0;
|
||||||
|
|
||||||
|
/// FIXME: remove this and use PPQ from timebase everywhere:
|
||||||
|
pub const PPQ: usize = 96;
|
||||||
|
|
||||||
|
/// (pulses, name), assuming 96 PPQ
|
||||||
|
pub const NOTE_DURATIONS: [(usize, &str);26] = [
|
||||||
|
(1, "1/384"), (2, "1/192"),
|
||||||
|
(3, "1/128"), (4, "1/96"),
|
||||||
|
(6, "1/64"), (8, "1/48"),
|
||||||
|
(12, "1/32"), (16, "1/24"),
|
||||||
|
(24, "1/16"), (32, "1/12"),
|
||||||
|
(48, "1/8"), (64, "1/6"),
|
||||||
|
(96, "1/4"), (128, "1/3"),
|
||||||
|
(192, "1/2"), (256, "2/3"),
|
||||||
|
(384, "1/1"), (512, "4/3"),
|
||||||
|
(576, "3/2"), (768, "2/1"),
|
||||||
|
(1152, "3/1"), (1536, "4/1"),
|
||||||
|
(2304, "6/1"), (3072, "8/1"),
|
||||||
|
(3456, "9/1"), (6144, "16/1"),
|
||||||
|
];
|
||||||
|
|
||||||
|
pub const NOTE_NAMES: [&str; 128] = [
|
||||||
|
"C0", "C#0", "D0", "D#0", "E0", "F0", "F#0", "G0", "G#0", "A0", "A#0", "B0",
|
||||||
|
"C1", "C#1", "D1", "D#1", "E1", "F1", "F#1", "G1", "G#1", "A1", "A#1", "B1",
|
||||||
|
"C2", "C#2", "D2", "D#2", "E2", "F2", "F#2", "G2", "G#2", "A2", "A#2", "B2",
|
||||||
|
"C3", "C#3", "D3", "D#3", "E3", "F3", "F#3", "G3", "G#3", "A3", "A#3", "B3",
|
||||||
|
"C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4",
|
||||||
|
"C5", "C#5", "D5", "D#5", "E5", "F5", "F#5", "G5", "G#5", "A5", "A#5", "B5",
|
||||||
|
"C6", "C#6", "D6", "D#6", "E6", "F6", "F#6", "G6", "G#6", "A6", "A#6", "B6",
|
||||||
|
"C7", "C#7", "D7", "D#7", "E7", "F7", "F#7", "G7", "G#7", "A7", "A#7", "B7",
|
||||||
|
"C8", "C#8", "D8", "D#8", "E8", "F8", "F#8", "G8", "G#8", "A8", "A#8", "B8",
|
||||||
|
"C9", "C#9", "D9", "D#9", "E9", "F9", "F#9", "G9", "G#9", "A9", "A#9", "B9",
|
||||||
|
"C10", "C#10", "D10", "D#10", "E10", "F10", "F#10", "G10",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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; },
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the next shorter length
|
||||||
|
pub fn note_duration_prev (pulses: usize) -> usize {
|
||||||
|
for (length, _) in NOTE_DURATIONS.iter().rev() { if *length < pulses { return *length } }
|
||||||
|
pulses
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the next longer length
|
||||||
|
pub fn note_duration_next (pulses: usize) -> usize {
|
||||||
|
for (length, _) in NOTE_DURATIONS.iter() { if *length > pulses { return *length } }
|
||||||
|
pulses
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn note_duration_to_name (pulses: usize) -> &'static str {
|
||||||
|
for (length, name) in NOTE_DURATIONS.iter() { if *length == pulses { return name } }
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn note_pitch_to_name (n: usize) -> &'static str {
|
||||||
|
if n > 127 {
|
||||||
|
panic!("to_note_name({n}): must be 0-127");
|
||||||
|
}
|
||||||
|
NOTE_NAMES[n]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//macro_rules! impl_port {
|
||||||
|
//($Name:ident : $Spec:ident -> $Pair:ident |$jack:ident, $name:ident|$port:expr) => {
|
||||||
|
//#[derive(Debug)] pub struct $Name {
|
||||||
|
///// Handle to JACK client, for receiving reconnect events.
|
||||||
|
//jack: Jack<'static>,
|
||||||
|
///// Port name
|
||||||
|
//name: Arc<str>,
|
||||||
|
///// Port handle.
|
||||||
|
//port: Port<$Spec>,
|
||||||
|
///// List of ports to connect to.
|
||||||
|
//conn: Vec<PortConnect>
|
||||||
|
//}
|
||||||
|
//impl AsRef<Port<$Spec>> for $Name {
|
||||||
|
//fn as_ref (&self) -> &Port<$Spec> { &self.port }
|
||||||
|
//}
|
||||||
|
//impl $Name {
|
||||||
|
//pub fn new ($jack: &Jack, name: impl AsRef<str>, connect: &[PortConnect])
|
||||||
|
//-> Usually<Self>
|
||||||
|
//{
|
||||||
|
//let $name = name.as_ref();
|
||||||
|
//let jack = $jack.clone();
|
||||||
|
//let port = $port?;
|
||||||
|
//let name = $name.into();
|
||||||
|
//let conn = connect.to_vec();
|
||||||
|
//let port = Self { jack, port, name, conn };
|
||||||
|
//port.connect_to_matching()?;
|
||||||
|
//Ok(port)
|
||||||
|
//}
|
||||||
|
//pub fn name (&self) -> &Arc<str> { &self.name }
|
||||||
|
//pub fn port (&self) -> &Port<$Spec> { &self.port }
|
||||||
|
//pub fn port_mut (&mut self) -> &mut Port<$Spec> { &mut self.port }
|
||||||
|
//pub fn into_port (self) -> Port<$Spec> { self.port }
|
||||||
|
//pub fn close (self) -> Usually<()> {
|
||||||
|
//let Self { jack, port, .. } = self;
|
||||||
|
//Ok(jack.with_client(|client|client.unregister_port(port))?)
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//impl HasJack<'static> for $Name {
|
||||||
|
//fn jack (&self) -> &'static Jack<'static> { &self.jack }
|
||||||
|
//}
|
||||||
|
//impl JackPort<'static> for $Name {
|
||||||
|
//type Port = $Spec;
|
||||||
|
//type Pair = $Pair;
|
||||||
|
//fn port (&self) -> &Port<$Spec> { &self.port }
|
||||||
|
//}
|
||||||
|
//impl ConnectTo<'static, &str> for $Name {
|
||||||
|
//fn connect_to (&self, to: &str) -> Usually<PortConnectStatus> {
|
||||||
|
//self.with_client(|c|if let Some(ref port) = c.port_by_name(to.as_ref()) {
|
||||||
|
//self.connect_to(port)
|
||||||
|
//} else {
|
||||||
|
//Ok(Missing)
|
||||||
|
//})
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//impl ConnectTo<'static, &Port<Unowned>> for $Name {
|
||||||
|
//fn connect_to (&self, port: &Port<Unowned>) -> Usually<PortConnectStatus> {
|
||||||
|
//self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(&self.port, port) {
|
||||||
|
//Connected
|
||||||
|
//} else if let Ok(_) = c.connect_ports(port, &self.port) {
|
||||||
|
//Connected
|
||||||
|
//} else {
|
||||||
|
//Mismatch
|
||||||
|
//}))
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//impl ConnectTo<'static, &Port<$Pair>> for $Name {
|
||||||
|
//fn connect_to (&self, port: &Port<$Pair>) -> Usually<PortConnectStatus> {
|
||||||
|
//self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(&self.port, port) {
|
||||||
|
//Connected
|
||||||
|
//} else if let Ok(_) = c.connect_ports(port, &self.port) {
|
||||||
|
//Connected
|
||||||
|
//} else {
|
||||||
|
//Mismatch
|
||||||
|
//}))
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//impl ConnectAuto<'static> for $Name {
|
||||||
|
//fn connections (&self) -> &[PortConnect] {
|
||||||
|
//&self.conn
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//};
|
||||||
|
//}
|
||||||
363
engine/engine_impls.rs
Normal file
363
engine/engine_impls.rs
Normal file
|
|
@ -0,0 +1,363 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//impl<T, U: Has<Option<T>>> MaybeHas<T> for U {
|
||||||
|
//fn get (&self) -> Option<&T> {
|
||||||
|
//Has::<Option<T>>::get(self).as_ref()
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
|
||||||
|
impl Default for MidiCursor {
|
||||||
|
fn default () -> Self {
|
||||||
|
Self {
|
||||||
|
time_pos: Arc::new(0.into()),
|
||||||
|
note_pos: Arc::new(36.into()),
|
||||||
|
note_len: Arc::new(24.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NotePoint for MidiCursor {
|
||||||
|
fn note_len (&self) -> &AtomicUsize {
|
||||||
|
&self.note_len
|
||||||
|
}
|
||||||
|
fn note_pos (&self) -> &AtomicUsize {
|
||||||
|
&self.note_pos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimePoint for MidiCursor {
|
||||||
|
fn time_pos (&self) -> &AtomicUsize {
|
||||||
|
self.time_pos.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: NotePoint + TimePoint> MidiPoint for T {}
|
||||||
|
|
||||||
|
from!(MidiSelection: |data:(usize, bool)| Self {
|
||||||
|
time_len: Arc::new(0.into()),
|
||||||
|
note_axis: Arc::new(0.into()),
|
||||||
|
note_lo: Arc::new(0.into()),
|
||||||
|
time_axis: Arc::new(0.into()),
|
||||||
|
time_start: Arc::new(0.into()),
|
||||||
|
time_zoom: Arc::new(data.0.into()),
|
||||||
|
time_lock: Arc::new(data.1.into()),
|
||||||
|
});
|
||||||
|
|
||||||
|
impl<T: TimeRange + NoteRange> MidiRange for T {}
|
||||||
|
|
||||||
|
impl TimeRange for MidiSelection {
|
||||||
|
fn time_len (&self) -> &AtomicUsize { &self.time_len }
|
||||||
|
fn time_zoom (&self) -> &AtomicUsize { &self.time_zoom }
|
||||||
|
fn time_lock (&self) -> &AtomicBool { &self.time_lock }
|
||||||
|
fn time_start (&self) -> &AtomicUsize { &self.time_start }
|
||||||
|
fn time_axis (&self) -> &AtomicUsize { &self.time_axis }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NoteRange for MidiSelection {
|
||||||
|
fn note_lo (&self) -> &AtomicUsize { &self.note_lo }
|
||||||
|
fn note_axis (&self) -> &AtomicUsize { &self.note_axis }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Moment {
|
||||||
|
pub fn zero (timebase: &Arc<Timebase>) -> Self {
|
||||||
|
Self { usec: 0.into(), sample: 0.into(), pulse: 0.into(), timebase: timebase.clone() }
|
||||||
|
}
|
||||||
|
pub fn from_usec (timebase: &Arc<Timebase>, usec: f64) -> Self {
|
||||||
|
Self {
|
||||||
|
usec: usec.into(),
|
||||||
|
sample: timebase.sr.usecs_to_sample(usec).into(),
|
||||||
|
pulse: timebase.usecs_to_pulse(usec).into(),
|
||||||
|
timebase: timebase.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn from_sample (timebase: &Arc<Timebase>, sample: f64) -> Self {
|
||||||
|
Self {
|
||||||
|
sample: sample.into(),
|
||||||
|
usec: timebase.sr.samples_to_usec(sample).into(),
|
||||||
|
pulse: timebase.samples_to_pulse(sample).into(),
|
||||||
|
timebase: timebase.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn from_pulse (timebase: &Arc<Timebase>, pulse: f64) -> Self {
|
||||||
|
Self {
|
||||||
|
pulse: pulse.into(),
|
||||||
|
sample: timebase.pulses_to_sample(pulse).into(),
|
||||||
|
usec: timebase.pulses_to_usec(pulse).into(),
|
||||||
|
timebase: timebase.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[inline] pub fn update_from_usec (&self, usec: f64) {
|
||||||
|
self.usec.set(usec);
|
||||||
|
self.pulse.set(self.timebase.usecs_to_pulse(usec));
|
||||||
|
self.sample.set(self.timebase.sr.usecs_to_sample(usec));
|
||||||
|
}
|
||||||
|
#[inline] pub fn update_from_sample (&self, sample: f64) {
|
||||||
|
self.usec.set(self.timebase.sr.samples_to_usec(sample));
|
||||||
|
self.pulse.set(self.timebase.samples_to_pulse(sample));
|
||||||
|
self.sample.set(sample);
|
||||||
|
}
|
||||||
|
#[inline] pub fn update_from_pulse (&self, pulse: f64) {
|
||||||
|
self.usec.set(self.timebase.pulses_to_usec(pulse));
|
||||||
|
self.pulse.set(pulse);
|
||||||
|
self.sample.set(self.timebase.pulses_to_sample(pulse));
|
||||||
|
}
|
||||||
|
#[inline] pub fn format_beat (&self) -> Arc<str> {
|
||||||
|
self.timebase.format_beats_1(self.pulse.get()).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LaunchSync {
|
||||||
|
pub fn next (&self) -> f64 {
|
||||||
|
note_duration_next(self.get() as usize) as f64
|
||||||
|
}
|
||||||
|
pub fn prev (&self) -> f64 {
|
||||||
|
note_duration_prev(self.get() as usize) as f64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Quantize {
|
||||||
|
pub fn next (&self) -> f64 {
|
||||||
|
note_duration_next(self.get() as usize) as f64
|
||||||
|
}
|
||||||
|
pub fn prev (&self) -> f64 {
|
||||||
|
note_duration_prev(self.get() as usize) as f64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for TicksIterator {
|
||||||
|
type Item = (usize, usize);
|
||||||
|
fn next (&mut self) -> Option<Self::Item> {
|
||||||
|
loop {
|
||||||
|
if self.sample > self.end { return None }
|
||||||
|
let spp = self.spp;
|
||||||
|
let sample = self.sample as f64;
|
||||||
|
let start = self.start;
|
||||||
|
let end = self.end;
|
||||||
|
self.sample += 1;
|
||||||
|
//println!("{spp} {sample} {start} {end}");
|
||||||
|
let jitter = sample.rem_euclid(spp); // ramps
|
||||||
|
let next_jitter = (sample + 1.0).rem_euclid(spp);
|
||||||
|
if jitter > next_jitter { // at crossing:
|
||||||
|
let time = (sample as usize) % (end as usize-start as usize);
|
||||||
|
let tick = (sample / spp) as usize;
|
||||||
|
return Some((time, tick))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Timebase {
|
||||||
|
/// Specify sample rate, BPM and PPQ
|
||||||
|
pub fn new (
|
||||||
|
s: impl Into<SampleRate>,
|
||||||
|
b: impl Into<BeatsPerMinute>,
|
||||||
|
p: impl Into<PulsesPerQuaver>
|
||||||
|
) -> Self {
|
||||||
|
Self { sr: s.into(), bpm: b.into(), ppq: p.into() }
|
||||||
|
}
|
||||||
|
/// Iterate over ticks between start and end.
|
||||||
|
#[inline] pub fn pulses_between_samples (&self, start: usize, end: usize) -> TicksIterator {
|
||||||
|
TicksIterator { spp: self.samples_per_pulse(), sample: start, start, end }
|
||||||
|
}
|
||||||
|
/// Return the duration fo a beat in microseconds
|
||||||
|
#[inline] pub fn usec_per_beat (&self) -> f64 { 60_000_000f64 / self.bpm.get() }
|
||||||
|
/// Return the number of beats in a second
|
||||||
|
#[inline] pub fn beat_per_second (&self) -> f64 { self.bpm.get() / 60f64 }
|
||||||
|
/// Return the number of microseconds corresponding to a note of the given duration
|
||||||
|
#[inline] pub fn note_to_usec (&self, (num, den): (f64, f64)) -> f64 {
|
||||||
|
4.0 * self.usec_per_beat() * num / den
|
||||||
|
}
|
||||||
|
/// Return duration of a pulse in microseconds (BPM-dependent)
|
||||||
|
#[inline] pub fn pulse_per_usec (&self) -> f64 { self.ppq.get() / self.usec_per_beat() }
|
||||||
|
/// Return duration of a pulse in microseconds (BPM-dependent)
|
||||||
|
#[inline] pub fn usec_per_pulse (&self) -> f64 { self.usec_per_beat() / self.ppq.get() }
|
||||||
|
/// Return number of pulses to which a number of microseconds corresponds (BPM-dependent)
|
||||||
|
#[inline] pub fn usecs_to_pulse (&self, usec: f64) -> f64 { usec * self.pulse_per_usec() }
|
||||||
|
/// Convert a number of pulses to a sample number (SR- and BPM-dependent)
|
||||||
|
#[inline] pub fn pulses_to_usec (&self, pulse: f64) -> f64 { pulse / self.usec_per_pulse() }
|
||||||
|
/// Return number of pulses in a second (BPM-dependent)
|
||||||
|
#[inline] pub fn pulses_per_second (&self) -> f64 { self.beat_per_second() * self.ppq.get() }
|
||||||
|
/// Return fraction of a pulse to which a sample corresponds (SR- and BPM-dependent)
|
||||||
|
#[inline] pub fn pulses_per_sample (&self) -> f64 {
|
||||||
|
self.usec_per_pulse() / self.sr.usec_per_sample()
|
||||||
|
}
|
||||||
|
/// Return number of samples in a pulse (SR- and BPM-dependent)
|
||||||
|
#[inline] pub fn samples_per_pulse (&self) -> f64 {
|
||||||
|
self.sr.get() / self.pulses_per_second()
|
||||||
|
}
|
||||||
|
/// Convert a number of pulses to a sample number (SR- and BPM-dependent)
|
||||||
|
#[inline] pub fn pulses_to_sample (&self, p: f64) -> f64 {
|
||||||
|
self.pulses_per_sample() * p
|
||||||
|
}
|
||||||
|
/// Convert a number of samples to a pulse number (SR- and BPM-dependent)
|
||||||
|
#[inline] pub fn samples_to_pulse (&self, s: f64) -> f64 {
|
||||||
|
s / self.pulses_per_sample()
|
||||||
|
}
|
||||||
|
/// Return the number of samples corresponding to a note of the given duration
|
||||||
|
#[inline] pub fn note_to_samples (&self, note: (f64, f64)) -> f64 {
|
||||||
|
self.usec_to_sample(self.note_to_usec(note))
|
||||||
|
}
|
||||||
|
/// Return the number of samples corresponding to the given number of microseconds
|
||||||
|
#[inline] pub fn usec_to_sample (&self, usec: f64) -> f64 {
|
||||||
|
usec * self.sr.get() / 1000f64
|
||||||
|
}
|
||||||
|
/// Return the quantized position of a moment in time given a step
|
||||||
|
#[inline] pub fn quantize (&self, step: (f64, f64), time: f64) -> (f64, f64) {
|
||||||
|
let step = self.note_to_usec(step);
|
||||||
|
(time / step, time % step)
|
||||||
|
}
|
||||||
|
/// Quantize a collection of events
|
||||||
|
#[inline] pub fn quantize_into <E: Iterator<Item=(f64, f64)> + Sized, T> (
|
||||||
|
&self, step: (f64, f64), events: E
|
||||||
|
) -> Vec<(f64, f64)> {
|
||||||
|
events.map(|(time, event)|(self.quantize(step, time).0, event)).collect()
|
||||||
|
}
|
||||||
|
/// Format a number of pulses into Beat.Bar.Pulse starting from 0
|
||||||
|
#[inline] pub fn format_beats_0 (&self, pulse: f64) -> Arc<str> {
|
||||||
|
let pulse = pulse as usize;
|
||||||
|
let ppq = self.ppq.get() as usize;
|
||||||
|
let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) };
|
||||||
|
format!("{}.{}.{pulses:02}", beats / 4, beats % 4).into()
|
||||||
|
}
|
||||||
|
/// Format a number of pulses into Beat.Bar starting from 0
|
||||||
|
#[inline] pub fn format_beats_0_short (&self, pulse: f64) -> Arc<str> {
|
||||||
|
let pulse = pulse as usize;
|
||||||
|
let ppq = self.ppq.get() as usize;
|
||||||
|
let beats = if ppq > 0 { pulse / ppq } else { 0 };
|
||||||
|
format!("{}.{}", beats / 4, beats % 4).into()
|
||||||
|
}
|
||||||
|
/// Format a number of pulses into Beat.Bar.Pulse starting from 1
|
||||||
|
#[inline] pub fn format_beats_1 (&self, pulse: f64) -> Arc<str> {
|
||||||
|
let mut string = String::with_capacity(16);
|
||||||
|
self.format_beats_1_to(&mut string, pulse).expect("failed to format {pulse} into beat");
|
||||||
|
string.into()
|
||||||
|
}
|
||||||
|
/// Format a number of pulses into Beat.Bar.Pulse starting from 1
|
||||||
|
#[inline] pub fn format_beats_1_to (&self, w: &mut impl std::fmt::Write, pulse: f64) -> Result<(), std::fmt::Error> {
|
||||||
|
let pulse = pulse as usize;
|
||||||
|
let ppq = self.ppq.get() as usize;
|
||||||
|
let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) };
|
||||||
|
write!(w, "{}.{}.{pulses:02}", beats / 4 + 1, beats % 4 + 1)
|
||||||
|
}
|
||||||
|
/// Format a number of pulses into Beat.Bar.Pulse starting from 1
|
||||||
|
#[inline] pub fn format_beats_1_short (&self, pulse: f64) -> Arc<str> {
|
||||||
|
let pulse = pulse as usize;
|
||||||
|
let ppq = self.ppq.get() as usize;
|
||||||
|
let beats = if ppq > 0 { pulse / ppq } else { 0 };
|
||||||
|
format!("{}.{}", beats / 4 + 1, beats % 4 + 1).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Timebase {
|
||||||
|
fn default () -> Self { Self::new(48000f64, 150f64, DEFAULT_PPQ) }
|
||||||
|
}
|
||||||
|
impl SampleRate {
|
||||||
|
/// Return the duration of a sample in microseconds (floating)
|
||||||
|
#[inline] pub fn usec_per_sample (&self) -> f64 {
|
||||||
|
1_000_000f64 / self.get()
|
||||||
|
}
|
||||||
|
/// Return the duration of a sample in microseconds (floating)
|
||||||
|
#[inline] pub fn sample_per_usec (&self) -> f64 {
|
||||||
|
self.get() / 1_000_000f64
|
||||||
|
}
|
||||||
|
/// Convert a number of samples to microseconds (floating)
|
||||||
|
#[inline] pub fn samples_to_usec (&self, samples: f64) -> f64 {
|
||||||
|
self.usec_per_sample() * samples
|
||||||
|
}
|
||||||
|
/// Convert a number of microseconds to samples (floating)
|
||||||
|
#[inline] pub fn usecs_to_sample (&self, usecs: f64) -> f64 {
|
||||||
|
self.sample_per_usec() * usecs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Microsecond {
|
||||||
|
#[inline] pub fn format_msu (&self) -> Arc<str> {
|
||||||
|
let usecs = self.get() as usize;
|
||||||
|
let (seconds, msecs) = (usecs / 1000000, usecs / 1000 % 1000);
|
||||||
|
let (minutes, seconds) = (seconds / 60, seconds % 60);
|
||||||
|
format!("{minutes}:{seconds:02}:{msecs:03}").into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implement an arithmetic operation for a unit of time
|
||||||
|
#[macro_export] macro_rules! impl_op {
|
||||||
|
($T:ident, $Op:ident, $method:ident, |$a:ident,$b:ident|{$impl:expr}) => {
|
||||||
|
impl $Op<Self> for $T {
|
||||||
|
type Output = Self; #[inline] fn $method (self, other: Self) -> Self::Output {
|
||||||
|
let $a = self.get(); let $b = other.get(); Self($impl.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl $Op<usize> for $T {
|
||||||
|
type Output = Self; #[inline] fn $method (self, other: usize) -> Self::Output {
|
||||||
|
let $a = self.get(); let $b = other as f64; Self($impl.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl $Op<f64> for $T {
|
||||||
|
type Output = Self; #[inline] fn $method (self, other: f64) -> Self::Output {
|
||||||
|
let $a = self.get(); let $b = other; Self($impl.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Define and implement a unit of time
|
||||||
|
#[macro_export] macro_rules! impl_time_unit {
|
||||||
|
($T:ident) => {
|
||||||
|
impl Gettable<f64> for $T {
|
||||||
|
fn get (&self) -> f64 { self.0.load(Relaxed) }
|
||||||
|
}
|
||||||
|
impl InteriorMutable<f64> for $T {
|
||||||
|
fn set (&self, value: f64) -> f64 {
|
||||||
|
let old = self.get();
|
||||||
|
self.0.store(value, Relaxed);
|
||||||
|
old
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl TimeUnit for $T {}
|
||||||
|
impl_op!($T, Add, add, |a, b|{a + b});
|
||||||
|
impl_op!($T, Sub, sub, |a, b|{a - b});
|
||||||
|
impl_op!($T, Mul, mul, |a, b|{a * b});
|
||||||
|
impl_op!($T, Div, div, |a, b|{a / b});
|
||||||
|
impl_op!($T, Rem, rem, |a, b|{a % b});
|
||||||
|
impl From<f64> for $T { fn from (value: f64) -> Self { Self(value.into()) } }
|
||||||
|
impl From<usize> for $T { fn from (value: usize) -> Self { Self((value as f64).into()) } }
|
||||||
|
impl From<$T> for f64 { fn from (value: $T) -> Self { value.get() } }
|
||||||
|
impl From<$T> for usize { fn from (value: $T) -> Self { value.get() as usize } }
|
||||||
|
impl From<&$T> for f64 { fn from (value: &$T) -> Self { value.get() } }
|
||||||
|
impl From<&$T> for usize { fn from (value: &$T) -> Self { value.get() as usize } }
|
||||||
|
impl Clone for $T { fn clone (&self) -> Self { Self(self.get().into()) } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_time_unit!(SampleCount);
|
||||||
|
impl_time_unit!(SampleRate);
|
||||||
|
impl_time_unit!(Microsecond);
|
||||||
|
impl_time_unit!(Quantize);
|
||||||
|
impl_time_unit!(PulsesPerQuaver);
|
||||||
|
impl_time_unit!(Pulse);
|
||||||
|
impl_time_unit!(BeatsPerMinute);
|
||||||
|
impl_time_unit!(LaunchSync);
|
||||||
156
engine/engine_structs.rs
Normal file
156
engine/engine_structs.rs
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// Temporal resolutions: sample rate, tempo, MIDI pulses per quaver (beat)
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone)] pub struct Timebase {
|
||||||
|
/// Audio samples per second
|
||||||
|
pub sr: SampleRate,
|
||||||
|
/// MIDI beats per minute
|
||||||
|
pub bpm: BeatsPerMinute,
|
||||||
|
/// MIDI ticks per beat
|
||||||
|
pub ppq: PulsesPerQuaver,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterator that emits subsequent ticks within a range.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Default)] pub struct TicksIterator {
|
||||||
|
pub spp: f64,
|
||||||
|
pub sample: usize,
|
||||||
|
pub start: usize,
|
||||||
|
pub end: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let model = MidiSelection::from((1, false));
|
||||||
|
///
|
||||||
|
/// let _ = model.get_time_len();
|
||||||
|
/// let _ = model.get_time_zoom();
|
||||||
|
/// let _ = model.get_time_lock();
|
||||||
|
/// let _ = model.get_time_start();
|
||||||
|
/// let _ = model.get_time_axis();
|
||||||
|
/// let _ = model.get_time_end();
|
||||||
|
///
|
||||||
|
/// let _ = model.get_note_lo();
|
||||||
|
/// let _ = model.get_note_axis();
|
||||||
|
/// let _ = model.get_note_hi();
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone)] pub struct MidiCursor {
|
||||||
|
/// Time coordinate of cursor
|
||||||
|
pub time_pos: Arc<AtomicUsize>,
|
||||||
|
/// Note coordinate of cursor
|
||||||
|
pub note_pos: Arc<AtomicUsize>,
|
||||||
|
/// Length of note that will be inserted, in pulses
|
||||||
|
pub note_len: Arc<AtomicUsize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default)] pub struct MidiSelection {
|
||||||
|
pub time_len: Arc<AtomicUsize>,
|
||||||
|
/// Length of visible time axis
|
||||||
|
pub time_axis: Arc<AtomicUsize>,
|
||||||
|
/// Earliest time displayed
|
||||||
|
pub time_start: Arc<AtomicUsize>,
|
||||||
|
/// Time step
|
||||||
|
pub time_zoom: Arc<AtomicUsize>,
|
||||||
|
/// Auto rezoom to fit in time axis
|
||||||
|
pub time_lock: Arc<AtomicBool>,
|
||||||
|
/// Length of visible note axis
|
||||||
|
pub note_axis: Arc<AtomicUsize>,
|
||||||
|
// Lowest note displayed
|
||||||
|
pub note_lo: Arc<AtomicUsize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A point in time in all time scales (microsecond, sample, MIDI pulse)
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct Moment {
|
||||||
|
pub timebase: Arc<Timebase>,
|
||||||
|
/// Current time in microseconds
|
||||||
|
pub usec: Microsecond,
|
||||||
|
/// Current time in audio samples
|
||||||
|
pub sample: SampleCount,
|
||||||
|
/// Current time in MIDI pulses
|
||||||
|
pub pulse: Pulse,
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub enum Moment2 {
|
||||||
|
#[default] None,
|
||||||
|
Zero,
|
||||||
|
Usec(Microsecond),
|
||||||
|
Sample(SampleCount),
|
||||||
|
Pulse(Pulse),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MIDI resolution in PPQ (pulses per quarter note)
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Default)] pub struct PulsesPerQuaver (pub(crate) AtomicF64);
|
||||||
|
|
||||||
|
/// Timestamp in MIDI pulses
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Default)] pub struct Pulse (pub(crate) AtomicF64);
|
||||||
|
|
||||||
|
/// Tempo in beats per minute
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Default)] pub struct BeatsPerMinute (pub(crate) AtomicF64);
|
||||||
|
|
||||||
|
/// Quantization setting for launching clips
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Default)] pub struct LaunchSync (pub(crate) AtomicF64);
|
||||||
|
|
||||||
|
/// Quantization setting for notes
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Default)] pub struct Quantize (pub(crate) AtomicF64);
|
||||||
|
|
||||||
|
/// Timestamp in audio samples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Default)] pub struct SampleCount (pub(crate) AtomicF64);
|
||||||
|
|
||||||
|
/// Audio sample rate in Hz (samples per second)
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Default)] pub struct SampleRate (pub(crate) AtomicF64);
|
||||||
|
|
||||||
|
/// Timestamp in microseconds
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Default)] pub struct Microsecond (pub(crate) AtomicF64);
|
||||||
129
engine/engine_traits.rs
Normal file
129
engine/engine_traits.rs
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
use crate::*;
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
|
||||||
|
//pub trait MaybeHas<T>: Send + Sync {
|
||||||
|
//fn get (&self) -> Option<&T>;
|
||||||
|
//}
|
||||||
|
|
||||||
|
pub trait HasN<T>: Send + Sync {
|
||||||
|
fn get_nth (&self, key: usize) -> &T;
|
||||||
|
fn get_nth_mut (&mut self, key: usize) -> &mut T;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait NotePoint {
|
||||||
|
fn note_len (&self) -> &AtomicUsize;
|
||||||
|
/// Get the current length of the note cursor.
|
||||||
|
fn get_note_len (&self) -> usize {
|
||||||
|
self.note_len().load(Relaxed)
|
||||||
|
}
|
||||||
|
/// Set the length of the note cursor, returning the previous value.
|
||||||
|
fn set_note_len (&self, x: usize) -> usize {
|
||||||
|
self.note_len().swap(x, Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn note_pos (&self) -> &AtomicUsize;
|
||||||
|
/// Get the current pitch of the note cursor.
|
||||||
|
fn get_note_pos (&self) -> usize {
|
||||||
|
self.note_pos().load(Relaxed).min(127)
|
||||||
|
}
|
||||||
|
/// Set the current pitch fo the note cursor, returning the previous value.
|
||||||
|
fn set_note_pos (&self, x: usize) -> usize {
|
||||||
|
self.note_pos().swap(x.min(127), Relaxed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait TimePoint {
|
||||||
|
fn time_pos (&self) -> &AtomicUsize;
|
||||||
|
/// Get the current time position of the note cursor.
|
||||||
|
fn get_time_pos (&self) -> usize {
|
||||||
|
self.time_pos().load(Relaxed)
|
||||||
|
}
|
||||||
|
/// Set the current time position of the note cursor, returning the previous value.
|
||||||
|
fn set_time_pos (&self, x: usize) -> usize {
|
||||||
|
self.time_pos().swap(x, Relaxed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait MidiPoint: NotePoint + TimePoint {
|
||||||
|
/// Get the current end of the note cursor.
|
||||||
|
fn get_note_end (&self) -> usize {
|
||||||
|
self.get_time_pos() + self.get_note_len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait TimeRange {
|
||||||
|
fn time_len (&self) -> &AtomicUsize;
|
||||||
|
fn get_time_len (&self) -> usize {
|
||||||
|
self.time_len().load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
fn time_zoom (&self) -> &AtomicUsize;
|
||||||
|
fn get_time_zoom (&self) -> usize {
|
||||||
|
self.time_zoom().load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
fn set_time_zoom (&self, value: usize) -> usize {
|
||||||
|
self.time_zoom().swap(value, Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
fn time_lock (&self) -> &AtomicBool;
|
||||||
|
fn get_time_lock (&self) -> bool {
|
||||||
|
self.time_lock().load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
fn set_time_lock (&self, value: bool) -> bool {
|
||||||
|
self.time_lock().swap(value, Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
fn time_start (&self) -> &AtomicUsize;
|
||||||
|
fn get_time_start (&self) -> usize {
|
||||||
|
self.time_start().load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
fn set_time_start (&self, value: usize) -> usize {
|
||||||
|
self.time_start().swap(value, Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
fn time_axis (&self) -> &AtomicUsize;
|
||||||
|
fn get_time_axis (&self) -> usize {
|
||||||
|
self.time_axis().load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
fn get_time_end (&self) -> usize {
|
||||||
|
self.time_start().get() + self.time_axis().get() * self.time_zoom().get()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait NoteRange {
|
||||||
|
fn note_lo (&self) -> &AtomicUsize;
|
||||||
|
fn get_note_lo (&self) -> usize {
|
||||||
|
self.note_lo().load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
fn set_note_lo (&self, x: usize) -> usize {
|
||||||
|
self.note_lo().swap(x, Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
fn note_axis (&self) -> &AtomicUsize;
|
||||||
|
fn get_note_axis (&self) -> usize {
|
||||||
|
self.note_axis().load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
fn get_note_hi (&self) -> usize {
|
||||||
|
(self.note_lo().get() + self.note_axis().get().saturating_sub(1)).min(127)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait MidiRange: TimeRange + NoteRange {}
|
||||||
|
|
||||||
|
/// A unit of time, represented as an atomic 64-bit float.
|
||||||
|
///
|
||||||
|
/// According to https://stackoverflow.com/a/873367, as per IEEE754,
|
||||||
|
/// every integer between 1 and 2^53 can be represented exactly.
|
||||||
|
/// This should mean that, even at 192kHz sampling rate, over 1 year of audio
|
||||||
|
/// can be clocked in microseconds with f64 without losing precision.
|
||||||
|
pub trait TimeUnit: InteriorMutable<f64> {}
|
||||||
131
engine/jack_impls.rs
Normal file
131
engine/jack_impls.rs
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
use crate::*;
|
||||||
|
use JackState::*;
|
||||||
|
|
||||||
|
/// Implement [Jack] constructor and methods
|
||||||
|
impl<'j> Jack<'j> {
|
||||||
|
|
||||||
|
/// Register new [Client] and wrap it for shared use.
|
||||||
|
pub fn new_run <T: HasJack<'j> + Audio + Send + Sync + 'static> (
|
||||||
|
name: &impl AsRef<str>,
|
||||||
|
init: impl FnOnce(Jack<'j>)->Usually<T>
|
||||||
|
) -> Usually<Arc<RwLock<T>>> {
|
||||||
|
Jack::new(name)?.run(init)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new (name: &impl AsRef<str>) -> Usually<Self> {
|
||||||
|
let client = Client::new(name.as_ref(), ClientOptions::NO_START_SERVER)?.0;
|
||||||
|
Ok(Jack(Arc::new(RwLock::new(JackState::Inactive(client)))))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run <T: HasJack<'j> + Audio + Send + Sync + 'static>
|
||||||
|
(self, init: impl FnOnce(Self)->Usually<T>) -> Usually<Arc<RwLock<T>>>
|
||||||
|
{
|
||||||
|
let client_state = self.0.clone();
|
||||||
|
let app: Arc<RwLock<T>> = Arc::new(RwLock::new(init(self)?));
|
||||||
|
let mut state = Activating;
|
||||||
|
std::mem::swap(&mut*client_state.write().unwrap(), &mut state);
|
||||||
|
if let Inactive(client) = state {
|
||||||
|
// This is the misc notifications handler. It's a struct that wraps a [Box]
|
||||||
|
// which performs type erasure on a callback that takes [JackEvent], which is
|
||||||
|
// one of the available misc notifications.
|
||||||
|
let notify = JackNotify(Box::new({
|
||||||
|
let app = app.clone();
|
||||||
|
move|event|(&mut*app.write().unwrap()).handle(event)
|
||||||
|
}) as BoxedJackEventHandler);
|
||||||
|
// This is the main processing handler. It's a struct that wraps a [Box]
|
||||||
|
// which performs type erasure on a callback that takes [Client] and [ProcessScope]
|
||||||
|
// and passes them down to the `app`'s `process` callback, which in turn
|
||||||
|
// implements audio and MIDI input and output on a realtime basis.
|
||||||
|
let process = ClosureProcessHandler::new(Box::new({
|
||||||
|
let app = app.clone();
|
||||||
|
move|c: &_, s: &_|if let Ok(mut app) = app.write() {
|
||||||
|
app.process(c, s)
|
||||||
|
} else {
|
||||||
|
Control::Quit
|
||||||
|
}
|
||||||
|
}) as BoxedAudioHandler);
|
||||||
|
// Launch a client with the two handlers.
|
||||||
|
*client_state.write().unwrap() = Active(
|
||||||
|
client.activate_async(notify, process)?
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
unreachable!();
|
||||||
|
}
|
||||||
|
Ok(app)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run something with the client.
|
||||||
|
pub fn with_client <T> (&self, op: impl FnOnce(&Client)->T) -> T {
|
||||||
|
match &*self.0.read().unwrap() {
|
||||||
|
Inert => panic!("jack client not activated"),
|
||||||
|
Inactive(client) => op(client),
|
||||||
|
Activating => panic!("jack client has not finished activation"),
|
||||||
|
Active(client) => op(client.as_client()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'j> HasJack<'j> for Jack<'j> {
|
||||||
|
fn jack (&self) -> &Jack<'j> {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'j> HasJack<'j> for &Jack<'j> {
|
||||||
|
fn jack (&self) -> &Jack<'j> {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Fn(JackEvent) + Send> NotificationHandler for JackNotify<T> {
|
||||||
|
fn thread_init(&self, _: &Client) {
|
||||||
|
self.0(JackEvent::ThreadInit);
|
||||||
|
}
|
||||||
|
unsafe fn shutdown(&mut self, status: ClientStatus, reason: &str) {
|
||||||
|
self.0(JackEvent::Shutdown(status, reason.into()));
|
||||||
|
}
|
||||||
|
fn freewheel(&mut self, _: &Client, enabled: bool) {
|
||||||
|
self.0(JackEvent::Freewheel(enabled));
|
||||||
|
}
|
||||||
|
fn sample_rate(&mut self, _: &Client, frames: Frames) -> Control {
|
||||||
|
self.0(JackEvent::SampleRate(frames));
|
||||||
|
Control::Quit
|
||||||
|
}
|
||||||
|
fn client_registration(&mut self, _: &Client, name: &str, reg: bool) {
|
||||||
|
self.0(JackEvent::ClientRegistration(name.into(), reg));
|
||||||
|
}
|
||||||
|
fn port_registration(&mut self, _: &Client, id: PortId, reg: bool) {
|
||||||
|
self.0(JackEvent::PortRegistration(id, reg));
|
||||||
|
}
|
||||||
|
fn port_rename(&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control {
|
||||||
|
self.0(JackEvent::PortRename(id, old.into(), new.into()));
|
||||||
|
Control::Continue
|
||||||
|
}
|
||||||
|
fn ports_connected(&mut self, _: &Client, a: PortId, b: PortId, are: bool) {
|
||||||
|
self.0(JackEvent::PortsConnected(a, b, are));
|
||||||
|
}
|
||||||
|
fn graph_reorder(&mut self, _: &Client) -> Control {
|
||||||
|
self.0(JackEvent::GraphReorder);
|
||||||
|
Control::Continue
|
||||||
|
}
|
||||||
|
fn xrun(&mut self, _: &Client) -> Control {
|
||||||
|
self.0(JackEvent::XRun);
|
||||||
|
Control::Continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JackPerfModel for PerfModel {
|
||||||
|
fn update_from_jack_scope (&self, t0: Option<u64>, scope: &ProcessScope) {
|
||||||
|
if let Some(t0) = t0 {
|
||||||
|
let t1 = self.clock.raw();
|
||||||
|
self.used.store(
|
||||||
|
self.clock.delta_as_nanos(t0, t1) as f64,
|
||||||
|
Relaxed,
|
||||||
|
);
|
||||||
|
self.window.store(
|
||||||
|
scope.cycle_times().unwrap().period_usecs as f64,
|
||||||
|
Relaxed,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
53
engine/jack_structs.rs
Normal file
53
engine/jack_structs.rs
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// Wraps [JackState], and through it [jack::Client] when connected.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let jack: tek_engine::Jack = Default::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct Jack<'j> (pub(crate) Arc<RwLock<JackState<'j>>>);
|
||||||
|
|
||||||
|
/// This is a connection which may be [Inactive], [Activating], or [Active].
|
||||||
|
/// In the [Active] and [Inactive] states, [JackState::client] returns a
|
||||||
|
/// [jack::Client], which you can use to talk to the JACK API.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let state: tek_engine::JackState = Default::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub enum JackState<'j> {
|
||||||
|
/// Unused
|
||||||
|
#[default] Inert,
|
||||||
|
/// Before activation.
|
||||||
|
Inactive(Client),
|
||||||
|
/// During activation.
|
||||||
|
Activating,
|
||||||
|
/// After activation. Must not be dropped for JACK thread to persist.
|
||||||
|
Active(DynamicAsyncClient<'j>),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Event enum for JACK events.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let event: tek_engine::JackState = JackEvent::XRun;
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, PartialEq)] pub enum JackEvent {
|
||||||
|
ThreadInit,
|
||||||
|
Shutdown(ClientStatus, Arc<str>),
|
||||||
|
Freewheel(bool),
|
||||||
|
SampleRate(Frames),
|
||||||
|
ClientRegistration(Arc<str>, bool),
|
||||||
|
PortRegistration(PortId, bool),
|
||||||
|
PortRename(PortId, Arc<str>, Arc<str>),
|
||||||
|
PortsConnected(PortId, PortId, bool),
|
||||||
|
GraphReorder,
|
||||||
|
XRun,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generic notification handler that emits [JackEvent]
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let notify = tek_engine::JackNotify(|_: JackEvent|{});
|
||||||
|
/// ```
|
||||||
|
pub struct JackNotify<T: Fn(JackEvent) + Send>(pub T);
|
||||||
104
engine/jack_traits.rs
Normal file
104
engine/jack_traits.rs
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// Things that can provide a [jack::Client] reference.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// #[derive(Default)] struct Jacked<'j>(tek_engine::Jack<'j>);
|
||||||
|
///
|
||||||
|
/// impl tek_engine::HasJack<'j> for Jacked<'j> {
|
||||||
|
/// fn jack (&self) -> &Jack<'j> { &self.0 }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// let jack: &Jack<'j> = Jacked::default().jack();
|
||||||
|
/// ```
|
||||||
|
pub trait HasJack<'j>: Send + Sync {
|
||||||
|
|
||||||
|
/// Return the internal [jack::Client] handle
|
||||||
|
/// that lets you call the JACK API.
|
||||||
|
fn jack (&self) -> &Jack<'j>;
|
||||||
|
|
||||||
|
fn with_client <T> (&self, op: impl FnOnce(&Client)->T) -> T {
|
||||||
|
self.jack().with_client(op)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn port_by_name (&self, name: &str) -> Option<Port<Unowned>> {
|
||||||
|
self.with_client(|client|client.port_by_name(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn port_by_id (&self, id: u32) -> Option<Port<Unowned>> {
|
||||||
|
self.with_client(|c|c.port_by_id(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_port <PS: PortSpec + Default> (&self, name: impl AsRef<str>) -> Usually<Port<PS>> {
|
||||||
|
self.with_client(|client|Ok(client.register_port(name.as_ref(), PS::default())?))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sync_lead (&self, enable: bool, callback: impl Fn(TimebaseInfo)->Position) -> Usually<()> {
|
||||||
|
if enable {
|
||||||
|
self.with_client(|client|match client.register_timebase_callback(false, callback) {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(e) => Err(e)
|
||||||
|
})?
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sync_follow (&self, _enable: bool) -> Usually<()> {
|
||||||
|
// TODO: sync follow
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait for thing that has a JACK process callback.
|
||||||
|
pub trait Audio {
|
||||||
|
|
||||||
|
/// Handle a JACK event.
|
||||||
|
fn handle (&mut self, _event: JackEvent) {}
|
||||||
|
|
||||||
|
/// Projecss a JACK chunk.
|
||||||
|
fn process (&mut self, _: &Client, _: &ProcessScope) -> Control {
|
||||||
|
Control::Continue
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The JACK process callback function passed to the server.
|
||||||
|
fn callback (
|
||||||
|
state: &Arc<RwLock<Self>>, client: &Client, scope: &ProcessScope
|
||||||
|
) -> Control where Self: Sized {
|
||||||
|
if let Ok(mut state) = state.write() {
|
||||||
|
state.process(client, scope)
|
||||||
|
} else {
|
||||||
|
Control::Quit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implement [Audio]: provide JACK callbacks.
|
||||||
|
#[macro_export] macro_rules! audio {
|
||||||
|
|
||||||
|
(|
|
||||||
|
$self1:ident:
|
||||||
|
$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?,$c:ident,$s:ident
|
||||||
|
|$cb:expr$(;|$self2:ident,$e:ident|$cb2:expr)?) => {
|
||||||
|
impl $(<$($L),*$($T $(: $U)?),*>)? Audio for $Struct $(<$($L),*$($T),*>)? {
|
||||||
|
#[inline] fn process (&mut $self1, $c: &Client, $s: &ProcessScope) -> Control { $cb }
|
||||||
|
$(#[inline] fn handle (&mut $self2, $e: JackEvent) { $cb2 })?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
($Struct:ident: $process:ident, $handle:ident) => {
|
||||||
|
impl Audio for $Struct {
|
||||||
|
#[inline] fn process (&mut self, c: &Client, s: &ProcessScope) -> Control {
|
||||||
|
$process(self, c, s)
|
||||||
|
}
|
||||||
|
#[inline] fn handle (&mut self, e: JackEvent) {
|
||||||
|
$handle(self, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait JackPerfModel {
|
||||||
|
fn update_from_jack_scope (&self, t0: Option<u64>, scope: &ProcessScope);
|
||||||
|
}
|
||||||
|
|
@ -25,27 +25,3 @@ pub type DynamicNotifications<'j> =
|
||||||
/// Boxed [JackEvent] callback.
|
/// Boxed [JackEvent] callback.
|
||||||
pub type BoxedJackEventHandler<'j> =
|
pub type BoxedJackEventHandler<'j> =
|
||||||
Box<dyn Fn(JackEvent) + Send + Sync + 'j>;
|
Box<dyn Fn(JackEvent) + Send + Sync + 'j>;
|
||||||
|
|
||||||
pub type MidiData =
|
|
||||||
Vec<Vec<MidiMessage>>;
|
|
||||||
|
|
||||||
pub type ClipPool =
|
|
||||||
Vec<Arc<RwLock<MidiClip>>>;
|
|
||||||
|
|
||||||
pub type CollectedMidiInput<'a> =
|
|
||||||
Vec<Vec<(u32, Result<LiveEvent<'a>, MidiError>)>>;
|
|
||||||
|
|
||||||
pub type SceneWith<'a, T> =
|
|
||||||
(usize, &'a Scene, usize, usize, T);
|
|
||||||
|
|
||||||
pub type MidiSample =
|
|
||||||
(Option<u7>, Arc<RwLock<crate::Sample>>);
|
|
||||||
|
|
||||||
/// Collection of interaction modes.
|
|
||||||
pub type Modes = Arc<RwLock<BTreeMap<Arc<str>, Arc<Mode<Arc<str>>>>>>;
|
|
||||||
|
|
||||||
/// Collection of input bindings.
|
|
||||||
pub type Binds = Arc<RwLock<BTreeMap<Arc<str>, Bind<TuiEvent, Arc<str>>>>>;
|
|
||||||
|
|
||||||
/// Collection of view definitions.
|
|
||||||
pub type Views = Arc<RwLock<BTreeMap<Arc<str>, Arc<str>>>>;
|
|
||||||
2
tengri
2
tengri
|
|
@ -1 +1 @@
|
||||||
Subproject commit c1011ddb7f8061c5fe242b6eab62a66537ae671b
|
Subproject commit b4ebdb8ff80941f45647b628ac2d7b21969f3241
|
||||||
Loading…
Add table
Add a link
Reference in a new issue