mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-04-03 21:00:44 +02:00
wip: nomralize
This commit is contained in:
parent
35197fb826
commit
244e2b388e
16 changed files with 1880 additions and 1866 deletions
|
|
@ -1,7 +1,9 @@
|
||||||
|
use crate::*;
|
||||||
use ::std::sync::{Arc, RwLock};
|
use ::std::sync::{Arc, RwLock};
|
||||||
use ::tengri::{space::east, color::ItemTheme};
|
use ::tengri::{space::east, color::ItemTheme};
|
||||||
use ::tengri::{draw::*, term::*};
|
use ::tengri::{draw::*, term::*};
|
||||||
use crate::device::{MidiInput, MidiOutput, AudioInput, AudioOutput};
|
use crate::device::{MidiInput, MidiOutput, AudioInput, AudioOutput};
|
||||||
|
impl HasJack<'static> for Arrangement { fn jack (&self) -> &Jack<'static> { &self.jack } }
|
||||||
|
|
||||||
/// Arranger.
|
/// Arranger.
|
||||||
///
|
///
|
||||||
|
|
@ -48,6 +50,27 @@ use crate::device::{MidiInput, MidiOutput, AudioInput, AudioOutput};
|
||||||
#[cfg(feature = "scene")] pub scene_scroll: usize,
|
#[cfg(feature = "scene")] pub scene_scroll: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl_has!(Jack<'static>: |self: Arrangement| self.jack);
|
||||||
|
impl_has!(Measure<Tui>: |self: Arrangement| self.size);
|
||||||
|
impl_has!(Vec<Track>: |self: Arrangement| self.tracks);
|
||||||
|
impl_has!(Vec<Scene>: |self: Arrangement| self.scenes);
|
||||||
|
impl_has!(Vec<MidiInput>: |self: Arrangement| self.midi_ins);
|
||||||
|
impl_has!(Vec<MidiOutput>: |self: Arrangement| self.midi_outs);
|
||||||
|
impl_has!(Clock: |self: Arrangement| self.clock);
|
||||||
|
impl_has!(Selection: |self: Arrangement| self.selection);
|
||||||
|
impl_as_ref_opt!(MidiEditor: |self: Arrangement| self.editor.as_ref());
|
||||||
|
impl_as_mut_opt!(MidiEditor: |self: Arrangement| self.editor.as_mut());
|
||||||
|
impl_as_ref_opt!(Track: |self: Arrangement| self.selected_track());
|
||||||
|
impl_as_mut_opt!(Track: |self: Arrangement| self.selected_track_mut());
|
||||||
|
|
||||||
|
impl <T: AsRef<Selection>+AsMut<Selection>> HasSelection for T {}
|
||||||
|
impl <T: AsRef<Vec<Scene>>+AsMut<Vec<Scene>>> HasScenes for T {}
|
||||||
|
impl <T: AsRef<Vec<Track>>+AsMut<Vec<Track>>> HasTracks for T {}
|
||||||
|
impl <T: AsRefOpt<Scene>+AsMutOpt<Scene>+Send+Sync> HasScene for T {}
|
||||||
|
impl <T: AsRefOpt<Track>+AsMutOpt<Track>+Send+Sync> HasTrack for T {}
|
||||||
|
impl <T: ScenesView+HasMidiIns+HasMidiOuts+HasTrackScroll+Measured<Tui>> TracksView for T {}
|
||||||
|
impl <T: TracksView+ScenesView+Send+Sync> ClipsView for T {}
|
||||||
|
|
||||||
pub trait ClipsView: TracksView + ScenesView {
|
pub trait ClipsView: TracksView + ScenesView {
|
||||||
|
|
||||||
fn view_scenes_clips <'a> (&'a self)
|
fn view_scenes_clips <'a> (&'a self)
|
||||||
|
|
@ -674,18 +697,6 @@ impl Selection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl_has!(Jack<'static>: |self: Arrangement| self.jack);
|
|
||||||
impl_has!(Measure<Tui>: |self: Arrangement| self.size);
|
|
||||||
impl_has!(Vec<Track>: |self: Arrangement| self.tracks);
|
|
||||||
impl_has!(Vec<Scene>: |self: Arrangement| self.scenes);
|
|
||||||
impl_has!(Vec<MidiInput>: |self: Arrangement| self.midi_ins);
|
|
||||||
impl_has!(Vec<MidiOutput>: |self: Arrangement| self.midi_outs);
|
|
||||||
impl_has!(Clock: |self: Arrangement| self.clock);
|
|
||||||
impl_has!(Selection: |self: Arrangement| self.selection);
|
|
||||||
impl_as_ref_opt!(MidiEditor: |self: Arrangement| self.editor.as_ref());
|
|
||||||
impl_as_mut_opt!(MidiEditor: |self: Arrangement| self.editor.as_mut());
|
|
||||||
impl_as_ref_opt!(Track: |self: Arrangement| self.selected_track());
|
|
||||||
impl_as_mut_opt!(Track: |self: Arrangement| self.selected_track_mut());
|
|
||||||
impl Arrangement {
|
impl Arrangement {
|
||||||
/// Create a new arrangement.
|
/// Create a new arrangement.
|
||||||
pub fn new (
|
pub fn new (
|
||||||
|
|
@ -918,3 +929,45 @@ impl Selection {
|
||||||
impl HasClipsSize for Arrangement {
|
impl HasClipsSize for Arrangement {
|
||||||
fn clips_size (&self) -> &Measure<Tui> { &self.size_inner }
|
fn clips_size (&self) -> &Measure<Tui> { &self.size_inner }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub type SceneWith<'a, T> =
|
||||||
|
(usize, &'a Scene, usize, usize, T);
|
||||||
|
|
||||||
|
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_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 }),
|
||||||
|
});
|
||||||
|
|
||||||
|
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!()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
||||||
130
src/bind.rs
Normal file
130
src/bind.rs
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
use crate::*;
|
||||||
|
/// A control axis.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let axis = tek::ControlAxis::X;
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Copy, Clone)] pub enum ControlAxis {
|
||||||
|
X, Y, Z, I
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collection of input bindings.
|
||||||
|
pub type Binds = Arc<RwLock<BTreeMap<Arc<str>, Bind<TuiEvent, Arc<str>>>>>;
|
||||||
|
|
||||||
|
pub(crate) fn load_bind (binds: &Binds, name: &impl AsRef<str>, body: &impl Language) -> Usually<()> {
|
||||||
|
binds.write().unwrap().insert(name.as_ref().into(), Bind::load(body)?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An map of input events (e.g. [TuiEvent]) to [Binding]s.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let lang = "(@x (nop)) (@y (nop) (nop))";
|
||||||
|
/// let bind = tek::Bind::<tek::tengri::TuiEvent, std::sync::Arc<str>>::load(&lang).unwrap();
|
||||||
|
/// assert_eq!(bind.query(&'x'.into()).map(|x|x.len()), Some(1));
|
||||||
|
/// //assert_eq!(bind.query(&'y'.into()).map(|x|x.len()), Some(2));
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug)] pub struct Bind<E, C>(
|
||||||
|
/// Map of each event (e.g. key combination) to
|
||||||
|
/// all command expressions bound to it by
|
||||||
|
/// all loaded input layers.
|
||||||
|
pub BTreeMap<E, Vec<Binding<C>>>
|
||||||
|
);
|
||||||
|
|
||||||
|
/// A sequence of zero or more commands (e.g. [AppCommand]),
|
||||||
|
/// optionally filtered by [Condition] to form layers.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// //FIXME: Why does it overflow?
|
||||||
|
/// //let binding: Binding<()> = tek::Binding { ..Default::default() };
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone)] pub struct Binding<C> {
|
||||||
|
pub commands: Arc<[C]>,
|
||||||
|
pub condition: Option<Condition>,
|
||||||
|
pub description: Option<Arc<str>>,
|
||||||
|
pub source: Option<Arc<PathBuf>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Condition that must evaluate to true in order to enable an input layer.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let condition = tek::Condition(std::sync::Arc::new(Box::new(||{true})));
|
||||||
|
/// ```
|
||||||
|
#[derive(Clone)] pub struct Condition(
|
||||||
|
pub Arc<Box<dyn Fn()->bool + Send + Sync>>
|
||||||
|
);
|
||||||
|
|
||||||
|
impl Bind<TuiEvent, Arc<str>> {
|
||||||
|
pub fn load (lang: &impl Language) -> Usually<Self> {
|
||||||
|
let mut map = Bind::new();
|
||||||
|
lang.each(|item|if item.expr().head() == Ok(Some("see")) {
|
||||||
|
// TODO
|
||||||
|
Ok(())
|
||||||
|
} else if let Ok(Some(_word)) = item.expr().head().word() {
|
||||||
|
if let Some(key) = TuiEvent::from_dsl(item.expr()?.head()?)? {
|
||||||
|
map.add(key, Binding {
|
||||||
|
commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(),
|
||||||
|
condition: None,
|
||||||
|
description: None,
|
||||||
|
source: None
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
} else if Some(":char") == item.expr()?.head()? {
|
||||||
|
// TODO
|
||||||
|
return Ok(())
|
||||||
|
} else {
|
||||||
|
return Err(format!("Config::load_bind: invalid key: {:?}", item.expr()?.head()?).into())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(format!("Config::load_bind: unexpected: {item:?}").into())
|
||||||
|
})?;
|
||||||
|
Ok(map)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default is always empty map regardless if `E` and `C` implement [Default].
|
||||||
|
impl<E, C> Default for Bind<E, C> {
|
||||||
|
fn default () -> Self { Self(Default::default()) }
|
||||||
|
}
|
||||||
|
impl<C: Default> Default for Binding<C> {
|
||||||
|
fn default () -> Self {
|
||||||
|
Self {
|
||||||
|
commands: Default::default(),
|
||||||
|
condition: Default::default(),
|
||||||
|
description: Default::default(),
|
||||||
|
source: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: Clone + Ord, C> Bind<E, C> {
|
||||||
|
/// Create a new event map
|
||||||
|
pub fn new () -> Self {
|
||||||
|
Default::default()
|
||||||
|
}
|
||||||
|
/// Add a binding to an owned event map.
|
||||||
|
pub fn def (mut self, event: E, binding: Binding<C>) -> Self {
|
||||||
|
self.add(event, binding);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
/// Add a binding to an event map.
|
||||||
|
pub fn add (&mut self, event: E, binding: Binding<C>) -> &mut Self {
|
||||||
|
if !self.0.contains_key(&event) {
|
||||||
|
self.0.insert(event.clone(), Default::default());
|
||||||
|
}
|
||||||
|
self.0.get_mut(&event).unwrap().push(binding);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
/// Return the binding(s) that correspond to an event.
|
||||||
|
pub fn query (&self, event: &E) -> Option<&[Binding<C>]> {
|
||||||
|
self.0.get(event).map(|x|x.as_slice())
|
||||||
|
}
|
||||||
|
/// Return the first binding that corresponds to an event, considering conditions.
|
||||||
|
pub fn dispatch (&self, event: &E) -> Option<&Binding<C>> {
|
||||||
|
self.query(event)
|
||||||
|
.map(|bb|bb.iter().filter(|b|b.condition.as_ref().map(|c|(c.0)()).unwrap_or(true)).next())
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_debug!(Condition |self, w| { write!(w, "*") });
|
||||||
181
src/browse.rs
181
src/browse.rs
|
|
@ -1,7 +1,17 @@
|
||||||
|
use crate::*;
|
||||||
use ::std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::*}};
|
use ::std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::*}};
|
||||||
use crate::sequence::MidiClip;
|
use crate::sequence::MidiClip;
|
||||||
use crate::sample::Sample;
|
use crate::sample::Sample;
|
||||||
|
|
||||||
|
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")))))
|
||||||
|
});
|
||||||
|
|
||||||
/// Browses for files to load/save.
|
/// Browses for files to load/save.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
|
|
@ -276,6 +286,7 @@ impl ClipLength {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Browse {
|
impl Browse {
|
||||||
pub fn new (cwd: Option<PathBuf>) -> Usually<Self> {
|
pub fn new (cwd: Option<PathBuf>) -> Usually<Self> {
|
||||||
let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? };
|
let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? };
|
||||||
|
|
@ -349,3 +360,173 @@ impl ClipLength {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def_command!(BrowseCommand: |browse: Browse| {
|
||||||
|
SetVisible => Ok(None),
|
||||||
|
SetPath { address: PathBuf } => Ok(None),
|
||||||
|
SetSearch { filter: Arc<str> } => Ok(None),
|
||||||
|
SetCursor { cursor: usize } => Ok(None),
|
||||||
|
});
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
||||||
254
src/cli.rs
Normal file
254
src/cli.rs
Normal file
|
|
@ -0,0 +1,254 @@
|
||||||
|
use crate::*;
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// The command-line interface descriptor.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let cli: tek::Cli = Default::default();
|
||||||
|
///
|
||||||
|
/// use clap::CommandFactory;
|
||||||
|
/// tek::Cli::command().debug_assert();
|
||||||
|
/// ```
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "tek", version, about = Some(HEADER), long_about = Some(HEADER))]
|
||||||
|
#[derive(Debug, Default)] pub struct Cli {
|
||||||
|
/// Pre-defined configuration modes.
|
||||||
|
///
|
||||||
|
/// TODO: Replace these with scripted configurations.
|
||||||
|
#[command(subcommand)] pub action: Action,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Application modes that can be passed to the mommand line interface.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let action: tek::Action = Default::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Subcommand, Default)] pub enum Action {
|
||||||
|
/// Continue where you left off
|
||||||
|
#[default] Resume,
|
||||||
|
/// Run headlessly in current session.
|
||||||
|
Headless,
|
||||||
|
/// Show status of current session.
|
||||||
|
Status,
|
||||||
|
/// List known sessions.
|
||||||
|
List,
|
||||||
|
/// Continue work in a copy of the current session.
|
||||||
|
Fork,
|
||||||
|
/// Create a new empty session.
|
||||||
|
New {
|
||||||
|
/// Name of JACK client
|
||||||
|
#[arg(short='n', long)] name: Option<String>,
|
||||||
|
/// Whether to attempt to become transport master
|
||||||
|
#[arg(short='Y', long, default_value_t = false)] sync_lead: bool,
|
||||||
|
/// Whether to sync to external transport master
|
||||||
|
#[arg(short='y', long, default_value_t = true)] sync_follow: bool,
|
||||||
|
/// Initial tempo in beats per minute
|
||||||
|
#[arg(short='b', long, default_value = None)] bpm: Option<f64>,
|
||||||
|
/// Whether to include a transport toolbar (default: true)
|
||||||
|
#[arg(short='c', long, default_value_t = true)] show_clock: bool,
|
||||||
|
/// MIDI outs to connect to (multiple instances accepted)
|
||||||
|
#[arg(short='I', long)] midi_from: Vec<String>,
|
||||||
|
/// MIDI outs to connect to (multiple instances accepted)
|
||||||
|
#[arg(short='i', long)] midi_from_re: Vec<String>,
|
||||||
|
/// MIDI ins to connect to (multiple instances accepted)
|
||||||
|
#[arg(short='O', long)] midi_to: Vec<String>,
|
||||||
|
/// MIDI ins to connect to (multiple instances accepted)
|
||||||
|
#[arg(short='o', long)] midi_to_re: Vec<String>,
|
||||||
|
/// Audio outs to connect to left input
|
||||||
|
#[arg(short='l', long)] left_from: Vec<String>,
|
||||||
|
/// Audio outs to connect to right input
|
||||||
|
#[arg(short='r', long)] right_from: Vec<String>,
|
||||||
|
/// Audio ins to connect from left output
|
||||||
|
#[arg(short='L', long)] left_to: Vec<String>,
|
||||||
|
/// Audio ins to connect from right output
|
||||||
|
#[arg(short='R', long)] right_to: Vec<String>,
|
||||||
|
/// Tracks to create
|
||||||
|
#[arg(short='t', long)] tracks: Option<usize>,
|
||||||
|
/// Scenes to create
|
||||||
|
#[arg(short='s', long)] scenes: Option<usize>,
|
||||||
|
},
|
||||||
|
/// Import media as new session.
|
||||||
|
Import,
|
||||||
|
/// Show configuration.
|
||||||
|
Config,
|
||||||
|
/// Show version.
|
||||||
|
Version,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Command-line configuration.
|
||||||
|
#[cfg(feature = "cli")] impl Cli {
|
||||||
|
pub fn run (&self) -> Usually<()> {
|
||||||
|
if let Action::Version = self.action {
|
||||||
|
return Ok(tek_show_version())
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut config = Config::new(None);
|
||||||
|
config.init()?;
|
||||||
|
|
||||||
|
if let Action::Config = self.action {
|
||||||
|
tek_print_config(&config);
|
||||||
|
} else if let Action::List = self.action {
|
||||||
|
todo!("list sessions")
|
||||||
|
} else if let Action::Resume = self.action {
|
||||||
|
todo!("resume session")
|
||||||
|
} else if let Action::New {
|
||||||
|
name, bpm, tracks, scenes, sync_lead, sync_follow,
|
||||||
|
midi_from, midi_from_re, midi_to, midi_to_re,
|
||||||
|
left_from, right_from, left_to, right_to, ..
|
||||||
|
} = &self.action {
|
||||||
|
|
||||||
|
// Connect to JACK
|
||||||
|
let name = name.as_ref().map_or("tek", |x|x.as_str());
|
||||||
|
let jack = Jack::new(&name)?;
|
||||||
|
|
||||||
|
// TODO: Collect audio IO:
|
||||||
|
let empty = &[] as &[&str];
|
||||||
|
let left_froms = Connect::collect(&left_from, empty, empty);
|
||||||
|
let left_tos = Connect::collect(&left_to, empty, empty);
|
||||||
|
let right_froms = Connect::collect(&right_from, empty, empty);
|
||||||
|
let right_tos = Connect::collect(&right_to, empty, empty);
|
||||||
|
let _audio_froms = &[left_froms.as_slice(), right_froms.as_slice()];
|
||||||
|
let _audio_tos = &[left_tos.as_slice(), right_tos.as_slice()];
|
||||||
|
|
||||||
|
// Create initial project:
|
||||||
|
let clock = Clock::new(&jack, *bpm)?;
|
||||||
|
let mut project = Arrangement::new(
|
||||||
|
&jack,
|
||||||
|
None,
|
||||||
|
clock,
|
||||||
|
vec![],
|
||||||
|
vec![],
|
||||||
|
Connect::collect(&midi_from, &[] as &[&str], &midi_from_re).iter().enumerate()
|
||||||
|
.map(|(index, connect)|jack.midi_in(&format!("M/{index}"), &[connect.clone()]))
|
||||||
|
.collect::<Result<Vec<_>, _>>()?,
|
||||||
|
Connect::collect(&midi_to, &[] as &[&str], &midi_to_re).iter().enumerate()
|
||||||
|
.map(|(index, connect)|jack.midi_out(&format!("{index}/M"), &[connect.clone()]))
|
||||||
|
.collect::<Result<Vec<_>, _>>()?
|
||||||
|
);
|
||||||
|
project.tracks_add(tracks.unwrap_or(0), None, &[], &[])?;
|
||||||
|
project.scenes_add(scenes.unwrap_or(0))?;
|
||||||
|
|
||||||
|
if matches!(self.action, Action::Status) {
|
||||||
|
// Show status and exit
|
||||||
|
tek_print_status(&project);
|
||||||
|
return Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the app state
|
||||||
|
let app = tek(&jack, project, config, ":menu");
|
||||||
|
if matches!(self.action, Action::Headless) {
|
||||||
|
// TODO: Headless mode (daemon + client over IPC, then over network...)
|
||||||
|
println!("todo headless");
|
||||||
|
return Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the [Tui] and [Jack] threads with the [App] state.
|
||||||
|
Tui::new(Box::new(std::io::stdout()))?.run(true, &jack.run(move|jack|{
|
||||||
|
|
||||||
|
// Between jack init and app's first cycle:
|
||||||
|
|
||||||
|
jack.sync_lead(*sync_lead, |mut state|{
|
||||||
|
let clock = app.clock();
|
||||||
|
clock.playhead.update_from_sample(state.position.frame() as f64);
|
||||||
|
state.position.bbt = Some(clock.bbt());
|
||||||
|
state.position
|
||||||
|
})?;
|
||||||
|
|
||||||
|
jack.sync_follow(*sync_follow)?;
|
||||||
|
|
||||||
|
// FIXME: They don't work properly.
|
||||||
|
|
||||||
|
Ok(app)
|
||||||
|
|
||||||
|
})?)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tek_show_version () {
|
||||||
|
println!("todo version");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tek_print_config (config: &Config) {
|
||||||
|
use ::ansi_term::Color::*;
|
||||||
|
println!("{:?}", config.dirs);
|
||||||
|
for (k, v) in config.views.read().unwrap().iter() {
|
||||||
|
println!("{} {} {v}", Green.paint("VIEW"), Green.bold().paint(format!("{k:<16}")));
|
||||||
|
}
|
||||||
|
for (k, v) in config.binds.read().unwrap().iter() {
|
||||||
|
println!("{} {}", Green.paint("BIND"), Green.bold().paint(format!("{k:<16}")));
|
||||||
|
for (k, v) in v.0.iter() {
|
||||||
|
print!("{} ", &Yellow.paint(match &k.0 {
|
||||||
|
Event::Key(KeyEvent { modifiers, .. }) =>
|
||||||
|
format!("{:>16}", format!("{modifiers}")),
|
||||||
|
_ => unimplemented!()
|
||||||
|
}));
|
||||||
|
print!("{}", &Yellow.bold().paint(match &k.0 {
|
||||||
|
Event::Key(KeyEvent { code, .. }) =>
|
||||||
|
format!("{:<10}", format!("{code}")),
|
||||||
|
_ => unimplemented!()
|
||||||
|
}));
|
||||||
|
for v in v.iter() {
|
||||||
|
print!(" => {:?}", v.commands);
|
||||||
|
print!(" {}", v.condition.as_ref().map(|x|format!("{x:?}")).unwrap_or_default());
|
||||||
|
println!(" {}", v.description.as_ref().map(|x|x.as_ref()).unwrap_or_default());
|
||||||
|
//println!(" {:?}", v.source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (k, v) in config.modes.read().unwrap().iter() {
|
||||||
|
println!();
|
||||||
|
for v in v.name.iter() { print!("{}", Green.bold().paint(format!("{v} "))); }
|
||||||
|
for v in v.info.iter() { print!("\n{}", Green.paint(format!("{v}"))); }
|
||||||
|
print!("\n{} {}", Blue.paint("TOOL"), Green.bold().paint(format!("{k:<16}")));
|
||||||
|
print!("\n{}", Blue.paint("KEYS"));
|
||||||
|
for v in v.keys.iter() { print!("{}", Green.paint(format!(" {v}"))); }
|
||||||
|
println!();
|
||||||
|
for (k, v) in v.modes.read().unwrap().iter() {
|
||||||
|
print!("{} {} {:?}",
|
||||||
|
Blue.paint("MODE"),
|
||||||
|
Green.bold().paint(format!("{k:<16}")),
|
||||||
|
v.name);
|
||||||
|
print!(" INFO={:?}",
|
||||||
|
v.info);
|
||||||
|
print!(" VIEW={:?}",
|
||||||
|
v.view);
|
||||||
|
println!(" KEYS={:?}",
|
||||||
|
v.keys);
|
||||||
|
}
|
||||||
|
print!("{}", Blue.paint("VIEW"));
|
||||||
|
for v in v.view.iter() { print!("{}", Green.paint(format!(" {v}"))); }
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tek_print_status (project: &Arrangement) {
|
||||||
|
println!("Name: {:?}", &project.name);
|
||||||
|
println!("JACK: {:?}", &project.jack);
|
||||||
|
println!("Buffer: {:?}", &project.clock.chunk);
|
||||||
|
println!("Sample rate: {:?}", &project.clock.timebase.sr);
|
||||||
|
println!("MIDI PPQ: {:?}", &project.clock.timebase.ppq);
|
||||||
|
println!("Tempo: {:?}", &project.clock.timebase.bpm);
|
||||||
|
println!("Quantize: {:?}", &project.clock.quant);
|
||||||
|
println!("Launch: {:?}", &project.clock.sync);
|
||||||
|
println!("Playhead: {:?}us", &project.clock.playhead.usec);
|
||||||
|
println!("Playhead: {:?}s", &project.clock.playhead.sample);
|
||||||
|
println!("Playhead: {:?}p", &project.clock.playhead.pulse);
|
||||||
|
println!("Started: {:?}", &project.clock.started);
|
||||||
|
println!("Tracks:");
|
||||||
|
for (i, t) in project.tracks.iter().enumerate() {
|
||||||
|
println!(" Track {i}: {} {} {:?} {:?}", t.name, t.width,
|
||||||
|
&t.sequencer.play_clip, &t.sequencer.next_clip);
|
||||||
|
}
|
||||||
|
println!("Scenes:");
|
||||||
|
for (i, t) in project.scenes.iter().enumerate() {
|
||||||
|
println!(" Scene {i}: {} {:?}", &t.name, &t.clips);
|
||||||
|
}
|
||||||
|
println!("MIDI Ins: {:?}", &project.midi_ins);
|
||||||
|
println!("MIDI Outs: {:?}", &project.midi_outs);
|
||||||
|
println!("Audio Ins: {:?}", &project.audio_ins);
|
||||||
|
println!("Audio Outs: {:?}", &project.audio_outs);
|
||||||
|
// TODO git integration
|
||||||
|
// TODO dawvert integration
|
||||||
|
}
|
||||||
20
src/clock.rs
20
src/clock.rs
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::*;
|
||||||
use ::std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::*}};
|
use ::std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::*}};
|
||||||
use ::atomic_float::AtomicF64;
|
use ::atomic_float::AtomicF64;
|
||||||
use ::tengri::{draw::*, term::*};
|
use ::tengri::{draw::*, term::*};
|
||||||
|
|
@ -452,14 +453,6 @@ impl Microsecond {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_time_unit!(SampleCount);
|
|
||||||
impl_time_unit!(SampleRate);
|
|
||||||
impl_time_unit!(Microsecond);
|
|
||||||
impl_time_unit!(Quantize);
|
|
||||||
impl_time_unit!(Ppq);
|
|
||||||
impl_time_unit!(Pulse);
|
|
||||||
impl_time_unit!(Bpm);
|
|
||||||
impl_time_unit!(LaunchSync);
|
|
||||||
impl std::fmt::Debug for Clock {
|
impl std::fmt::Debug for Clock {
|
||||||
fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||||
f.debug_struct("Clock")
|
f.debug_struct("Clock")
|
||||||
|
|
@ -700,3 +693,14 @@ impl_time_unit!(LaunchSync);
|
||||||
lat: Memo::new(None, String::with_capacity(16)),
|
lat: Memo::new(None, String::with_capacity(16)),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
#[cfg(feature = "clock")] impl_has!(Clock: |self: Track|self.sequencer.clock);
|
||||||
|
impl_default!(Timebase: Self::new(48000f64, 150f64, DEFAULT_PPQ));
|
||||||
|
impl_time_unit!(SampleCount);
|
||||||
|
impl_time_unit!(SampleRate);
|
||||||
|
impl_time_unit!(Microsecond);
|
||||||
|
impl_time_unit!(Quantize);
|
||||||
|
impl_time_unit!(Ppq);
|
||||||
|
impl_time_unit!(Pulse);
|
||||||
|
impl_time_unit!(Bpm);
|
||||||
|
impl_time_unit!(LaunchSync);
|
||||||
|
|
|
||||||
92
src/config.rs
Normal file
92
src/config.rs
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// Configuration: mode, view, and bind definitions.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let config = tek::Config::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 {
|
||||||
|
/// XDG base directories of running user.
|
||||||
|
pub dirs: BaseDirectories,
|
||||||
|
/// Active collection of interaction modes.
|
||||||
|
pub modes: Modes,
|
||||||
|
/// Active collection of event bindings.
|
||||||
|
pub binds: Binds,
|
||||||
|
/// Active collection of view definitions.
|
||||||
|
pub views: Views,
|
||||||
|
}
|
||||||
|
impl Config {
|
||||||
|
const CONFIG_DIR: &'static str = "tek";
|
||||||
|
const CONFIG_SUB: &'static str = "v0";
|
||||||
|
const CONFIG: &'static str = "tek.edn";
|
||||||
|
const DEFAULTS: &'static str = include_str!("./tek.edn");
|
||||||
|
/// Create a new app configuration from a set of XDG base directories,
|
||||||
|
pub fn new (dirs: Option<BaseDirectories>) -> Self {
|
||||||
|
let default = ||BaseDirectories::with_profile(Self::CONFIG_DIR, Self::CONFIG_SUB);
|
||||||
|
let dirs = dirs.unwrap_or_else(default);
|
||||||
|
Self { dirs, ..Default::default() }
|
||||||
|
}
|
||||||
|
/// Write initial contents of configuration.
|
||||||
|
pub fn init (&mut self) -> Usually<()> {
|
||||||
|
self.init_one(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|{
|
||||||
|
cfgs.add(&dsl)?;
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
/// Write initial contents of a configuration file.
|
||||||
|
pub fn init_one (
|
||||||
|
&mut self, path: &str, defaults: &str, mut each: impl FnMut(&mut Self, &str)->Usually<()>
|
||||||
|
) -> Usually<()> {
|
||||||
|
if self.dirs.find_config_file(path).is_none() {
|
||||||
|
//println!("Creating {path:?}");
|
||||||
|
std::fs::write(self.dirs.place_config_file(path)?, defaults)?;
|
||||||
|
}
|
||||||
|
Ok(if let Some(path) = self.dirs.find_config_file(path) {
|
||||||
|
//println!("Loading {path:?}");
|
||||||
|
let src = std::fs::read_to_string(&path)?;
|
||||||
|
src.as_str().each(move|item|each(self, item))?;
|
||||||
|
} else {
|
||||||
|
return Err(format!("{path}: not found").into())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/// Add statements to configuration from [Dsl] source.
|
||||||
|
pub fn add (&mut self, dsl: impl Language) -> Usually<&mut Self> {
|
||||||
|
dsl.each(|item|self.add_one(item))?;
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
fn add_one (&self, item: impl Language) -> Usually<()> {
|
||||||
|
if let Some(expr) = item.expr()? {
|
||||||
|
let head = expr.head()?;
|
||||||
|
let tail = expr.tail()?;
|
||||||
|
let name = tail.head()?;
|
||||||
|
let body = tail.tail()?;
|
||||||
|
//println!("Config::load: {} {} {}", head.unwrap_or_default(), name.unwrap_or_default(), body.unwrap_or_default());
|
||||||
|
match head {
|
||||||
|
Some("mode") if let Some(name) = name => load_mode(&self.modes, &name, &body)?,
|
||||||
|
Some("keys") if let Some(name) = name => load_bind(&self.binds, &name, &body)?,
|
||||||
|
Some("view") if let Some(name) = name => load_view(&self.views, &name, &body)?,
|
||||||
|
_ => return Err(format!("Config::load: expected view/keys/mode, got: {item:?}").into())
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
return Err(format!("Config::load: expected expr, got: {item:?}").into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,3 +1,26 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
def_command!(DeviceCommand: |device: Device| {});
|
||||||
|
|
||||||
|
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!(),
|
||||||
|
});
|
||||||
|
|
||||||
impl Device {
|
impl Device {
|
||||||
pub fn name (&self) -> &str {
|
pub fn name (&self) -> &str {
|
||||||
|
|
@ -581,3 +604,19 @@ impl_audio!(|self: DeviceAudio<'a>, client, scope|{
|
||||||
#[cfg(feature = "sf2")] Sf2 => { todo!() }, // TODO
|
#[cfg(feature = "sf2")] Sf2 => { todo!() }, // TODO
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
pub fn device_kinds () -> &'static [&'static str] {
|
||||||
|
&[
|
||||||
|
#[cfg(feature = "sampler")] "Sampler",
|
||||||
|
#[cfg(feature = "lv2")] "Plugin (LV2)",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: AsRef<Vec<Device>> + AsMut<Vec<Device>>> HasDevices for T {
|
||||||
|
fn devices (&self) -> &Vec<Device> {
|
||||||
|
self.as_ref()
|
||||||
|
}
|
||||||
|
fn devices_mut (&mut self) -> &mut Vec<Device> {
|
||||||
|
self.as_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
106
src/dialog.rs
Normal file
106
src/dialog.rs
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// Various possible dialog modes.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let dialog: tek::Dialog = Default::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq)] pub enum Dialog {
|
||||||
|
#[default] None,
|
||||||
|
Help(usize),
|
||||||
|
Menu(usize, MenuItems),
|
||||||
|
Device(usize),
|
||||||
|
Message(Arc<str>),
|
||||||
|
Browse(BrowseTarget, Arc<Browse>),
|
||||||
|
Options,
|
||||||
|
}
|
||||||
|
namespace!(App: Dialog { symbol = |app| {
|
||||||
|
":dialog/none" => Dialog::None,
|
||||||
|
":dialog/options" => Dialog::Options,
|
||||||
|
":dialog/device" => Dialog::Device(0),
|
||||||
|
":dialog/device/prev" => Dialog::Device(0),
|
||||||
|
":dialog/device/next" => Dialog::Device(0),
|
||||||
|
":dialog/help" => Dialog::Help(0),
|
||||||
|
":dialog/save" => Dialog::Browse(BrowseTarget::SaveProject,
|
||||||
|
Browse::new(None).unwrap().into()),
|
||||||
|
":dialog/load" => Dialog::Browse(BrowseTarget::LoadProject,
|
||||||
|
Browse::new(None).unwrap().into()),
|
||||||
|
":dialog/import/clip" => Dialog::Browse(BrowseTarget::ImportClip(Default::default()),
|
||||||
|
Browse::new(None).unwrap().into()),
|
||||||
|
":dialog/export/clip" => Dialog::Browse(BrowseTarget::ExportClip(Default::default()),
|
||||||
|
Browse::new(None).unwrap().into()),
|
||||||
|
":dialog/import/sample" => Dialog::Browse(BrowseTarget::ImportSample(Default::default()),
|
||||||
|
Browse::new(None).unwrap().into()),
|
||||||
|
":dialog/export/sample" => Dialog::Browse(BrowseTarget::ExportSample(Default::default()),
|
||||||
|
Browse::new(None).unwrap().into()),
|
||||||
|
}; });
|
||||||
|
impl Dialog {
|
||||||
|
/// ```
|
||||||
|
/// let _ = tek::Dialog::welcome();
|
||||||
|
/// ```
|
||||||
|
pub fn welcome () -> Self {
|
||||||
|
Self::Menu(1, MenuItems([
|
||||||
|
MenuItem("Resume session".into(), Arc::new(Box::new(|_|Ok(())))),
|
||||||
|
MenuItem("Create new session".into(), Arc::new(Box::new(|app|Ok({
|
||||||
|
app.dialog = Dialog::None;
|
||||||
|
app.mode = app.config.modes.clone().read().unwrap().get(":arranger").cloned().unwrap();
|
||||||
|
})))),
|
||||||
|
MenuItem("Load old session".into(), Arc::new(Box::new(|_|Ok(())))),
|
||||||
|
].into()))
|
||||||
|
}
|
||||||
|
/// FIXME: generalize
|
||||||
|
/// ```
|
||||||
|
/// let _ = tek::Dialog::welcome().menu_selected();
|
||||||
|
/// ```
|
||||||
|
pub fn menu_selected (&self) -> Option<usize> {
|
||||||
|
if let Self::Menu(selected, _) = self { Some(*selected) } else { None }
|
||||||
|
}
|
||||||
|
/// FIXME: generalize
|
||||||
|
/// ```
|
||||||
|
/// let _ = tek::Dialog::welcome().menu_next();
|
||||||
|
/// ```
|
||||||
|
pub fn menu_next (&self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.0.len()), items.clone()),
|
||||||
|
_ => Self::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// FIXME: generalize
|
||||||
|
/// ```
|
||||||
|
/// let _ = tek::Dialog::welcome().menu_prev();
|
||||||
|
/// ```
|
||||||
|
pub fn menu_prev (&self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.0.len()), items.clone()),
|
||||||
|
_ => Self::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// FIXME: generalize
|
||||||
|
/// ```
|
||||||
|
/// let _ = tek::Dialog::welcome().device_kind();
|
||||||
|
/// ```
|
||||||
|
pub fn device_kind (&self) -> Option<usize> {
|
||||||
|
if let Self::Device(index) = self { Some(*index) } else { None }
|
||||||
|
}
|
||||||
|
/// FIXME: generalize
|
||||||
|
/// ```
|
||||||
|
/// let _ = tek::Dialog::welcome().device_kind_next();
|
||||||
|
/// ```
|
||||||
|
pub fn device_kind_next (&self) -> Option<usize> {
|
||||||
|
self.device_kind().map(|index|(index + 1) % device_kinds().len())
|
||||||
|
}
|
||||||
|
/// FIXME: generalize
|
||||||
|
/// ```
|
||||||
|
/// let _ = tek::Dialog::welcome().device_kind_prev();
|
||||||
|
/// ```
|
||||||
|
pub fn device_kind_prev (&self) -> Option<usize> {
|
||||||
|
self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1)))
|
||||||
|
}
|
||||||
|
/// FIXME: implement
|
||||||
|
pub fn message (&self) -> Option<&str> { todo!() }
|
||||||
|
/// FIXME: implement
|
||||||
|
pub fn browser (&self) -> Option<&Arc<Browse>> { todo!() }
|
||||||
|
/// FIXME: implement
|
||||||
|
pub fn browser_target (&self) -> Option<&BrowseTarget> { todo!() }
|
||||||
|
}
|
||||||
|
|
||||||
27
src/menu.rs
Normal file
27
src/menu.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) });
|
||||||
|
impl_default!(MenuItem: Self("".into(), Arc::new(Box::new(|_|Ok(())))));
|
||||||
|
impl PartialEq for MenuItem { fn eq (&self, other: &Self) -> bool { self.0 == other.0 } }
|
||||||
|
impl AsRef<Arc<[MenuItem]>> for MenuItems { fn as_ref (&self) -> &Arc<[MenuItem]> { &self.0 } }
|
||||||
|
|
||||||
|
/// List of menu items.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let items: tek::MenuItems = Default::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq)] pub struct MenuItems(
|
||||||
|
pub Arc<[MenuItem]>
|
||||||
|
);
|
||||||
|
|
||||||
|
/// An item of a menu.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let item: tek::MenuItem = Default::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Clone)] pub struct MenuItem(
|
||||||
|
/// Label
|
||||||
|
pub Arc<str>,
|
||||||
|
/// Callback
|
||||||
|
pub Arc<Box<dyn Fn(&mut App)->Usually<()> + Send + Sync>>
|
||||||
|
);
|
||||||
51
src/mix.rs
51
src/mix.rs
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::*;
|
||||||
#[derive(Debug, Default)] pub enum MeteringMode {
|
#[derive(Debug, Default)] pub enum MeteringMode {
|
||||||
#[default] Rms,
|
#[default] Rms,
|
||||||
Log10,
|
Log10,
|
||||||
|
|
@ -68,3 +69,53 @@
|
||||||
h_full(RmsMeter(*value))
|
h_full(RmsMeter(*value))
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
|
||||||
98
src/mode.rs
Normal file
98
src/mode.rs
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
use crate::*;
|
||||||
|
impl Config {
|
||||||
|
pub fn get_mode (&self, mode: impl AsRef<str>) -> Option<Arc<Mode<Arc<str>>>> {
|
||||||
|
self.modes.clone().read().unwrap().get(mode.as_ref()).cloned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn load_mode (modes: &Modes, name: &impl AsRef<str>, body: &impl Language) -> Usually<()> {
|
||||||
|
let mut mode = Mode::default();
|
||||||
|
body.each(|item|mode.add(item))?;
|
||||||
|
modes.write().unwrap().insert(name.as_ref().into(), Arc::new(mode));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collection of interaction modes.
|
||||||
|
pub type Modes = Arc<RwLock<BTreeMap<Arc<str>, Arc<Mode<Arc<str>>>>>>;
|
||||||
|
|
||||||
|
impl Mode<Arc<str>> {
|
||||||
|
/// Add a definition to the mode.
|
||||||
|
///
|
||||||
|
/// Supported definitions:
|
||||||
|
///
|
||||||
|
/// - (name ...) -> name
|
||||||
|
/// - (info ...) -> description
|
||||||
|
/// - (keys ...) -> key bindings
|
||||||
|
/// - (mode ...) -> submode
|
||||||
|
/// - ... -> view
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let mut mode: tek::Mode<std::sync::Arc<str>> = Default::default();
|
||||||
|
/// mode.add("(name hello)").unwrap();
|
||||||
|
/// ```
|
||||||
|
pub fn add (&mut self, dsl: impl Language) -> Usually<()> {
|
||||||
|
Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() {
|
||||||
|
//println!("Mode::add: {head} {:?}", expr.tail());
|
||||||
|
let tail = expr.tail()?.map(|x|x.trim()).unwrap_or("");
|
||||||
|
match head {
|
||||||
|
"name" => self.add_name(tail)?,
|
||||||
|
"info" => self.add_info(tail)?,
|
||||||
|
"keys" => self.add_keys(tail)?,
|
||||||
|
"mode" => self.add_mode(tail)?,
|
||||||
|
_ => self.add_view(tail)?,
|
||||||
|
};
|
||||||
|
} else if let Ok(Some(word)) = dsl.word() {
|
||||||
|
self.add_view(word);
|
||||||
|
} else {
|
||||||
|
return Err(format!("Mode::add: unexpected: {dsl:?}").into());
|
||||||
|
})
|
||||||
|
|
||||||
|
//DslParse(dsl, ||Err(format!("Mode::add: unexpected: {dsl:?}").into()))
|
||||||
|
//.word(|word|self.add_view(word))
|
||||||
|
//.expr(|expr|expr.head(|head|{
|
||||||
|
////println!("Mode::add: {head} {:?}", expr.tail());
|
||||||
|
//let tail = expr.tail()?.map(|x|x.trim()).unwrap_or("");
|
||||||
|
//match head {
|
||||||
|
//"name" => self.add_name(tail),
|
||||||
|
//"info" => self.add_info(tail),
|
||||||
|
//"keys" => self.add_keys(tail)?,
|
||||||
|
//"mode" => self.add_mode(tail)?,
|
||||||
|
//_ => self.add_view(tail),
|
||||||
|
//};
|
||||||
|
//}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_name (&mut self, dsl: impl Language) -> Perhaps<()> {
|
||||||
|
Ok(dsl.src()?.map(|src|self.name.push(src.into())))
|
||||||
|
}
|
||||||
|
fn add_info (&mut self, dsl: impl Language) -> Perhaps<()> {
|
||||||
|
Ok(dsl.src()?.map(|src|self.info.push(src.into())))
|
||||||
|
}
|
||||||
|
fn add_view (&mut self, dsl: impl Language) -> Perhaps<()> {
|
||||||
|
Ok(dsl.src()?.map(|src|self.view.push(src.into())))
|
||||||
|
}
|
||||||
|
fn add_keys (&mut self, dsl: impl Language) -> Perhaps<()> {
|
||||||
|
Ok(Some(dsl.each(|expr|{ self.keys.push(expr.trim().into()); Ok(()) })?))
|
||||||
|
}
|
||||||
|
fn add_mode (&mut self, dsl: impl Language) -> Perhaps<()> {
|
||||||
|
Ok(Some(if let Some(id) = dsl.head()? {
|
||||||
|
load_mode(&self.modes, &id, &dsl.tail())?;
|
||||||
|
} else {
|
||||||
|
return Err(format!("Mode::add: self: incomplete: {dsl:?}").into());
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Group of view and keys definitions.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let mode = tek::Mode::<std::sync::Arc<str>>::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Default, Debug)] pub struct Mode<D: Language + Ord> {
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub name: Vec<D>,
|
||||||
|
pub info: Vec<D>,
|
||||||
|
pub view: Vec<D>,
|
||||||
|
pub keys: Vec<D>,
|
||||||
|
pub modes: Modes,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
/// A LV2 plugin.
|
/// A LV2 plugin.
|
||||||
#[derive(Debug)] #[cfg(feature = "lv2")] pub struct Lv2 {
|
#[derive(Debug)] #[cfg(feature = "lv2")] pub struct Lv2 {
|
||||||
|
|
@ -176,3 +177,30 @@ fn draw_header (state: &Lv2, to: &mut Tui, x: u16, y: u16, w: u16) {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "vst2")] impl<E: Engine> ::vst::host::Host for Plugin<E> {}
|
#[cfg(feature = "vst2")] impl<E: Engine> ::vst::host::Host for Plugin<E> {}
|
||||||
|
|
||||||
|
|
||||||
|
#[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()?
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,54 @@
|
||||||
|
use crate::*;
|
||||||
use ::std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::*}};
|
use ::std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::*}};
|
||||||
use crate::device::{MidiInput, MidiOutput, AudioInput, AudioOutput};
|
use crate::device::{MidiInput, MidiOutput, AudioInput, AudioOutput};
|
||||||
|
|
||||||
|
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.samples.0[*index]);
|
||||||
|
sample
|
||||||
|
}); // TODO: undo
|
||||||
|
Ok(None)
|
||||||
|
},
|
||||||
|
RecordCancel => {
|
||||||
|
sampler.recording = None;
|
||||||
|
Ok(None)
|
||||||
|
},
|
||||||
|
PlaySample { slot: usize } => {
|
||||||
|
let slot = *slot;
|
||||||
|
if let Some(ref sample) = sampler.samples.0[slot] {
|
||||||
|
sampler.voices.write().unwrap().push(Sample::play(sample, 0, &u7::from(128)));
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
},
|
||||||
|
StopSample { slot: usize } => {
|
||||||
|
let _slot = *slot;
|
||||||
|
todo!();
|
||||||
|
//Ok(None)
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
/// Plays [Voice]s from [Sample]s.
|
/// Plays [Voice]s from [Sample]s.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
|
|
@ -651,3 +699,7 @@ fn draw_sample (
|
||||||
to.blit(&label2, x+3+label1.len()as u16, y, Some(style));
|
to.blit(&label2, x+3+label1.len()as u16, y, Some(style));
|
||||||
Ok(label1.len() + label2.len() + 4)
|
Ok(label1.len() + label2.len() + 4)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn read_sample_data (_: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,30 @@
|
||||||
|
use crate::*;
|
||||||
use ::std::sync::{Arc, RwLock};
|
use ::std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
/// Contains state for viewing and editing a clip.
|
/// Contains state for viewing and editing a clip.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
|
|
@ -1414,3 +1439,70 @@ impl Iterator for Ticker {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return boxed iterator of MIDI events
|
||||||
|
pub fn parse_midi_input <'a> (input: ::tengri::jack::MidiIter<'a>)
|
||||||
|
-> Box<dyn Iterator<Item=(usize, LiveEvent<'a>, &'a [u8])> + 'a>
|
||||||
|
{
|
||||||
|
Box::new(input.map(|::tengri::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]
|
||||||
|
}
|
||||||
|
|
|
||||||
1389
src/tek.rs
1389
src/tek.rs
File diff suppressed because it is too large
Load diff
10
src/view.rs
Normal file
10
src/view.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// Collection of view definitions.
|
||||||
|
pub type Views = Arc<RwLock<BTreeMap<Arc<str>, Arc<str>>>>;
|
||||||
|
|
||||||
|
pub(crate) fn load_view (views: &Views, name: &impl AsRef<str>, body: &impl Language) -> Usually<()> {
|
||||||
|
views.write().unwrap().insert(name.as_ref().into(), body.src()?.unwrap_or_default().into());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue