mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-01-31 16:36:40 +01:00
Compare commits
11 commits
22d3cc5a5f
...
b8e0ffc136
| Author | SHA1 | Date | |
|---|---|---|---|
| b8e0ffc136 | |||
| cd8d85bd97 | |||
| aefc147347 | |||
| 0efcb7f0fe | |||
| 26baa8127d | |||
| 6ed0627056 | |||
| 0e5207a79d | |||
| b0c936bda0 | |||
| 0533ea92ac | |||
| 457e6bb7eb | |||
| a22a793c31 |
31 changed files with 1429 additions and 1332 deletions
25
config/config_arranger.edn
Normal file
25
config/config_arranger.edn
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
(name "Arranger")
|
||||||
|
|
||||||
|
(info "A session grid.")
|
||||||
|
|
||||||
|
(view
|
||||||
|
(bsp/a :modal
|
||||||
|
(bsp/s (fixed/y 1 :transport)
|
||||||
|
(bsp/n (fixed/y 1 :status)
|
||||||
|
(fill/xy (bsp/a
|
||||||
|
(fill/xy (align/e :pool))
|
||||||
|
:arranger))))))
|
||||||
|
|
||||||
|
(keys
|
||||||
|
(layer-if :mode-pool-import "./keys_pool_file.edn")
|
||||||
|
(layer-if :mode-pool-export "./keys_pool_file.edn")
|
||||||
|
(layer-if :mode-pool-rename "./keys_clip_rename.edn")
|
||||||
|
(layer-if :mode-pool-length "./keys_clip_length.edn")
|
||||||
|
(layer "./keys_global.edn")
|
||||||
|
(layer-if :mode-editor "./keys_editor.edn")
|
||||||
|
(layer-if :mode-clip "./keys_clip.edn")
|
||||||
|
(layer-if :mode-track "./keys_track.edn")
|
||||||
|
(layer-if :mode-scene "./keys_scene.edn")
|
||||||
|
(layer-if :mode-mix "./keys_mix.edn")
|
||||||
|
(layer "./keys_clock.edn")
|
||||||
|
(layer "./keys_arranger.edn"))
|
||||||
22
config/config_groovebox.edn
Normal file
22
config/config_groovebox.edn
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
(name "Arranger")
|
||||||
|
|
||||||
|
(info "A sequencer with built-in sampler.")
|
||||||
|
|
||||||
|
(view
|
||||||
|
(bsp/a :modal
|
||||||
|
(bsp/s (fixed/y 1 :transport)
|
||||||
|
(bsp/n (fixed/y 1 :status)
|
||||||
|
(bsp/n (fixed/y 5 :sample-viewer)
|
||||||
|
(bsp/w (fixed/x :w-sidebar :pool)
|
||||||
|
(bsp/e :samples-keys
|
||||||
|
(fill/y :editor))))))))
|
||||||
|
|
||||||
|
(keys
|
||||||
|
(layer-if :mode-pool-import "./keys_pool_file.edn")
|
||||||
|
(layer-if :mode-pool-export "./keys_pool_file.edn")
|
||||||
|
(layer-if :mode-pool-rename "./keys_clip_rename.edn")
|
||||||
|
(layer-if :mode-pool-length "./keys_clip_length.edn")
|
||||||
|
(layer "./keys_global.edn")
|
||||||
|
(layer-if :mode-editor "./keys_editor.edn")
|
||||||
|
(layer "./keys_clock.edn")
|
||||||
|
(layer "./keys_arranger.edn"))
|
||||||
13
config/config_sampler.edn
Normal file
13
config/config_sampler.edn
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
(name "Sampler")
|
||||||
|
|
||||||
|
(info "A sampling soundboard.")
|
||||||
|
|
||||||
|
(view
|
||||||
|
(bsp/a :modal
|
||||||
|
(bsp/s (fixed/y 1 :transport)
|
||||||
|
(bsp/n (fixed/y 1 :status)
|
||||||
|
(fill/xy :samples-grid)))))
|
||||||
|
|
||||||
|
(keys
|
||||||
|
(layer "./keys_global.edn")
|
||||||
|
(layer "./keys_sampler.edn"))
|
||||||
21
config/config_sequencer.edn
Normal file
21
config/config_sequencer.edn
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
(name "Sequencer")
|
||||||
|
|
||||||
|
(info "A MIDI sequencer.")
|
||||||
|
|
||||||
|
(view
|
||||||
|
(bsp/a :modal
|
||||||
|
(bsp/s (fixed/y 1 :transport)
|
||||||
|
(bsp/n (fixed/y 1 :status)
|
||||||
|
(fill/xy (bsp/a
|
||||||
|
(fill/xy (align/e :pool))
|
||||||
|
:editor)))))
|
||||||
|
|
||||||
|
(keys
|
||||||
|
(layer-if :mode-pool-import "./keys_pool_file.edn")
|
||||||
|
(layer-if :mode-pool-export "./keys_pool_file.edn")
|
||||||
|
(layer-if :mode-pool-rename "./keys_clip_rename.edn")
|
||||||
|
(layer-if :mode-pool-length "./keys_clip_length.edn")
|
||||||
|
(layer "./keys_global.edn")
|
||||||
|
(layer "./keys_editor.edn")
|
||||||
|
(layer "./keys_clock.edn")
|
||||||
|
(layer "./keys_arranger.edn"))
|
||||||
9
config/config_transport.edn
Normal file
9
config/config_transport.edn
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
(name "Transport")
|
||||||
|
|
||||||
|
(info "A JACK transport controller.")
|
||||||
|
|
||||||
|
(view :transport)
|
||||||
|
|
||||||
|
(keys
|
||||||
|
(layer "./keys_global.edn")
|
||||||
|
(layer "./keys_clock.edn"))
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
(bsp/a :modal
|
|
||||||
(bsp/s (fixed/y 1 :transport)
|
|
||||||
(bsp/n (fixed/y 1 :status)
|
|
||||||
(fill/xy (bsp/a
|
|
||||||
(fill/xy (align/e :pool))
|
|
||||||
:arranger)))))
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
(bsp/a :modal
|
|
||||||
(bsp/s (fixed/y 1 :transport)
|
|
||||||
(bsp/n (fixed/y 1 :status)
|
|
||||||
(bsp/n (fixed/y 5 :sample-viewer)
|
|
||||||
(bsp/w (fixed/x :w-sidebar :pool)
|
|
||||||
(bsp/e :samples-keys
|
|
||||||
(fill/y :editor)))))))
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
(bsp/a :modal
|
|
||||||
(bsp/s (fixed/y 1 :transport)
|
|
||||||
(bsp/n (fixed/y 1 :status)
|
|
||||||
(fill/xy :samples-grid))))
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
(bsp/a :modal
|
|
||||||
(bsp/s (fixed/y 1 :transport)
|
|
||||||
(bsp/n (fixed/y 1 :status)
|
|
||||||
(fill/xy (bsp/a
|
|
||||||
(fill/xy (align/e :pool))
|
|
||||||
:editor)))))
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
:transport
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
type MaybeClip = Option<Arc<RwLock<MidiClip>>>;
|
type MaybeClip = Option<Arc<RwLock<MidiClip>>>;
|
||||||
|
|
||||||
|
|
@ -31,8 +32,50 @@ expose!([self: Tek]
|
||||||
(":scene-prev" self.selected.scene_prev())
|
(":scene-prev" self.selected.scene_prev())
|
||||||
(":track-next" self.selected.track_next(self.tracks.len()))
|
(":track-next" self.selected.track_next(self.tracks.len()))
|
||||||
(":track-prev" self.selected.track_prev())));
|
(":track-prev" self.selected.track_prev())));
|
||||||
|
provide!(bool: |self: MidiPool| {});
|
||||||
|
provide!(MidiClip: |self: MidiPool| {
|
||||||
|
":new-clip" => self.new_clip(),
|
||||||
|
":cloned-clip" => self.cloned_clip(),
|
||||||
|
});
|
||||||
|
provide!(PathBuf: |self: MidiPool| {});
|
||||||
|
provide!(Arc<str>: |self: MidiPool| {});
|
||||||
|
provide!(usize: |self: MidiPool| {
|
||||||
|
":current" => 0,
|
||||||
|
":after" => 0,
|
||||||
|
":previous" => 0,
|
||||||
|
":next" => 0
|
||||||
|
});
|
||||||
|
provide!(ItemColor: |self: MidiPool| {
|
||||||
|
":random-color" => ItemColor::random()
|
||||||
|
});
|
||||||
|
provide!(bool: |self: MidiEditor| {
|
||||||
|
":true" => true,
|
||||||
|
":false" => false,
|
||||||
|
":time-lock" => self.time_lock().get(),
|
||||||
|
":time-lock-toggle" => !self.time_lock().get(),
|
||||||
|
});
|
||||||
|
provide!(usize: |self: MidiEditor| {
|
||||||
|
":note-length" => self.note_len(),
|
||||||
|
":note-pos" => self.note_pos(),
|
||||||
|
":note-pos-next" => self.note_pos() + 1,
|
||||||
|
":note-pos-prev" => self.note_pos().saturating_sub(1),
|
||||||
|
":note-pos-next-octave" => self.note_pos() + 12,
|
||||||
|
":note-pos-prev-octave" => self.note_pos().saturating_sub(12),
|
||||||
|
":note-len" => self.note_len(),
|
||||||
|
":note-len-next" => self.note_len() + 1,
|
||||||
|
":note-len-prev" => self.note_len().saturating_sub(1),
|
||||||
|
":note-range" => self.note_axis().get(),
|
||||||
|
":note-range-prev" => self.note_axis().get() + 1,
|
||||||
|
":note-range-next" => self.note_axis().get().saturating_sub(1),
|
||||||
|
":time-pos" => self.time_pos(),
|
||||||
|
":time-pos-next" => self.time_pos() + self.time_zoom().get(),
|
||||||
|
":time-pos-prev" => self.time_pos().saturating_sub(self.time_zoom().get()),
|
||||||
|
":time-zoom" => self.time_zoom().get(),
|
||||||
|
":time-zoom-next" => self.time_zoom().get() + 1,
|
||||||
|
":time-zoom-prev" => self.time_zoom().get().saturating_sub(1).max(1),
|
||||||
|
});
|
||||||
|
|
||||||
handle!(TuiIn: |self: Tek, input|Ok(if let Some(command) = self.keys.command(self, input) {
|
handle!(TuiIn: |self: Tek, input|Ok(if let Some(command) = self.config.keys.command(self, input) {
|
||||||
let undo = command.execute(self)?;
|
let undo = command.execute(self)?;
|
||||||
if let Some(undo) = undo {
|
if let Some(undo) = undo {
|
||||||
self.history.push(undo);
|
self.history.push(undo);
|
||||||
|
|
@ -193,3 +236,309 @@ fn delegate_to_pool (app: &mut Tek, cmd: PoolCommand) -> Perhaps<TekCommand> {
|
||||||
None
|
None
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Debug)] pub enum PoolCommand {
|
||||||
|
/// Toggle visibility of pool
|
||||||
|
Show(bool),
|
||||||
|
/// Select a clip from the clip pool
|
||||||
|
Select(usize),
|
||||||
|
/// Rename a clip
|
||||||
|
Rename(ClipRenameCommand),
|
||||||
|
/// Change the length of a clip
|
||||||
|
Length(ClipLengthCommand),
|
||||||
|
/// Import from file
|
||||||
|
Import(FileBrowserCommand),
|
||||||
|
/// Export to file
|
||||||
|
Export(FileBrowserCommand),
|
||||||
|
/// Update the contents of the clip pool
|
||||||
|
Clip(PoolClipCommand),
|
||||||
|
}
|
||||||
|
|
||||||
|
atom_command!(PoolCommand: |state: MidiPool| {
|
||||||
|
("show" [a: bool] Some(Self::Show(a.expect("no flag"))))
|
||||||
|
("select" [i: usize] Some(Self::Select(i.expect("no index"))))
|
||||||
|
("rename" [,..a] ClipRenameCommand::try_from_expr(state, a).map(Self::Rename))
|
||||||
|
("length" [,..a] ClipLengthCommand::try_from_expr(state, a).map(Self::Length))
|
||||||
|
("import" [,..a] FileBrowserCommand::try_from_expr(state, a).map(Self::Import))
|
||||||
|
("export" [,..a] FileBrowserCommand::try_from_expr(state, a).map(Self::Export))
|
||||||
|
("clip" [,..a] PoolClipCommand::try_from_expr(state, a).map(Self::Clip))
|
||||||
|
});
|
||||||
|
|
||||||
|
command!(|self: PoolCommand, state: MidiPool|{
|
||||||
|
use PoolCommand::*;
|
||||||
|
match self {
|
||||||
|
Rename(ClipRenameCommand::Begin) => { state.begin_clip_rename(); None }
|
||||||
|
Rename(command) => command.delegate(state, Rename)?,
|
||||||
|
Length(ClipLengthCommand::Begin) => { state.begin_clip_length(); None },
|
||||||
|
Length(command) => command.delegate(state, Length)?,
|
||||||
|
Import(FileBrowserCommand::Begin) => { state.begin_import()?; None },
|
||||||
|
Import(command) => command.delegate(state, Import)?,
|
||||||
|
Export(FileBrowserCommand::Begin) => { state.begin_export()?; None },
|
||||||
|
Export(command) => command.delegate(state, Export)?,
|
||||||
|
Clip(command) => command.execute(state)?.map(Clip),
|
||||||
|
Show(visible) => { state.visible = visible; Some(Self::Show(!visible)) },
|
||||||
|
Select(clip) => { state.set_clip_index(clip); None },
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)] pub enum PoolClipCommand {
|
||||||
|
Add(usize, MidiClip),
|
||||||
|
Delete(usize),
|
||||||
|
Swap(usize, usize),
|
||||||
|
Import(usize, PathBuf),
|
||||||
|
Export(usize, PathBuf),
|
||||||
|
SetName(usize, Arc<str>),
|
||||||
|
SetLength(usize, usize),
|
||||||
|
SetColor(usize, ItemColor),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: HasClips> Command<T> for PoolClipCommand {
|
||||||
|
fn execute (self, model: &mut T) -> Perhaps<Self> {
|
||||||
|
use PoolClipCommand::*;
|
||||||
|
Ok(match self {
|
||||||
|
Add(mut index, clip) => {
|
||||||
|
let clip = Arc::new(RwLock::new(clip));
|
||||||
|
let mut clips = model.clips_mut();
|
||||||
|
if index >= clips.len() {
|
||||||
|
index = clips.len();
|
||||||
|
clips.push(clip)
|
||||||
|
} else {
|
||||||
|
clips.insert(index, clip);
|
||||||
|
}
|
||||||
|
Some(Self::Delete(index))
|
||||||
|
},
|
||||||
|
Delete(index) => {
|
||||||
|
let clip = model.clips_mut().remove(index).read().unwrap().clone();
|
||||||
|
Some(Self::Add(index, clip))
|
||||||
|
},
|
||||||
|
Swap(index, other) => {
|
||||||
|
model.clips_mut().swap(index, other);
|
||||||
|
Some(Self::Swap(index, other))
|
||||||
|
},
|
||||||
|
Import(index, path) => {
|
||||||
|
let bytes = std::fs::read(&path)?;
|
||||||
|
let smf = Smf::parse(bytes.as_slice())?;
|
||||||
|
let mut t = 0u32;
|
||||||
|
let mut events = vec![];
|
||||||
|
for track in smf.tracks.iter() {
|
||||||
|
for event in track.iter() {
|
||||||
|
t += event.delta.as_int();
|
||||||
|
if let TrackEventKind::Midi { channel, message } = event.kind {
|
||||||
|
events.push((t, channel.as_int(), message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut clip = MidiClip::new("imported", true, t as usize + 1, None, None);
|
||||||
|
for event in events.iter() {
|
||||||
|
clip.notes[event.0 as usize].push(event.2);
|
||||||
|
}
|
||||||
|
Self::Add(index, clip).execute(model)?
|
||||||
|
},
|
||||||
|
Export(_index, _path) => {
|
||||||
|
todo!("export clip to midi file");
|
||||||
|
},
|
||||||
|
SetName(index, name) => {
|
||||||
|
let clip = &mut model.clips_mut()[index];
|
||||||
|
let old_name = clip.read().unwrap().name.clone();
|
||||||
|
clip.write().unwrap().name = name;
|
||||||
|
Some(Self::SetName(index, old_name))
|
||||||
|
},
|
||||||
|
SetLength(index, length) => {
|
||||||
|
let clip = &mut model.clips_mut()[index];
|
||||||
|
let old_len = clip.read().unwrap().length;
|
||||||
|
clip.write().unwrap().length = length;
|
||||||
|
Some(Self::SetLength(index, old_len))
|
||||||
|
},
|
||||||
|
SetColor(index, color) => {
|
||||||
|
let mut color = ItemTheme::from(color);
|
||||||
|
std::mem::swap(&mut color, &mut model.clips()[index].write().unwrap().color);
|
||||||
|
Some(Self::SetColor(index, color.base))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
atom_command!(ClipRenameCommand: |state: MidiPool| {
|
||||||
|
("begin" [] Some(Self::Begin))
|
||||||
|
("cancel" [] Some(Self::Cancel))
|
||||||
|
("confirm" [] Some(Self::Confirm))
|
||||||
|
("set" [n: Arc<str>] Some(Self::Set(n.expect("no name"))))
|
||||||
|
});
|
||||||
|
atom_command!(ClipLengthCommand: |state: MidiPool| {
|
||||||
|
("begin" [] Some(Self::Begin))
|
||||||
|
("cancel" [] Some(Self::Cancel))
|
||||||
|
("next" [] Some(Self::Next))
|
||||||
|
("prev" [] Some(Self::Prev))
|
||||||
|
("inc" [] Some(Self::Inc))
|
||||||
|
("dec" [] Some(Self::Dec))
|
||||||
|
("set" [l: usize] Some(Self::Set(l.expect("no length"))))
|
||||||
|
});
|
||||||
|
atom_command!(FileBrowserCommand: |state: MidiPool| {
|
||||||
|
("begin" [] Some(Self::Begin))
|
||||||
|
("cancel" [] Some(Self::Cancel))
|
||||||
|
("confirm" [] Some(Self::Confirm))
|
||||||
|
("select" [i: usize] Some(Self::Select(i.expect("no index"))))
|
||||||
|
("chdir" [p: PathBuf] Some(Self::Chdir(p.expect("no path"))))
|
||||||
|
("filter" [f: Arc<str>] Some(Self::Filter(f.expect("no filter"))))
|
||||||
|
});
|
||||||
|
atom_command!(MidiEditCommand: |state: MidiEditor| {
|
||||||
|
("note/append" [] Some(Self::AppendNote))
|
||||||
|
("note/put" [] Some(Self::PutNote))
|
||||||
|
("note/del" [] Some(Self::DelNote))
|
||||||
|
("note/pos" [a: usize] Some(Self::SetNoteCursor(a.expect("no note cursor"))))
|
||||||
|
("note/len" [a: usize] Some(Self::SetNoteLength(a.expect("no note length"))))
|
||||||
|
("time/pos" [a: usize] Some(Self::SetTimeCursor(a.expect("no time cursor"))))
|
||||||
|
("time/zoom" [a: usize] Some(Self::SetTimeZoom(a.expect("no time zoom"))))
|
||||||
|
("time/lock" [a: bool] Some(Self::SetTimeLock(a.expect("no time lock"))))
|
||||||
|
("time/lock" [] Some(Self::SetTimeLock(!state.time_lock().get())))
|
||||||
|
});
|
||||||
|
atom_command!(PoolClipCommand: |state: MidiPool| {
|
||||||
|
("add" [i: usize, c: MidiClip]
|
||||||
|
Some(Self::Add(i.expect("no index"), c.expect("no clip"))))
|
||||||
|
("delete" [i: usize]
|
||||||
|
Some(Self::Delete(i.expect("no index"))))
|
||||||
|
("swap" [a: usize, b: usize]
|
||||||
|
Some(Self::Swap(a.expect("no index"), b.expect("no index"))))
|
||||||
|
("import" [i: usize, p: PathBuf]
|
||||||
|
Some(Self::Import(i.expect("no index"), p.expect("no path"))))
|
||||||
|
("export" [i: usize, p: PathBuf]
|
||||||
|
Some(Self::Export(i.expect("no index"), p.expect("no path"))))
|
||||||
|
("set-name" [i: usize, n: Arc<str>]
|
||||||
|
Some(Self::SetName(i.expect("no index"), n.expect("no name"))))
|
||||||
|
("set-length" [i: usize, l: usize]
|
||||||
|
Some(Self::SetLength(i.expect("no index"), l.expect("no length"))))
|
||||||
|
("set-color" [i: usize, c: ItemColor]
|
||||||
|
Some(Self::SetColor(i.expect("no index"), c.expect("no color"))))
|
||||||
|
});
|
||||||
|
// TODO: 1-9 seek markers that by default start every 8th of the clip
|
||||||
|
defcom!([self, state: MidiEditor]
|
||||||
|
(MidiEditCommand
|
||||||
|
(AppendNote [] { state.put_note(true); None })
|
||||||
|
(PutNote [] { state.put_note(false); None })
|
||||||
|
(DelNote [] { None })
|
||||||
|
(SetNoteCursor [x: usize] { state.set_note_pos(x.min(127)); None })
|
||||||
|
(SetNoteLength [x: usize] {
|
||||||
|
let note_len = state.note_len();
|
||||||
|
let time_zoom = state.time_zoom().get();
|
||||||
|
state.set_note_len(x);
|
||||||
|
//if note_len / time_zoom != x / time_zoom {
|
||||||
|
state.redraw();
|
||||||
|
//}
|
||||||
|
None
|
||||||
|
})
|
||||||
|
(SetNoteScroll [x: usize] { state.note_lo().set(x.min(127)); None })
|
||||||
|
(SetTimeCursor [x: usize] { state.set_time_pos(x); None })
|
||||||
|
(SetTimeScroll [x: usize] { state.time_start().set(x); None })
|
||||||
|
(SetTimeZoom [x: usize] { state.time_zoom().set(x); state.redraw(); None })
|
||||||
|
(SetTimeLock [x: bool] { state.time_lock().set(x); None })
|
||||||
|
(Show [x: MaybeClip] { state.set_clip(x.as_ref()); None })));
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)] pub enum ClipRenameCommand {
|
||||||
|
Begin,
|
||||||
|
Cancel,
|
||||||
|
Confirm,
|
||||||
|
Set(Arc<str>),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
command!(|self: ClipRenameCommand, state: MidiPool|if let Some(
|
||||||
|
PoolMode::Rename(clip, ref mut old_name)
|
||||||
|
) = state.mode_mut().clone() {
|
||||||
|
match self {
|
||||||
|
Self::Set(s) => {
|
||||||
|
state.clips()[clip].write().unwrap().name = s;
|
||||||
|
return Ok(Some(Self::Set(old_name.clone().into())))
|
||||||
|
},
|
||||||
|
Self::Confirm => {
|
||||||
|
let old_name = old_name.clone();
|
||||||
|
*state.mode_mut() = None;
|
||||||
|
return Ok(Some(Self::Set(old_name)))
|
||||||
|
},
|
||||||
|
Self::Cancel => {
|
||||||
|
state.clips()[clip].write().unwrap().name = old_name.clone().into();
|
||||||
|
return Ok(None)
|
||||||
|
},
|
||||||
|
_ => unreachable!()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
});
|
||||||
|
|
||||||
|
command!(|self: FileBrowserCommand, state: MidiPool|{
|
||||||
|
use PoolMode::*;
|
||||||
|
use FileBrowserCommand::*;
|
||||||
|
let mode = &mut state.mode;
|
||||||
|
match mode {
|
||||||
|
Some(Import(index, ref mut browser)) => match self {
|
||||||
|
Cancel => { *mode = None; },
|
||||||
|
Chdir(cwd) => { *mode = Some(Import(*index, FileBrowser::new(Some(cwd))?)); },
|
||||||
|
Select(index) => { browser.index = index; },
|
||||||
|
Confirm => if browser.is_file() {
|
||||||
|
let index = *index;
|
||||||
|
let path = browser.path();
|
||||||
|
*mode = None;
|
||||||
|
PoolClipCommand::Import(index, path).execute(state)?;
|
||||||
|
} else if browser.is_dir() {
|
||||||
|
*mode = Some(Import(*index, browser.chdir()?));
|
||||||
|
},
|
||||||
|
_ => todo!(),
|
||||||
|
},
|
||||||
|
Some(Export(index, ref mut browser)) => match self {
|
||||||
|
Cancel => { *mode = None; },
|
||||||
|
Chdir(cwd) => { *mode = Some(Export(*index, FileBrowser::new(Some(cwd))?)); },
|
||||||
|
Select(index) => { browser.index = index; },
|
||||||
|
_ => unreachable!()
|
||||||
|
},
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
None
|
||||||
|
});
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||||
|
pub enum ClipLengthCommand {
|
||||||
|
Begin,
|
||||||
|
Cancel,
|
||||||
|
Set(usize),
|
||||||
|
Next,
|
||||||
|
Prev,
|
||||||
|
Inc,
|
||||||
|
Dec,
|
||||||
|
}
|
||||||
|
|
||||||
|
command!(|self: ClipLengthCommand, state: MidiPool|{
|
||||||
|
use ClipLengthCommand::*;
|
||||||
|
use ClipLengthFocus::*;
|
||||||
|
if let Some(
|
||||||
|
PoolMode::Length(clip, ref mut length, ref mut focus)
|
||||||
|
) = state.mode_mut().clone() {
|
||||||
|
match self {
|
||||||
|
Cancel => { *state.mode_mut() = None; },
|
||||||
|
Prev => { focus.prev() },
|
||||||
|
Next => { focus.next() },
|
||||||
|
Inc => match focus {
|
||||||
|
Bar => { *length += 4 * PPQ },
|
||||||
|
Beat => { *length += PPQ },
|
||||||
|
Tick => { *length += 1 },
|
||||||
|
},
|
||||||
|
Dec => match focus {
|
||||||
|
Bar => { *length = length.saturating_sub(4 * PPQ) },
|
||||||
|
Beat => { *length = length.saturating_sub(PPQ) },
|
||||||
|
Tick => { *length = length.saturating_sub(1) },
|
||||||
|
},
|
||||||
|
Set(length) => {
|
||||||
|
let old_length;
|
||||||
|
{
|
||||||
|
let clip = state.clips()[clip].clone();//.write().unwrap();
|
||||||
|
old_length = Some(clip.read().unwrap().length);
|
||||||
|
clip.write().unwrap().length = length;
|
||||||
|
}
|
||||||
|
*state.mode_mut() = None;
|
||||||
|
return Ok(old_length.map(Self::Set))
|
||||||
|
},
|
||||||
|
_ => unreachable!()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
unreachable!();
|
||||||
|
}
|
||||||
|
None
|
||||||
|
});
|
||||||
|
|
|
||||||
161
crates/app/src/config.rs
Normal file
161
crates/app/src/config.rs
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
use crate::*;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// Configuration
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct Configuration {
|
||||||
|
/// Path of configuration entrypoint
|
||||||
|
pub path: PathBuf,
|
||||||
|
/// Name of configuration
|
||||||
|
pub name: Option<Arc<str>>,
|
||||||
|
/// Description of configuration
|
||||||
|
pub info: Option<Arc<str>>,
|
||||||
|
/// View definition
|
||||||
|
pub view: TokenIter<'static>,
|
||||||
|
// Input keymap
|
||||||
|
pub keys: InputMap<'static, Tek, TekCommand, TuiIn, TokenIter<'static>>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Configuration {
|
||||||
|
|
||||||
|
pub fn new (path: &impl AsRef<Path>, _watch: bool) -> Usually<Self> {
|
||||||
|
let text = read_and_leak(path.as_ref())?;
|
||||||
|
let [name, info, view, keys] = Self::parse(TokenIter::from(text))?;
|
||||||
|
Ok(Self {
|
||||||
|
path: path.as_ref().into(),
|
||||||
|
info: info.map(Self::parse_info).flatten(),
|
||||||
|
name: name.map(Self::parse_name).flatten(),
|
||||||
|
view: Self::parse_view(view)?,
|
||||||
|
keys: Self::parse_keys(&path, keys)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse (iter: TokenIter) -> Usually<[Option<TokenIter>;4]> {
|
||||||
|
let mut name: Option<TokenIter> = None;
|
||||||
|
let mut info: Option<TokenIter> = None;
|
||||||
|
let mut view: Option<TokenIter> = None;
|
||||||
|
let mut keys: Option<TokenIter> = None;
|
||||||
|
for token in iter {
|
||||||
|
match token.value {
|
||||||
|
Value::Exp(_, mut exp) => {
|
||||||
|
let next = exp.next();
|
||||||
|
match next {
|
||||||
|
Some(Token { value: Value::Key(sym), .. }) => match sym {
|
||||||
|
"name" => name = Some(exp),
|
||||||
|
"info" => info = Some(exp),
|
||||||
|
"keys" => keys = Some(exp),
|
||||||
|
"view" => view = Some(exp),
|
||||||
|
_ => return Err(
|
||||||
|
format!("(e3) unexpected symbol {sym:?}").into()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
_ => return Err(
|
||||||
|
format!("(e2) unexpected exp {:?}", next.map(|x|x.value)).into()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
t => return Err(
|
||||||
|
format!("(e1) unexpected token {token:?}").into()
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Ok([name, info, view, keys])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_info (mut iter: TokenIter) -> Option<Arc<str>> {
|
||||||
|
iter.next().and_then(|x|if let Value::Str(x) = x.value {
|
||||||
|
Some(x.into())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_name (mut iter: TokenIter) -> Option<Arc<str>> {
|
||||||
|
iter.next().and_then(|x|if let Value::Str(x) = x.value {
|
||||||
|
Some(x.into())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_view (iter: Option<TokenIter>) -> Usually<TokenIter> {
|
||||||
|
if let Some(view) = iter {
|
||||||
|
Ok(view)
|
||||||
|
} else {
|
||||||
|
Err(format!("missing view definition").into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_keys (base: &impl AsRef<Path>, iter: Option<TokenIter>)
|
||||||
|
-> Usually<InputMap<'static, Tek, TekCommand, TuiIn, TokenIter<'static>>>
|
||||||
|
{
|
||||||
|
if let Some(mut keys) = iter {
|
||||||
|
let mut map = InputMap::default();
|
||||||
|
while let Some(token) = keys.next() {
|
||||||
|
match token.value {
|
||||||
|
Value::Exp(_, mut exp) => {
|
||||||
|
let next = exp.next();
|
||||||
|
match next {
|
||||||
|
Some(Token { value: Value::Key(sym), .. }) => match sym {
|
||||||
|
"layer" => {
|
||||||
|
let next = exp.next();
|
||||||
|
match next {
|
||||||
|
Some(Token { value: Value::Str(path), .. }) => {
|
||||||
|
map.add_layer(read_and_leak(path)?.into());
|
||||||
|
},
|
||||||
|
_ => return Err(
|
||||||
|
format!("(e4) unexpected non-string {next:?}").into()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
todo!()
|
||||||
|
},
|
||||||
|
"layer-if" => {
|
||||||
|
let next = exp.next();
|
||||||
|
match next {
|
||||||
|
Some(Token { value: Value::Sym(sym), .. }) => {
|
||||||
|
todo!()
|
||||||
|
},
|
||||||
|
_ => return Err(
|
||||||
|
format!("(e4) unexpected non-symbol {next:?}").into()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
let next = exp.next();
|
||||||
|
match next {
|
||||||
|
Some(Token { value: Value::Str(path), .. }) => {
|
||||||
|
todo!()
|
||||||
|
},
|
||||||
|
_ => return Err(
|
||||||
|
format!("(e4) unexpected non-symbol {next:?}").into()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
todo!()
|
||||||
|
},
|
||||||
|
_ => return Err(
|
||||||
|
format!("(e3) unexpected symbol {sym:?}").into()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => return Err(
|
||||||
|
format!("(e2) unexpected exp {:?}", next.map(|x|x.value)).into()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
t => return Err(
|
||||||
|
format!("(e1) unexpected token {token:?}").into()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(map)
|
||||||
|
} else {
|
||||||
|
return Err(format!("missing keys definition").into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_and_leak (path: impl AsRef<Path>) -> Usually<&'static str> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
let text = String::from_utf8(std::fs::read(path)?)?;
|
||||||
|
let text: Box<str> = text.into();
|
||||||
|
let text: &'static str = Box::leak(text);
|
||||||
|
Ok(text)
|
||||||
|
}
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
mod editor_api;
|
|
||||||
mod editor_model;
|
|
||||||
mod editor_view;
|
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
provide!(bool: |self: MidiEditor| {
|
|
||||||
":true" => true,
|
|
||||||
":false" => false,
|
|
||||||
":time-lock" => self.time_lock().get(),
|
|
||||||
":time-lock-toggle" => !self.time_lock().get(),
|
|
||||||
});
|
|
||||||
|
|
||||||
provide!(usize: |self: MidiEditor| {
|
|
||||||
":note-length" => self.note_len(),
|
|
||||||
|
|
||||||
":note-pos" => self.note_pos(),
|
|
||||||
":note-pos-next" => self.note_pos() + 1,
|
|
||||||
":note-pos-prev" => self.note_pos().saturating_sub(1),
|
|
||||||
":note-pos-next-octave" => self.note_pos() + 12,
|
|
||||||
":note-pos-prev-octave" => self.note_pos().saturating_sub(12),
|
|
||||||
|
|
||||||
":note-len" => self.note_len(),
|
|
||||||
":note-len-next" => self.note_len() + 1,
|
|
||||||
":note-len-prev" => self.note_len().saturating_sub(1),
|
|
||||||
|
|
||||||
":note-range" => self.note_axis().get(),
|
|
||||||
":note-range-prev" => self.note_axis().get() + 1,
|
|
||||||
":note-range-next" => self.note_axis().get().saturating_sub(1),
|
|
||||||
|
|
||||||
":time-pos" => self.time_pos(),
|
|
||||||
":time-pos-next" => self.time_pos() + self.time_zoom().get(),
|
|
||||||
":time-pos-prev" => self.time_pos().saturating_sub(self.time_zoom().get()),
|
|
||||||
|
|
||||||
":time-zoom" => self.time_zoom().get(),
|
|
||||||
":time-zoom-next" => self.time_zoom().get() + 1,
|
|
||||||
":time-zoom-prev" => self.time_zoom().get().saturating_sub(1).max(1),
|
|
||||||
});
|
|
||||||
|
|
||||||
atom_command!(MidiEditCommand: |state: MidiEditor| {
|
|
||||||
("note/append" [] Some(Self::AppendNote))
|
|
||||||
("note/put" [] Some(Self::PutNote))
|
|
||||||
("note/del" [] Some(Self::DelNote))
|
|
||||||
("note/pos" [a: usize] Some(Self::SetNoteCursor(a.expect("no note cursor"))))
|
|
||||||
("note/len" [a: usize] Some(Self::SetNoteLength(a.expect("no note length"))))
|
|
||||||
("time/pos" [a: usize] Some(Self::SetTimeCursor(a.expect("no time cursor"))))
|
|
||||||
("time/zoom" [a: usize] Some(Self::SetTimeZoom(a.expect("no time zoom"))))
|
|
||||||
("time/lock" [a: bool] Some(Self::SetTimeLock(a.expect("no time lock"))))
|
|
||||||
("time/lock" [] Some(Self::SetTimeLock(!state.time_lock().get())))
|
|
||||||
});
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)] pub enum MidiEditCommand {
|
|
||||||
// TODO: 1-9 seek markers that by default start every 8th of the clip
|
|
||||||
AppendNote,
|
|
||||||
PutNote,
|
|
||||||
DelNote,
|
|
||||||
SetNoteCursor(usize),
|
|
||||||
SetNoteLength(usize),
|
|
||||||
SetNoteScroll(usize),
|
|
||||||
SetTimeCursor(usize),
|
|
||||||
SetTimeScroll(usize),
|
|
||||||
SetTimeZoom(usize),
|
|
||||||
SetTimeLock(bool),
|
|
||||||
Show(Option<Arc<RwLock<MidiClip>>>),
|
|
||||||
}
|
|
||||||
|
|
||||||
handle!(TuiIn: |self: MidiEditor, input|Ok(if let Some(command) = self.keys.command(self, input) {
|
|
||||||
command.execute(self)?;
|
|
||||||
Some(true)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}));
|
|
||||||
|
|
||||||
impl Command<MidiEditor> for MidiEditCommand {
|
|
||||||
fn execute (self, state: &mut MidiEditor) -> Perhaps<Self> {
|
|
||||||
use MidiEditCommand::*;
|
|
||||||
match self {
|
|
||||||
Show(clip) => { state.set_clip(clip.as_ref()); },
|
|
||||||
DelNote => {},
|
|
||||||
PutNote => { state.put_note(false); },
|
|
||||||
AppendNote => { state.put_note(true); },
|
|
||||||
SetTimeZoom(x) => { state.time_zoom().set(x); state.redraw(); },
|
|
||||||
SetTimeLock(x) => { state.time_lock().set(x); },
|
|
||||||
SetTimeScroll(x) => { state.time_start().set(x); },
|
|
||||||
SetNoteScroll(x) => { state.note_lo().set(x.min(127)); },
|
|
||||||
SetNoteLength(x) => {
|
|
||||||
let note_len = state.note_len();
|
|
||||||
let time_zoom = state.time_zoom().get();
|
|
||||||
state.set_note_len(x);
|
|
||||||
//if note_len / time_zoom != x / time_zoom {
|
|
||||||
state.redraw();
|
|
||||||
//}
|
|
||||||
},
|
|
||||||
SetTimeCursor(x) => { state.set_time_pos(x); },
|
|
||||||
SetNoteCursor(note) => { state.set_note_pos(note.min(127)); },
|
|
||||||
//_ => todo!("{:?}", self)
|
|
||||||
}
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,179 +0,0 @@
|
||||||
//! MIDI editor.
|
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
/// Contains state for viewing and editing a clip
|
|
||||||
pub struct MidiEditor {
|
|
||||||
/// Size of editor on screen
|
|
||||||
pub size: Measure<TuiOut>,
|
|
||||||
/// View mode and state of editor
|
|
||||||
pub mode: PianoHorizontal,
|
|
||||||
/// Input keymap
|
|
||||||
pub keys: InputMap<'static, Self, MidiEditCommand, TuiIn, SourceIter<'static>>
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Debug for MidiEditor {
|
|
||||||
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
|
||||||
f.debug_struct("MidiEditor")
|
|
||||||
.field("mode", &self.mode)
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for MidiEditor {
|
|
||||||
fn default () -> Self {
|
|
||||||
Self {
|
|
||||||
size: Measure::new(),
|
|
||||||
mode: PianoHorizontal::new(None),
|
|
||||||
keys: InputMap::new(SourceIter(include_str!("../../../../config/keys_editor.edn"))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
has_size!(<TuiOut>|self: MidiEditor|&self.size);
|
|
||||||
|
|
||||||
content!(TuiOut: |self: MidiEditor| {
|
|
||||||
self.autoscroll();
|
|
||||||
//self.autozoom();
|
|
||||||
self.size.of(&self.mode)
|
|
||||||
});
|
|
||||||
|
|
||||||
from!(|clip: &Arc<RwLock<MidiClip>>|MidiEditor = {
|
|
||||||
let model = Self::from(Some(clip.clone()));
|
|
||||||
model.redraw();
|
|
||||||
model
|
|
||||||
});
|
|
||||||
|
|
||||||
from!(|clip: Option<Arc<RwLock<MidiClip>>>|MidiEditor = {
|
|
||||||
let mut model = Self::default();
|
|
||||||
*model.clip_mut() = clip;
|
|
||||||
model.redraw();
|
|
||||||
model
|
|
||||||
});
|
|
||||||
|
|
||||||
impl MidiEditor {
|
|
||||||
|
|
||||||
/// Put note at current position
|
|
||||||
pub fn put_note (&mut self, advance: bool) {
|
|
||||||
let mut redraw = false;
|
|
||||||
if let Some(clip) = self.clip() {
|
|
||||||
let mut clip = clip.write().unwrap();
|
|
||||||
let note_start = self.time_pos();
|
|
||||||
let note_pos = self.note_pos();
|
|
||||||
let note_len = self.note_len();
|
|
||||||
let note_end = note_start + (note_len.saturating_sub(1));
|
|
||||||
let key: u7 = u7::from(note_pos as u8);
|
|
||||||
let vel: u7 = 100.into();
|
|
||||||
let length = clip.length;
|
|
||||||
let note_end = note_end % length;
|
|
||||||
let note_on = MidiMessage::NoteOn { key, vel };
|
|
||||||
if !clip.notes[note_start].iter().any(|msg|*msg == note_on) {
|
|
||||||
clip.notes[note_start].push(note_on);
|
|
||||||
}
|
|
||||||
let note_off = MidiMessage::NoteOff { key, vel };
|
|
||||||
if !clip.notes[note_end].iter().any(|msg|*msg == note_off) {
|
|
||||||
clip.notes[note_end].push(note_off);
|
|
||||||
}
|
|
||||||
if advance {
|
|
||||||
self.set_time_pos(note_end);
|
|
||||||
}
|
|
||||||
redraw = true;
|
|
||||||
}
|
|
||||||
if redraw {
|
|
||||||
self.mode.redraw();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clip_status (&self) -> impl Content<TuiOut> + '_ {
|
|
||||||
let (color, name, length, looped) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) {
|
|
||||||
(clip.color, clip.name.clone(), clip.length, clip.looped)
|
|
||||||
} else { (ItemTheme::G[64], String::new().into(), 0, false) };
|
|
||||||
Bsp::e(
|
|
||||||
FieldH(color, "Edit", format!("{name} ({length})")),
|
|
||||||
FieldH(color, "Loop", looped.to_string())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn edit_status (&self) -> impl Content<TuiOut> + '_ {
|
|
||||||
let (color, length) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) {
|
|
||||||
(clip.color, clip.length)
|
|
||||||
} else { (ItemTheme::G[64], 0) };
|
|
||||||
let time_pos = self.time_pos();
|
|
||||||
let time_zoom = self.time_zoom().get();
|
|
||||||
let time_lock = if self.time_lock().get() { "[lock]" } else { " " };
|
|
||||||
let note_pos = format!("{:>3}", self.note_pos());
|
|
||||||
let note_name = format!("{:4}", Note::pitch_to_name(self.note_pos()));
|
|
||||||
let note_len = format!("{:>4}", self.note_len());
|
|
||||||
Bsp::e(
|
|
||||||
FieldH(color, "Time", format!("{length}/{time_zoom}+{time_pos} {time_lock}")),
|
|
||||||
FieldH(color, "Note", format!("{note_name} {note_pos} {note_len}")),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
//fn clip_length (&self) -> usize {
|
|
||||||
//self.clip().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1)
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TimeRange for MidiEditor {
|
|
||||||
fn time_len (&self) -> &AtomicUsize { self.mode.time_len() }
|
|
||||||
fn time_zoom (&self) -> &AtomicUsize { self.mode.time_zoom() }
|
|
||||||
fn time_lock (&self) -> &AtomicBool { self.mode.time_lock() }
|
|
||||||
fn time_start (&self) -> &AtomicUsize { self.mode.time_start() }
|
|
||||||
fn time_axis (&self) -> &AtomicUsize { self.mode.time_axis() }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NoteRange for MidiEditor {
|
|
||||||
fn note_lo (&self) -> &AtomicUsize { self.mode.note_lo() }
|
|
||||||
fn note_axis (&self) -> &AtomicUsize { self.mode.note_axis() }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NotePoint for MidiEditor {
|
|
||||||
fn note_len (&self) -> usize { self.mode.note_len() }
|
|
||||||
fn set_note_len (&self, x: usize) -> usize { self.mode.set_note_len(x) }
|
|
||||||
fn note_pos (&self) -> usize { self.mode.note_pos() }
|
|
||||||
fn set_note_pos (&self, x: usize) -> usize { self.mode.set_note_pos(x) }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TimePoint for MidiEditor {
|
|
||||||
fn time_pos (&self) -> usize { self.mode.time_pos() }
|
|
||||||
fn set_time_pos (&self, x: usize) -> usize { self.mode.set_time_pos(x) }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MidiViewer for MidiEditor {
|
|
||||||
fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { self.mode.buffer_size(clip) }
|
|
||||||
fn redraw (&self) { self.mode.redraw() }
|
|
||||||
fn clip (&self) -> &Option<Arc<RwLock<MidiClip>>> { self.mode.clip() }
|
|
||||||
fn clip_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>> { self.mode.clip_mut() }
|
|
||||||
fn set_clip (&mut self, p: Option<&Arc<RwLock<MidiClip>>>) { self.mode.set_clip(p) }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait HasEditor {
|
|
||||||
fn editor (&self) -> &Option<MidiEditor>;
|
|
||||||
fn editor_mut (&mut self) -> &Option<MidiEditor>;
|
|
||||||
fn is_editing (&self) -> bool { true }
|
|
||||||
fn editor_w (&self) -> usize { 0 }
|
|
||||||
fn editor_h (&self) -> usize { 0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export] macro_rules! has_editor {
|
|
||||||
(|$self:ident: $Struct:ident|{
|
|
||||||
editor = $e0:expr;
|
|
||||||
editor_w = $e1:expr;
|
|
||||||
editor_h = $e2:expr;
|
|
||||||
is_editing = $e3:expr;
|
|
||||||
}) => {
|
|
||||||
impl HasEditor for $Struct {
|
|
||||||
fn editor (&$self) -> &Option<MidiEditor> { &$e0 }
|
|
||||||
fn editor_mut (&mut $self) -> &Option<MidiEditor> { &mut $e0 }
|
|
||||||
fn editor_w (&$self) -> usize { $e1 }
|
|
||||||
fn editor_h (&$self) -> usize { $e2 }
|
|
||||||
fn is_editing (&$self) -> bool { $e3 }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
|
||||||
impl $(<$($L),*$($T $(: $U)?),*>)? HasEditor for $Struct $(<$($L),*$($T),*>)? {
|
|
||||||
fn editor (&$self) -> &MidiEditor { &$cb }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,363 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
use Color::*;
|
|
||||||
|
|
||||||
/// A clip, rendered as a horizontal piano roll.
|
|
||||||
#[derive(Clone)]
|
|
||||||
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: MidiRangeModel,
|
|
||||||
/// The note cursor
|
|
||||||
pub point: MidiPointModel,
|
|
||||||
/// The highlight color palette
|
|
||||||
pub color: ItemTheme,
|
|
||||||
/// Width of the keyboard
|
|
||||||
pub keys_width: u16,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PianoHorizontal {
|
|
||||||
pub fn new (clip: Option<&Arc<RwLock<MidiClip>>>) -> Self {
|
|
||||||
let size = Measure::new();
|
|
||||||
let mut range = MidiRangeModel::from((12, true));
|
|
||||||
range.time_axis = size.x.clone();
|
|
||||||
range.note_axis = size.y.clone();
|
|
||||||
let piano = Self {
|
|
||||||
keys_width: 5,
|
|
||||||
size,
|
|
||||||
range,
|
|
||||||
buffer: RwLock::new(Default::default()).into(),
|
|
||||||
point: MidiPointModel::default(),
|
|
||||||
clip: clip.cloned(),
|
|
||||||
color: clip.as_ref().map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]),
|
|
||||||
};
|
|
||||||
piano.redraw();
|
|
||||||
piano
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn note_y_iter (note_lo: usize, note_hi: usize, y0: u16) -> impl Iterator<Item=(usize, u16, usize)> {
|
|
||||||
(note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n))
|
|
||||||
}
|
|
||||||
|
|
||||||
content!(TuiOut:|self: PianoHorizontal| Tui::bg(Tui::g(40), Bsp::s(
|
|
||||||
Bsp::e(
|
|
||||||
Fixed::x(5, format!("{}x{}", self.size.w(), self.size.h())),
|
|
||||||
self.timeline()
|
|
||||||
),
|
|
||||||
Bsp::e(
|
|
||||||
self.keys(),
|
|
||||||
self.size.of(Tui::bg(Tui::g(32), Bsp::b(
|
|
||||||
Fill::xy(self.notes()),
|
|
||||||
Fill::xy(self.cursor()),
|
|
||||||
)))
|
|
||||||
),
|
|
||||||
)));
|
|
||||||
|
|
||||||
impl PianoHorizontal {
|
|
||||||
/// Draw the piano roll background.
|
|
||||||
///
|
|
||||||
/// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄
|
|
||||||
fn draw_bg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize, note_len: usize) {
|
|
||||||
for (y, note) in (0..=127).rev().enumerate() {
|
|
||||||
for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) {
|
|
||||||
let cell = buf.get_mut(x, y).unwrap();
|
|
||||||
cell.set_bg(clip.color.darkest.rgb);
|
|
||||||
if time % 384 == 0 {
|
|
||||||
cell.set_fg(clip.color.darker.rgb);
|
|
||||||
cell.set_char('│');
|
|
||||||
} else if time % 96 == 0 {
|
|
||||||
cell.set_fg(clip.color.dark.rgb);
|
|
||||||
cell.set_char('╎');
|
|
||||||
} else if time % note_len == 0 {
|
|
||||||
cell.set_fg(clip.color.darker.rgb);
|
|
||||||
cell.set_char('┊');
|
|
||||||
} else if (127 - note) % 12 == 0 {
|
|
||||||
cell.set_fg(clip.color.darker.rgb);
|
|
||||||
cell.set_char('=');
|
|
||||||
} else if (127 - note) % 6 == 0 {
|
|
||||||
cell.set_fg(clip.color.darker.rgb);
|
|
||||||
cell.set_char('—');
|
|
||||||
} else {
|
|
||||||
cell.set_fg(clip.color.darker.rgb);
|
|
||||||
cell.set_char('·');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Draw the piano roll foreground.
|
|
||||||
///
|
|
||||||
/// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄
|
|
||||||
fn draw_fg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize) {
|
|
||||||
let style = Style::default().fg(clip.color.base.rgb);//.bg(Rgb(0, 0, 0));
|
|
||||||
let mut notes_on = [false;128];
|
|
||||||
for (x, time_start) in (0..clip.length).step_by(zoom).enumerate() {
|
|
||||||
for (_y, note) in (0..=127).rev().enumerate() {
|
|
||||||
if let Some(cell) = buf.get_mut(x, note) {
|
|
||||||
if notes_on[note] {
|
|
||||||
cell.set_char('▂');
|
|
||||||
cell.set_style(style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let time_end = time_start + zoom;
|
|
||||||
for time in time_start..time_end.min(clip.length) {
|
|
||||||
for event in clip.notes[time].iter() {
|
|
||||||
match event {
|
|
||||||
MidiMessage::NoteOn { key, .. } => {
|
|
||||||
let note = key.as_int() as usize;
|
|
||||||
if let Some(cell) = buf.get_mut(x, note) {
|
|
||||||
cell.set_char('█');
|
|
||||||
cell.set_style(style);
|
|
||||||
}
|
|
||||||
notes_on[note] = true
|
|
||||||
},
|
|
||||||
MidiMessage::NoteOff { key, .. } => {
|
|
||||||
notes_on[key.as_int() as usize] = false
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn notes (&self) -> impl Content<TuiOut> {
|
|
||||||
let time_start = self.time_start().get();
|
|
||||||
let note_lo = self.note_lo().get();
|
|
||||||
let note_hi = self.note_hi();
|
|
||||||
let buffer = self.buffer.clone();
|
|
||||||
ThunkRender::new(move|to: &mut TuiOut|{
|
|
||||||
let source = buffer.read().unwrap();
|
|
||||||
let [x0, y0, w, _h] = to.area().xywh();
|
|
||||||
//if h as usize != note_axis {
|
|
||||||
//panic!("area height mismatch: {h} <> {note_axis}");
|
|
||||||
//}
|
|
||||||
for (area_x, screen_x) in (x0..x0+w).enumerate() {
|
|
||||||
for (area_y, screen_y, _note) in note_y_iter(note_lo, note_hi, y0) {
|
|
||||||
let source_x = time_start + area_x;
|
|
||||||
let source_y = note_hi - area_y;
|
|
||||||
// TODO: enable loop rollover:
|
|
||||||
//let source_x = (time_start + area_x) % source.width.max(1);
|
|
||||||
//let source_y = (note_hi - area_y) % source.height.max(1);
|
|
||||||
let is_in_x = source_x < source.width;
|
|
||||||
let is_in_y = source_y < source.height;
|
|
||||||
if is_in_x && is_in_y {
|
|
||||||
if let Some(source_cell) = source.get(source_x, source_y) {
|
|
||||||
if let Some(cell) = to.buffer.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) {
|
|
||||||
*cell = source_cell.clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
fn cursor (&self) -> impl Content<TuiOut> {
|
|
||||||
let style = Some(Style::default().fg(self.color.lightest.rgb));
|
|
||||||
let note_hi = self.note_hi();
|
|
||||||
let note_lo = self.note_lo().get();
|
|
||||||
let note_pos = self.note_pos();
|
|
||||||
let note_len = self.note_len();
|
|
||||||
let time_pos = self.time_pos();
|
|
||||||
let time_start = self.time_start().get();
|
|
||||||
let time_zoom = self.time_zoom().get();
|
|
||||||
ThunkRender::new(move|to: &mut TuiOut|{
|
|
||||||
let [x0, y0, w, _] = to.area().xywh();
|
|
||||||
for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) {
|
|
||||||
if note == note_pos {
|
|
||||||
for x in 0..w {
|
|
||||||
let screen_x = x0 + x;
|
|
||||||
let time_1 = time_start + x as usize * time_zoom;
|
|
||||||
let time_2 = time_1 + time_zoom;
|
|
||||||
if time_1 <= time_pos && time_pos < time_2 {
|
|
||||||
to.blit(&"█", screen_x, screen_y, style);
|
|
||||||
let tail = note_len as u16 / time_zoom as u16;
|
|
||||||
for x_tail in (screen_x + 1)..(screen_x + tail) {
|
|
||||||
to.blit(&"▂", x_tail, screen_y, style);
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
fn keys (&self) -> impl Content<TuiOut> {
|
|
||||||
let state = self;
|
|
||||||
let color = state.color;
|
|
||||||
let note_lo = state.note_lo().get();
|
|
||||||
let note_hi = state.note_hi();
|
|
||||||
let note_pos = state.note_pos();
|
|
||||||
let key_style = Some(Style::default().fg(Rgb(192, 192, 192)).bg(Rgb(0, 0, 0)));
|
|
||||||
let off_style = Some(Style::default().fg(Tui::g(255)));
|
|
||||||
let on_style = Some(Style::default().fg(Rgb(255,0,0)).bg(color.base.rgb).bold());
|
|
||||||
Fill::y(Fixed::x(self.keys_width, ThunkRender::new(move|to: &mut TuiOut|{
|
|
||||||
let [x, y0, _w, _h] = to.area().xywh();
|
|
||||||
for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) {
|
|
||||||
to.blit(&to_key(note), x, screen_y, key_style);
|
|
||||||
if note > 127 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if note == note_pos {
|
|
||||||
to.blit(&format!("{:<5}", Note::pitch_to_name(note)), x, screen_y, on_style)
|
|
||||||
} else {
|
|
||||||
to.blit(&Note::pitch_to_name(note), x, screen_y, off_style)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
fn timeline (&self) -> impl Content<TuiOut> + '_ {
|
|
||||||
Fill::x(Fixed::y(1, ThunkRender::new(move|to: &mut TuiOut|{
|
|
||||||
let [x, y, w, _h] = to.area();
|
|
||||||
let style = Some(Style::default().dim());
|
|
||||||
let length = self.clip.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1);
|
|
||||||
for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) {
|
|
||||||
let t = area_x as usize * self.time_zoom().get();
|
|
||||||
if t < length {
|
|
||||||
to.blit(&"|", screen_x, y, style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
has_size!(<TuiOut>|self:PianoHorizontal|&self.size);
|
|
||||||
|
|
||||||
impl TimeRange for PianoHorizontal {
|
|
||||||
fn time_len (&self) -> &AtomicUsize { self.range.time_len() }
|
|
||||||
fn time_zoom (&self) -> &AtomicUsize { self.range.time_zoom() }
|
|
||||||
fn time_lock (&self) -> &AtomicBool { self.range.time_lock() }
|
|
||||||
fn time_start (&self) -> &AtomicUsize { self.range.time_start() }
|
|
||||||
fn time_axis (&self) -> &AtomicUsize { self.range.time_axis() }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NoteRange for PianoHorizontal {
|
|
||||||
fn note_lo (&self) -> &AtomicUsize { self.range.note_lo() }
|
|
||||||
fn note_axis (&self) -> &AtomicUsize { self.range.note_axis() }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NotePoint for PianoHorizontal {
|
|
||||||
fn note_len (&self) -> usize { self.point.note_len() }
|
|
||||||
fn set_note_len (&self, x: usize) -> usize { self.point.set_note_len(x) }
|
|
||||||
fn note_pos (&self) -> usize { self.point.note_pos() }
|
|
||||||
fn set_note_pos (&self, x: usize) -> usize { self.point.set_note_pos(x) }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TimePoint for PianoHorizontal {
|
|
||||||
fn time_pos (&self) -> usize { self.point.time_pos() }
|
|
||||||
fn set_time_pos (&self, x: usize) -> usize { self.point.set_time_pos(x) }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MidiViewer for PianoHorizontal {
|
|
||||||
fn clip (&self) -> &Option<Arc<RwLock<MidiClip>>> {
|
|
||||||
&self.clip
|
|
||||||
}
|
|
||||||
fn clip_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>> {
|
|
||||||
&mut self.clip
|
|
||||||
}
|
|
||||||
/// Determine the required space to render the clip.
|
|
||||||
fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) {
|
|
||||||
(clip.length / self.range.time_zoom().get(), 128)
|
|
||||||
}
|
|
||||||
fn redraw (&self) {
|
|
||||||
*self.buffer.write().unwrap() = if let Some(clip) = self.clip.as_ref() {
|
|
||||||
let clip = clip.read().unwrap();
|
|
||||||
let buf_size = self.buffer_size(&clip);
|
|
||||||
let mut buffer = BigBuffer::from(buf_size);
|
|
||||||
let note_len = self.note_len();
|
|
||||||
let time_zoom = self.time_zoom().get();
|
|
||||||
self.time_len().set(clip.length);
|
|
||||||
PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom, note_len);
|
|
||||||
PianoHorizontal::draw_fg(&mut buffer, &clip, time_zoom);
|
|
||||||
buffer
|
|
||||||
} else {
|
|
||||||
Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn set_clip (&mut self, clip: Option<&Arc<RwLock<MidiClip>>>) {
|
|
||||||
*self.clip_mut() = clip.cloned();
|
|
||||||
self.color = clip.map(|p|p.read().unwrap().color)
|
|
||||||
.unwrap_or(ItemTheme::G[64]);
|
|
||||||
self.redraw();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Debug for PianoHorizontal {
|
|
||||||
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
|
||||||
let buffer = self.buffer.read().unwrap();
|
|
||||||
f.debug_struct("PianoHorizontal")
|
|
||||||
.field("time_zoom", &self.range.time_zoom)
|
|
||||||
.field("buffer", &format!("{}x{}", buffer.width, buffer.height))
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Update sequencer playhead indicator
|
|
||||||
//self.now().set(0.);
|
|
||||||
//if let Some((ref started_at, Some(ref playing))) = self.player.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 to_key (note: usize) -> &'static str {
|
|
||||||
match note % 12 {
|
|
||||||
11 => "████▌",
|
|
||||||
10 => " ",
|
|
||||||
9 => "████▌",
|
|
||||||
8 => " ",
|
|
||||||
7 => "████▌",
|
|
||||||
6 => " ",
|
|
||||||
5 => "████▌",
|
|
||||||
4 => "████▌",
|
|
||||||
3 => " ",
|
|
||||||
2 => "████▌",
|
|
||||||
1 => " ",
|
|
||||||
0 => "████▌",
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct OctaveVertical {
|
|
||||||
on: [bool; 12],
|
|
||||||
colors: [Color; 3]
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for OctaveVertical {
|
|
||||||
fn default () -> Self {
|
|
||||||
Self {
|
|
||||||
on: [false; 12],
|
|
||||||
colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OctaveVertical {
|
|
||||||
fn color (&self, pitch: usize) -> Color {
|
|
||||||
let pitch = pitch % 12;
|
|
||||||
self.colors[if self.on[pitch] { 2 } else {
|
|
||||||
match pitch { 0 | 2 | 4 | 5 | 6 | 8 | 10 => 0, _ => 1 }
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Content<TuiOut> for OctaveVertical {
|
|
||||||
fn content (&self) -> impl Render<TuiOut> {
|
|
||||||
row!(
|
|
||||||
Tui::fg_bg(self.color(0), self.color(1), "▙"),
|
|
||||||
Tui::fg_bg(self.color(2), self.color(3), "▙"),
|
|
||||||
Tui::fg_bg(self.color(4), self.color(5), "▌"),
|
|
||||||
Tui::fg_bg(self.color(6), self.color(7), "▟"),
|
|
||||||
Tui::fg_bg(self.color(8), self.color(9), "▟"),
|
|
||||||
Tui::fg_bg(self.color(10), self.color(11), "▟"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -31,16 +31,16 @@ pub use ::tengri::tui::ratatui::prelude::Color::{self, *};
|
||||||
pub use ::tengri::tui::ratatui::prelude::{Style, Stylize, Buffer, Modifier};
|
pub use ::tengri::tui::ratatui::prelude::{Style, Stylize, Buffer, Modifier};
|
||||||
pub use ::tengri::tui::crossterm;
|
pub use ::tengri::tui::crossterm;
|
||||||
pub use ::tengri::tui::crossterm::event::{Event, KeyCode::{self, *}};
|
pub use ::tengri::tui::crossterm::event::{Event, KeyCode::{self, *}};
|
||||||
pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicBool, Ordering::Relaxed}};
|
pub(crate) use std::path::Path;
|
||||||
|
pub(crate) use std::sync::{Arc, RwLock};
|
||||||
|
pub(crate) use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed};
|
||||||
|
|
||||||
mod api; pub use self::api::*;
|
mod api; pub use self::api::*;
|
||||||
mod audio; pub use self::audio::*;
|
mod audio; pub use self::audio::*;
|
||||||
|
mod config; pub use self::config::*;
|
||||||
mod model; pub use self::model::*;
|
mod model; pub use self::model::*;
|
||||||
mod view; pub use self::view::*;
|
mod view; pub use self::view::*;
|
||||||
|
|
||||||
mod pool;
|
|
||||||
mod editor;
|
|
||||||
|
|
||||||
#[cfg(test)] #[test] fn test_model () {
|
#[cfg(test)] #[test] fn test_model () {
|
||||||
let mut tek = Tek::default();
|
let mut tek = Tek::default();
|
||||||
let _ = tek.clip();
|
let _ = tek.clip();
|
||||||
|
|
|
||||||
|
|
@ -48,14 +48,12 @@ pub struct Tek {
|
||||||
pub history: Vec<TekCommand>,
|
pub history: Vec<TekCommand>,
|
||||||
/// Port handles
|
/// Port handles
|
||||||
pub ports: std::collections::BTreeMap<u32, Port<Unowned>>,
|
pub ports: std::collections::BTreeMap<u32, Port<Unowned>>,
|
||||||
/// View definition
|
|
||||||
pub view: SourceIter<'static>,
|
|
||||||
// Cache of formatted strings
|
// Cache of formatted strings
|
||||||
pub view_cache: Arc<RwLock<ViewCache>>,
|
pub view_cache: Arc<RwLock<ViewCache>>,
|
||||||
// Modal overlay
|
// Modal overlay
|
||||||
pub modal: Option<Modal>,
|
pub modal: Option<Modal>,
|
||||||
// Input keymap
|
// View and input definition
|
||||||
pub keys: InputMap<'static, Self, TekCommand, TuiIn, TokenIter<'static>>
|
pub config: Configuration
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tek {
|
impl Tek {
|
||||||
|
|
@ -442,11 +440,11 @@ pub enum Selection {
|
||||||
/// A track is selected.
|
/// A track is selected.
|
||||||
Track(usize),
|
Track(usize),
|
||||||
/// A clip (track × scene) is selected.
|
/// A clip (track × scene) is selected.
|
||||||
TrackClip { track: usize, scene: usize },
|
TrackClip { track: usize, scene: usize },
|
||||||
/// A track's MIDI input connection is selected.
|
/// A track's MIDI input connection is selected.
|
||||||
TrackInput { track: usize, port: usize },
|
TrackInput { track: usize, port: usize },
|
||||||
/// A track's MIDI output connection is selected.
|
/// A track's MIDI output connection is selected.
|
||||||
TrackOutput { track: usize, port: usize },
|
TrackOutput { track: usize, port: usize },
|
||||||
/// A track device slot is selected.
|
/// A track device slot is selected.
|
||||||
TrackDevice { track: usize, device: usize },
|
TrackDevice { track: usize, device: usize },
|
||||||
}
|
}
|
||||||
|
|
@ -516,7 +514,7 @@ impl Selection {
|
||||||
pub fn scene_next (&self, len: usize) -> Self {
|
pub fn scene_next (&self, len: usize) -> Self {
|
||||||
use Selection::*;
|
use Selection::*;
|
||||||
match self {
|
match self {
|
||||||
Mix => Scene(0),
|
Mix => Scene(0),
|
||||||
Track(t) => TrackClip { track: *t, scene: 0 },
|
Track(t) => TrackClip { track: *t, scene: 0 },
|
||||||
Scene(s) => if s + 1 < len {
|
Scene(s) => if s + 1 < len {
|
||||||
Scene(s + 1)
|
Scene(s + 1)
|
||||||
|
|
@ -546,9 +544,11 @@ impl Selection {
|
||||||
use Selection::*;
|
use Selection::*;
|
||||||
format!("{}", match self {
|
format!("{}", match self {
|
||||||
Mix => "Everything".to_string(),
|
Mix => "Everything".to_string(),
|
||||||
Scene(s) => scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name))
|
Scene(s) => scenes.get(*s)
|
||||||
|
.map(|scene|format!("S{s}: {}", &scene.name))
|
||||||
.unwrap_or_else(||"S??".into()),
|
.unwrap_or_else(||"S??".into()),
|
||||||
Track(t) => tracks.get(*t).map(|track|format!("T{t}: {}", &track.name))
|
Track(t) => tracks.get(*t)
|
||||||
|
.map(|track|format!("T{t}: {}", &track.name))
|
||||||
.unwrap_or_else(||"T??".into()),
|
.unwrap_or_else(||"T??".into()),
|
||||||
TrackClip { track, scene } => match (tracks.get(*track), scenes.get(*scene)) {
|
TrackClip { track, scene } => match (tracks.get(*track), scenes.get(*scene)) {
|
||||||
(Some(_), Some(s)) => match s.clip(*track) {
|
(Some(_), Some(s)) => match s.clip(*track) {
|
||||||
|
|
@ -787,3 +787,374 @@ impl HasTracks for Tek {
|
||||||
fn tracks (&self) -> &Vec<Track> { &self.tracks }
|
fn tracks (&self) -> &Vec<Track> { &self.tracks }
|
||||||
fn tracks_mut (&mut self) -> &mut Vec<Track> { &mut self.tracks }
|
fn tracks_mut (&mut self) -> &mut Vec<Track> { &mut self.tracks }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct MidiPool {
|
||||||
|
pub visible: bool,
|
||||||
|
/// Collection of clips
|
||||||
|
pub clips: Arc<RwLock<Vec<Arc<RwLock<MidiClip>>>>>,
|
||||||
|
/// Selected clip
|
||||||
|
pub clip: AtomicUsize,
|
||||||
|
/// Mode switch
|
||||||
|
pub mode: Option<PoolMode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MidiPool {
|
||||||
|
fn default () -> Self {
|
||||||
|
use PoolMode::*;
|
||||||
|
Self {
|
||||||
|
visible: true,
|
||||||
|
clips: Arc::from(RwLock::from(vec![])),
|
||||||
|
clip: 0.into(),
|
||||||
|
mode: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
has_clips!(|self: MidiPool|self.clips);
|
||||||
|
|
||||||
|
has_clip!(|self: MidiPool|self.clips().get(self.clip_index()).map(|c|c.clone()));
|
||||||
|
|
||||||
|
from!(|clip:&Arc<RwLock<MidiClip>>|MidiPool = {
|
||||||
|
let model = Self::default();
|
||||||
|
model.clips.write().unwrap().push(clip.clone());
|
||||||
|
model.clip.store(1, Relaxed);
|
||||||
|
model
|
||||||
|
});
|
||||||
|
|
||||||
|
impl MidiPool {
|
||||||
|
pub fn clip_index (&self) -> usize {
|
||||||
|
self.clip.load(Relaxed)
|
||||||
|
}
|
||||||
|
pub fn set_clip_index (&self, value: usize) {
|
||||||
|
self.clip.store(value, Relaxed);
|
||||||
|
}
|
||||||
|
pub fn mode (&self) -> &Option<PoolMode> {
|
||||||
|
&self.mode
|
||||||
|
}
|
||||||
|
pub fn mode_mut (&mut self) -> &mut Option<PoolMode> {
|
||||||
|
&mut self.mode
|
||||||
|
}
|
||||||
|
pub fn begin_clip_length (&mut self) {
|
||||||
|
let length = self.clips()[self.clip_index()].read().unwrap().length;
|
||||||
|
*self.mode_mut() = Some(PoolMode::Length(
|
||||||
|
self.clip_index(),
|
||||||
|
length,
|
||||||
|
ClipLengthFocus::Bar
|
||||||
|
));
|
||||||
|
}
|
||||||
|
pub fn begin_clip_rename (&mut self) {
|
||||||
|
let name = self.clips()[self.clip_index()].read().unwrap().name.clone();
|
||||||
|
*self.mode_mut() = Some(PoolMode::Rename(
|
||||||
|
self.clip_index(),
|
||||||
|
name
|
||||||
|
));
|
||||||
|
}
|
||||||
|
pub fn begin_import (&mut self) -> Usually<()> {
|
||||||
|
*self.mode_mut() = Some(PoolMode::Import(
|
||||||
|
self.clip_index(),
|
||||||
|
FileBrowser::new(None)?
|
||||||
|
));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn begin_export (&mut self) -> Usually<()> {
|
||||||
|
*self.mode_mut() = Some(PoolMode::Export(
|
||||||
|
self.clip_index(),
|
||||||
|
FileBrowser::new(None)?
|
||||||
|
));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn new_clip (&self) -> MidiClip {
|
||||||
|
MidiClip::new("Clip", true, 4 * PPQ, None, Some(ItemTheme::random()))
|
||||||
|
}
|
||||||
|
pub fn cloned_clip (&self) -> MidiClip {
|
||||||
|
let index = self.clip_index();
|
||||||
|
let mut clip = self.clips()[index].read().unwrap().duplicate();
|
||||||
|
clip.color = ItemTheme::random_near(clip.color, 0.25);
|
||||||
|
clip
|
||||||
|
}
|
||||||
|
pub fn add_new_clip (&self) -> (usize, Arc<RwLock<MidiClip>>) {
|
||||||
|
let clip = Arc::new(RwLock::new(self.new_clip()));
|
||||||
|
let index = {
|
||||||
|
let mut clips = self.clips.write().unwrap();
|
||||||
|
clips.push(clip.clone());
|
||||||
|
clips.len().saturating_sub(1)
|
||||||
|
};
|
||||||
|
self.clip.store(index, Relaxed);
|
||||||
|
(index, clip)
|
||||||
|
}
|
||||||
|
pub fn delete_clip (&mut self, clip: &MidiClip) -> bool {
|
||||||
|
let index = self.clips.read().unwrap().iter().position(|x|*x.read().unwrap()==*clip);
|
||||||
|
if let Some(index) = index {
|
||||||
|
self.clips.write().unwrap().remove(index);
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Modes for clip pool
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum PoolMode {
|
||||||
|
/// Renaming a pattern
|
||||||
|
Rename(usize, Arc<str>),
|
||||||
|
/// Editing the length of a pattern
|
||||||
|
Length(usize, usize, ClipLengthFocus),
|
||||||
|
/// Load clip from disk
|
||||||
|
Import(usize, FileBrowser),
|
||||||
|
/// Save clip to disk
|
||||||
|
Export(usize, FileBrowser),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Focused field of `ClipLength`
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub enum ClipLengthFocus {
|
||||||
|
/// Editing the number of bars
|
||||||
|
Bar,
|
||||||
|
/// Editing the number of beats
|
||||||
|
Beat,
|
||||||
|
/// Editing the number of ticks
|
||||||
|
Tick,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClipLengthFocus {
|
||||||
|
pub fn next (&mut self) {
|
||||||
|
use ClipLengthFocus::*;
|
||||||
|
*self = match self { Bar => Beat, Beat => Tick, Tick => Bar, }
|
||||||
|
}
|
||||||
|
pub fn prev (&mut self) {
|
||||||
|
use ClipLengthFocus::*;
|
||||||
|
*self = match self { Bar => Tick, Beat => Bar, Tick => Beat, }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Displays and edits clip length.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ClipLength {
|
||||||
|
/// Pulses per beat (quaver)
|
||||||
|
ppq: usize,
|
||||||
|
/// Beats per bar
|
||||||
|
bpb: usize,
|
||||||
|
/// Length of clip in pulses
|
||||||
|
pulses: usize,
|
||||||
|
/// Selected subdivision
|
||||||
|
pub focus: Option<ClipLengthFocus>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClipLength {
|
||||||
|
pub fn _new (pulses: usize, focus: Option<ClipLengthFocus>) -> Self {
|
||||||
|
Self { ppq: PPQ, bpb: 4, pulses, focus }
|
||||||
|
}
|
||||||
|
pub fn bars (&self) -> usize {
|
||||||
|
self.pulses / (self.bpb * self.ppq)
|
||||||
|
}
|
||||||
|
pub fn beats (&self) -> usize {
|
||||||
|
(self.pulses % (self.bpb * self.ppq)) / self.ppq
|
||||||
|
}
|
||||||
|
pub fn ticks (&self) -> usize {
|
||||||
|
self.pulses % self.ppq
|
||||||
|
}
|
||||||
|
pub fn bars_string (&self) -> Arc<str> {
|
||||||
|
format!("{}", self.bars()).into()
|
||||||
|
}
|
||||||
|
pub fn beats_string (&self) -> Arc<str> {
|
||||||
|
format!("{}", self.beats()).into()
|
||||||
|
}
|
||||||
|
pub fn ticks_string (&self) -> Arc<str> {
|
||||||
|
format!("{:>02}", self.ticks()).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type ClipPool = Vec<Arc<RwLock<MidiClip>>>;
|
||||||
|
|
||||||
|
pub trait HasClips {
|
||||||
|
fn clips <'a> (&'a self) -> std::sync::RwLockReadGuard<'a, ClipPool>;
|
||||||
|
fn clips_mut <'a> (&'a self) -> std::sync::RwLockWriteGuard<'a, ClipPool>;
|
||||||
|
fn add_clip (&self) -> (usize, Arc<RwLock<MidiClip>>) {
|
||||||
|
let clip = Arc::new(RwLock::new(MidiClip::new("Clip", true, 384, None, None)));
|
||||||
|
self.clips_mut().push(clip.clone());
|
||||||
|
(self.clips().len() - 1, clip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export] macro_rules! has_clips {
|
||||||
|
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||||||
|
impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? {
|
||||||
|
fn clips <'a> (&'a $self) -> std::sync::RwLockReadGuard<'a, ClipPool> {
|
||||||
|
$cb.read().unwrap()
|
||||||
|
}
|
||||||
|
fn clips_mut <'a> (&'a $self) -> std::sync::RwLockWriteGuard<'a, ClipPool> {
|
||||||
|
$cb.write().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait HasEditor {
|
||||||
|
fn editor (&self) -> &Option<MidiEditor>;
|
||||||
|
fn editor_mut (&mut self) -> &Option<MidiEditor>;
|
||||||
|
fn is_editing (&self) -> bool { true }
|
||||||
|
fn editor_w (&self) -> usize { 0 }
|
||||||
|
fn editor_h (&self) -> usize { 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export] macro_rules! has_editor {
|
||||||
|
(|$self:ident: $Struct:ident|{
|
||||||
|
editor = $e0:expr;
|
||||||
|
editor_w = $e1:expr;
|
||||||
|
editor_h = $e2:expr;
|
||||||
|
is_editing = $e3:expr;
|
||||||
|
}) => {
|
||||||
|
impl HasEditor for $Struct {
|
||||||
|
fn editor (&$self) -> &Option<MidiEditor> { &$e0 }
|
||||||
|
fn editor_mut (&mut $self) -> &Option<MidiEditor> { &mut $e0 }
|
||||||
|
fn editor_w (&$self) -> usize { $e1 }
|
||||||
|
fn editor_h (&$self) -> usize { $e2 }
|
||||||
|
fn is_editing (&$self) -> bool { $e3 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||||||
|
impl $(<$($L),*$($T $(: $U)?),*>)? HasEditor for $Struct $(<$($L),*$($T),*>)? {
|
||||||
|
fn editor (&$self) -> &MidiEditor { &$cb }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Contains state for viewing and editing a clip
|
||||||
|
pub struct MidiEditor {
|
||||||
|
/// Size of editor on screen
|
||||||
|
pub size: Measure<TuiOut>,
|
||||||
|
/// View mode and state of editor
|
||||||
|
pub mode: PianoHorizontal,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for MidiEditor {
|
||||||
|
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||||
|
f.debug_struct("MidiEditor")
|
||||||
|
.field("mode", &self.mode)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MidiEditor {
|
||||||
|
fn default () -> Self {
|
||||||
|
Self {
|
||||||
|
size: Measure::new(),
|
||||||
|
mode: PianoHorizontal::new(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
has_size!(<TuiOut>|self: MidiEditor|&self.size);
|
||||||
|
|
||||||
|
content!(TuiOut: |self: MidiEditor| {
|
||||||
|
self.autoscroll();
|
||||||
|
//self.autozoom();
|
||||||
|
self.size.of(&self.mode)
|
||||||
|
});
|
||||||
|
|
||||||
|
from!(|clip: &Arc<RwLock<MidiClip>>|MidiEditor = {
|
||||||
|
let model = Self::from(Some(clip.clone()));
|
||||||
|
model.redraw();
|
||||||
|
model
|
||||||
|
});
|
||||||
|
|
||||||
|
from!(|clip: Option<Arc<RwLock<MidiClip>>>|MidiEditor = {
|
||||||
|
let mut model = Self::default();
|
||||||
|
*model.clip_mut() = clip;
|
||||||
|
model.redraw();
|
||||||
|
model
|
||||||
|
});
|
||||||
|
|
||||||
|
impl MidiEditor {
|
||||||
|
/// Put note at current position
|
||||||
|
pub fn put_note (&mut self, advance: bool) {
|
||||||
|
let mut redraw = false;
|
||||||
|
if let Some(clip) = self.clip() {
|
||||||
|
let mut clip = clip.write().unwrap();
|
||||||
|
let note_start = self.time_pos();
|
||||||
|
let note_pos = self.note_pos();
|
||||||
|
let note_len = self.note_len();
|
||||||
|
let note_end = note_start + (note_len.saturating_sub(1));
|
||||||
|
let key: u7 = u7::from(note_pos as u8);
|
||||||
|
let vel: u7 = 100.into();
|
||||||
|
let length = clip.length;
|
||||||
|
let note_end = note_end % length;
|
||||||
|
let note_on = MidiMessage::NoteOn { key, vel };
|
||||||
|
if !clip.notes[note_start].iter().any(|msg|*msg == note_on) {
|
||||||
|
clip.notes[note_start].push(note_on);
|
||||||
|
}
|
||||||
|
let note_off = MidiMessage::NoteOff { key, vel };
|
||||||
|
if !clip.notes[note_end].iter().any(|msg|*msg == note_off) {
|
||||||
|
clip.notes[note_end].push(note_off);
|
||||||
|
}
|
||||||
|
if advance {
|
||||||
|
self.set_time_pos(note_end);
|
||||||
|
}
|
||||||
|
redraw = true;
|
||||||
|
}
|
||||||
|
if redraw {
|
||||||
|
self.mode.redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clip_status (&self) -> impl Content<TuiOut> + '_ {
|
||||||
|
let (color, name, length, looped) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) {
|
||||||
|
(clip.color, clip.name.clone(), clip.length, clip.looped)
|
||||||
|
} else { (ItemTheme::G[64], String::new().into(), 0, false) };
|
||||||
|
Bsp::e(
|
||||||
|
FieldH(color, "Edit", format!("{name} ({length})")),
|
||||||
|
FieldH(color, "Loop", looped.to_string())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn edit_status (&self) -> impl Content<TuiOut> + '_ {
|
||||||
|
let (color, length) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) {
|
||||||
|
(clip.color, clip.length)
|
||||||
|
} else { (ItemTheme::G[64], 0) };
|
||||||
|
let time_pos = self.time_pos();
|
||||||
|
let time_zoom = self.time_zoom().get();
|
||||||
|
let time_lock = if self.time_lock().get() { "[lock]" } else { " " };
|
||||||
|
let note_pos = format!("{:>3}", self.note_pos());
|
||||||
|
let note_name = format!("{:4}", Note::pitch_to_name(self.note_pos()));
|
||||||
|
let note_len = format!("{:>4}", self.note_len());
|
||||||
|
Bsp::e(
|
||||||
|
FieldH(color, "Time", format!("{length}/{time_zoom}+{time_pos} {time_lock}")),
|
||||||
|
FieldH(color, "Note", format!("{note_name} {note_pos} {note_len}")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimeRange for MidiEditor {
|
||||||
|
fn time_len (&self) -> &AtomicUsize { self.mode.time_len() }
|
||||||
|
fn time_zoom (&self) -> &AtomicUsize { self.mode.time_zoom() }
|
||||||
|
fn time_lock (&self) -> &AtomicBool { self.mode.time_lock() }
|
||||||
|
fn time_start (&self) -> &AtomicUsize { self.mode.time_start() }
|
||||||
|
fn time_axis (&self) -> &AtomicUsize { self.mode.time_axis() }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NoteRange for MidiEditor {
|
||||||
|
fn note_lo (&self) -> &AtomicUsize { self.mode.note_lo() }
|
||||||
|
fn note_axis (&self) -> &AtomicUsize { self.mode.note_axis() }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NotePoint for MidiEditor {
|
||||||
|
fn note_len (&self) -> usize { self.mode.note_len() }
|
||||||
|
fn set_note_len (&self, x: usize) -> usize { self.mode.set_note_len(x) }
|
||||||
|
fn note_pos (&self) -> usize { self.mode.note_pos() }
|
||||||
|
fn set_note_pos (&self, x: usize) -> usize { self.mode.set_note_pos(x) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimePoint for MidiEditor {
|
||||||
|
fn time_pos (&self) -> usize { self.mode.time_pos() }
|
||||||
|
fn set_time_pos (&self, x: usize) -> usize { self.mode.set_time_pos(x) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MidiViewer for MidiEditor {
|
||||||
|
fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { self.mode.buffer_size(clip) }
|
||||||
|
fn redraw (&self) { self.mode.redraw() }
|
||||||
|
fn clip (&self) -> &Option<Arc<RwLock<MidiClip>>> { self.mode.clip() }
|
||||||
|
fn clip_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>> { self.mode.clip_mut() }
|
||||||
|
fn set_clip (&mut self, p: Option<&Arc<RwLock<MidiClip>>>) { self.mode.set_clip(p) }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
mod pool_api; pub use self::pool_api::*;
|
|
||||||
mod pool_clips; pub use self::pool_clips::*;
|
|
||||||
mod pool_model; pub use self::pool_model::*;
|
|
||||||
mod pool_view; pub use self::pool_view::*;
|
|
||||||
|
|
@ -1,314 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
handle!(TuiIn: |self: MidiPool, input|{
|
|
||||||
//Ok(if let Some(command) = match self.mode() {
|
|
||||||
//Some(PoolMode::Rename(..)) => self.keys_rename,
|
|
||||||
//Some(PoolMode::Length(..)) => self.keys_length,
|
|
||||||
//Some(PoolMode::Import(..)) | Some(PoolMode::Export(..)) => self.keys_file,
|
|
||||||
//_ => self.keys
|
|
||||||
//}.command::<Self, PoolCommand, TuiIn>(self, input) {
|
|
||||||
Ok(if let Some(command) = self.keys.command(self, input) {
|
|
||||||
let _undo = command.execute(self)?;
|
|
||||||
Some(true)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
provide!(bool: |self: MidiPool| {});
|
|
||||||
|
|
||||||
provide!(MidiClip: |self: MidiPool| {
|
|
||||||
":new-clip" => self.new_clip(),
|
|
||||||
":cloned-clip" => self.cloned_clip(),
|
|
||||||
});
|
|
||||||
|
|
||||||
provide!(PathBuf: |self: MidiPool| {});
|
|
||||||
|
|
||||||
provide!(Arc<str>: |self: MidiPool| {});
|
|
||||||
|
|
||||||
provide!(usize: |self: MidiPool| {
|
|
||||||
":current" => 0,
|
|
||||||
":after" => 0,
|
|
||||||
":previous" => 0,
|
|
||||||
":next" => 0
|
|
||||||
});
|
|
||||||
|
|
||||||
provide!(ItemColor: |self: MidiPool| {
|
|
||||||
":random-color" => ItemColor::random()
|
|
||||||
});
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Debug)] pub enum PoolCommand {
|
|
||||||
/// Toggle visibility of pool
|
|
||||||
Show(bool),
|
|
||||||
/// Select a clip from the clip pool
|
|
||||||
Select(usize),
|
|
||||||
/// Rename a clip
|
|
||||||
Rename(ClipRenameCommand),
|
|
||||||
/// Change the length of a clip
|
|
||||||
Length(ClipLengthCommand),
|
|
||||||
/// Import from file
|
|
||||||
Import(FileBrowserCommand),
|
|
||||||
/// Export to file
|
|
||||||
Export(FileBrowserCommand),
|
|
||||||
/// Update the contents of the clip pool
|
|
||||||
Clip(PoolClipCommand),
|
|
||||||
}
|
|
||||||
|
|
||||||
atom_command!(PoolCommand: |state: MidiPool| {
|
|
||||||
("show" [a: bool] Some(Self::Show(a.expect("no flag"))))
|
|
||||||
("select" [i: usize] Some(Self::Select(i.expect("no index"))))
|
|
||||||
("rename" [,..a] ClipRenameCommand::try_from_expr(state, a).map(Self::Rename))
|
|
||||||
("length" [,..a] ClipLengthCommand::try_from_expr(state, a).map(Self::Length))
|
|
||||||
("import" [,..a] FileBrowserCommand::try_from_expr(state, a).map(Self::Import))
|
|
||||||
("export" [,..a] FileBrowserCommand::try_from_expr(state, a).map(Self::Export))
|
|
||||||
("clip" [,..a] PoolClipCommand::try_from_expr(state, a).map(Self::Clip))
|
|
||||||
});
|
|
||||||
|
|
||||||
command!(|self: PoolCommand, state: MidiPool|{
|
|
||||||
use PoolCommand::*;
|
|
||||||
match self {
|
|
||||||
Rename(ClipRenameCommand::Begin) => { state.begin_clip_rename(); None }
|
|
||||||
Rename(command) => command.delegate(state, Rename)?,
|
|
||||||
Length(ClipLengthCommand::Begin) => { state.begin_clip_length(); None },
|
|
||||||
Length(command) => command.delegate(state, Length)?,
|
|
||||||
Import(FileBrowserCommand::Begin) => { state.begin_import()?; None },
|
|
||||||
Import(command) => command.delegate(state, Import)?,
|
|
||||||
Export(FileBrowserCommand::Begin) => { state.begin_export()?; None },
|
|
||||||
Export(command) => command.delegate(state, Export)?,
|
|
||||||
Clip(command) => command.execute(state)?.map(Clip),
|
|
||||||
Show(visible) => { state.visible = visible; Some(Self::Show(!visible)) },
|
|
||||||
Select(clip) => { state.set_clip_index(clip); None },
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)] pub enum PoolClipCommand {
|
|
||||||
Add(usize, MidiClip),
|
|
||||||
Delete(usize),
|
|
||||||
Swap(usize, usize),
|
|
||||||
Import(usize, PathBuf),
|
|
||||||
Export(usize, PathBuf),
|
|
||||||
SetName(usize, Arc<str>),
|
|
||||||
SetLength(usize, usize),
|
|
||||||
SetColor(usize, ItemColor),
|
|
||||||
}
|
|
||||||
|
|
||||||
atom_command!(PoolClipCommand: |state: MidiPool| {
|
|
||||||
("add" [i: usize, c: MidiClip]
|
|
||||||
Some(Self::Add(i.expect("no index"), c.expect("no clip"))))
|
|
||||||
("delete" [i: usize]
|
|
||||||
Some(Self::Delete(i.expect("no index"))))
|
|
||||||
("swap" [a: usize, b: usize]
|
|
||||||
Some(Self::Swap(a.expect("no index"), b.expect("no index"))))
|
|
||||||
("import" [i: usize, p: PathBuf]
|
|
||||||
Some(Self::Import(i.expect("no index"), p.expect("no path"))))
|
|
||||||
("export" [i: usize, p: PathBuf]
|
|
||||||
Some(Self::Export(i.expect("no index"), p.expect("no path"))))
|
|
||||||
("set-name" [i: usize, n: Arc<str>]
|
|
||||||
Some(Self::SetName(i.expect("no index"), n.expect("no name"))))
|
|
||||||
("set-length" [i: usize, l: usize]
|
|
||||||
Some(Self::SetLength(i.expect("no index"), l.expect("no length"))))
|
|
||||||
("set-color" [i: usize, c: ItemColor]
|
|
||||||
Some(Self::SetColor(i.expect("no index"), c.expect("no color"))))
|
|
||||||
});
|
|
||||||
|
|
||||||
impl<T: HasClips> Command<T> for PoolClipCommand {
|
|
||||||
fn execute (self, model: &mut T) -> Perhaps<Self> {
|
|
||||||
use PoolClipCommand::*;
|
|
||||||
Ok(match self {
|
|
||||||
Add(mut index, clip) => {
|
|
||||||
let clip = Arc::new(RwLock::new(clip));
|
|
||||||
let mut clips = model.clips_mut();
|
|
||||||
if index >= clips.len() {
|
|
||||||
index = clips.len();
|
|
||||||
clips.push(clip)
|
|
||||||
} else {
|
|
||||||
clips.insert(index, clip);
|
|
||||||
}
|
|
||||||
Some(Self::Delete(index))
|
|
||||||
},
|
|
||||||
Delete(index) => {
|
|
||||||
let clip = model.clips_mut().remove(index).read().unwrap().clone();
|
|
||||||
Some(Self::Add(index, clip))
|
|
||||||
},
|
|
||||||
Swap(index, other) => {
|
|
||||||
model.clips_mut().swap(index, other);
|
|
||||||
Some(Self::Swap(index, other))
|
|
||||||
},
|
|
||||||
Import(index, path) => {
|
|
||||||
let bytes = std::fs::read(&path)?;
|
|
||||||
let smf = Smf::parse(bytes.as_slice())?;
|
|
||||||
let mut t = 0u32;
|
|
||||||
let mut events = vec![];
|
|
||||||
for track in smf.tracks.iter() {
|
|
||||||
for event in track.iter() {
|
|
||||||
t += event.delta.as_int();
|
|
||||||
if let TrackEventKind::Midi { channel, message } = event.kind {
|
|
||||||
events.push((t, channel.as_int(), message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let mut clip = MidiClip::new("imported", true, t as usize + 1, None, None);
|
|
||||||
for event in events.iter() {
|
|
||||||
clip.notes[event.0 as usize].push(event.2);
|
|
||||||
}
|
|
||||||
Self::Add(index, clip).execute(model)?
|
|
||||||
},
|
|
||||||
Export(_index, _path) => {
|
|
||||||
todo!("export clip to midi file");
|
|
||||||
},
|
|
||||||
SetName(index, name) => {
|
|
||||||
let clip = &mut model.clips_mut()[index];
|
|
||||||
let old_name = clip.read().unwrap().name.clone();
|
|
||||||
clip.write().unwrap().name = name;
|
|
||||||
Some(Self::SetName(index, old_name))
|
|
||||||
},
|
|
||||||
SetLength(index, length) => {
|
|
||||||
let clip = &mut model.clips_mut()[index];
|
|
||||||
let old_len = clip.read().unwrap().length;
|
|
||||||
clip.write().unwrap().length = length;
|
|
||||||
Some(Self::SetLength(index, old_len))
|
|
||||||
},
|
|
||||||
SetColor(index, color) => {
|
|
||||||
let mut color = ItemTheme::from(color);
|
|
||||||
std::mem::swap(&mut color, &mut model.clips()[index].write().unwrap().color);
|
|
||||||
Some(Self::SetColor(index, color.base))
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)] pub enum ClipRenameCommand {
|
|
||||||
Begin,
|
|
||||||
Cancel,
|
|
||||||
Confirm,
|
|
||||||
Set(Arc<str>),
|
|
||||||
}
|
|
||||||
|
|
||||||
atom_command!(ClipRenameCommand: |state: MidiPool| {
|
|
||||||
("begin" [] Some(Self::Begin))
|
|
||||||
("cancel" [] Some(Self::Cancel))
|
|
||||||
("confirm" [] Some(Self::Confirm))
|
|
||||||
("set" [n: Arc<str>] Some(Self::Set(n.expect("no name"))))
|
|
||||||
});
|
|
||||||
|
|
||||||
command!(|self: ClipRenameCommand, state: MidiPool|if let Some(
|
|
||||||
PoolMode::Rename(clip, ref mut old_name)
|
|
||||||
) = state.mode_mut().clone() {
|
|
||||||
match self {
|
|
||||||
Self::Set(s) => {
|
|
||||||
state.clips()[clip].write().unwrap().name = s;
|
|
||||||
return Ok(Some(Self::Set(old_name.clone().into())))
|
|
||||||
},
|
|
||||||
Self::Confirm => {
|
|
||||||
let old_name = old_name.clone();
|
|
||||||
*state.mode_mut() = None;
|
|
||||||
return Ok(Some(Self::Set(old_name)))
|
|
||||||
},
|
|
||||||
Self::Cancel => {
|
|
||||||
state.clips()[clip].write().unwrap().name = old_name.clone().into();
|
|
||||||
return Ok(None)
|
|
||||||
},
|
|
||||||
_ => unreachable!()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
unreachable!()
|
|
||||||
});
|
|
||||||
|
|
||||||
atom_command!(FileBrowserCommand: |state: MidiPool| {
|
|
||||||
("begin" [] Some(Self::Begin))
|
|
||||||
("cancel" [] Some(Self::Cancel))
|
|
||||||
("confirm" [] Some(Self::Confirm))
|
|
||||||
("select" [i: usize] Some(Self::Select(i.expect("no index"))))
|
|
||||||
("chdir" [p: PathBuf] Some(Self::Chdir(p.expect("no path"))))
|
|
||||||
("filter" [f: Arc<str>] Some(Self::Filter(f.expect("no filter"))))
|
|
||||||
});
|
|
||||||
|
|
||||||
command!(|self: FileBrowserCommand, state: MidiPool|{
|
|
||||||
use PoolMode::*;
|
|
||||||
use FileBrowserCommand::*;
|
|
||||||
let mode = &mut state.mode;
|
|
||||||
match mode {
|
|
||||||
Some(Import(index, ref mut browser)) => match self {
|
|
||||||
Cancel => { *mode = None; },
|
|
||||||
Chdir(cwd) => { *mode = Some(Import(*index, FileBrowser::new(Some(cwd))?)); },
|
|
||||||
Select(index) => { browser.index = index; },
|
|
||||||
Confirm => if browser.is_file() {
|
|
||||||
let index = *index;
|
|
||||||
let path = browser.path();
|
|
||||||
*mode = None;
|
|
||||||
PoolClipCommand::Import(index, path).execute(state)?;
|
|
||||||
} else if browser.is_dir() {
|
|
||||||
*mode = Some(Import(*index, browser.chdir()?));
|
|
||||||
},
|
|
||||||
_ => todo!(),
|
|
||||||
},
|
|
||||||
Some(Export(index, ref mut browser)) => match self {
|
|
||||||
Cancel => { *mode = None; },
|
|
||||||
Chdir(cwd) => { *mode = Some(Export(*index, FileBrowser::new(Some(cwd))?)); },
|
|
||||||
Select(index) => { browser.index = index; },
|
|
||||||
_ => unreachable!()
|
|
||||||
},
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
None
|
|
||||||
});
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
|
||||||
pub enum ClipLengthCommand {
|
|
||||||
Begin,
|
|
||||||
Cancel,
|
|
||||||
Set(usize),
|
|
||||||
Next,
|
|
||||||
Prev,
|
|
||||||
Inc,
|
|
||||||
Dec,
|
|
||||||
}
|
|
||||||
|
|
||||||
atom_command!(ClipLengthCommand: |state: MidiPool| {
|
|
||||||
("begin" [] Some(Self::Begin))
|
|
||||||
("cancel" [] Some(Self::Cancel))
|
|
||||||
("next" [] Some(Self::Next))
|
|
||||||
("prev" [] Some(Self::Prev))
|
|
||||||
("inc" [] Some(Self::Inc))
|
|
||||||
("dec" [] Some(Self::Dec))
|
|
||||||
("set" [l: usize] Some(Self::Set(l.expect("no length"))))
|
|
||||||
});
|
|
||||||
|
|
||||||
command!(|self: ClipLengthCommand, state: MidiPool|{
|
|
||||||
use ClipLengthCommand::*;
|
|
||||||
use ClipLengthFocus::*;
|
|
||||||
if let Some(
|
|
||||||
PoolMode::Length(clip, ref mut length, ref mut focus)
|
|
||||||
) = state.mode_mut().clone() {
|
|
||||||
match self {
|
|
||||||
Cancel => { *state.mode_mut() = None; },
|
|
||||||
Prev => { focus.prev() },
|
|
||||||
Next => { focus.next() },
|
|
||||||
Inc => match focus {
|
|
||||||
Bar => { *length += 4 * PPQ },
|
|
||||||
Beat => { *length += PPQ },
|
|
||||||
Tick => { *length += 1 },
|
|
||||||
},
|
|
||||||
Dec => match focus {
|
|
||||||
Bar => { *length = length.saturating_sub(4 * PPQ) },
|
|
||||||
Beat => { *length = length.saturating_sub(PPQ) },
|
|
||||||
Tick => { *length = length.saturating_sub(1) },
|
|
||||||
},
|
|
||||||
Set(length) => {
|
|
||||||
let old_length;
|
|
||||||
{
|
|
||||||
let clip = state.clips()[clip].clone();//.write().unwrap();
|
|
||||||
old_length = Some(clip.read().unwrap().length);
|
|
||||||
clip.write().unwrap().length = length;
|
|
||||||
}
|
|
||||||
*state.mode_mut() = None;
|
|
||||||
return Ok(old_length.map(Self::Set))
|
|
||||||
},
|
|
||||||
_ => unreachable!()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
unreachable!();
|
|
||||||
}
|
|
||||||
None
|
|
||||||
});
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
pub type ClipPool = Vec<Arc<RwLock<MidiClip>>>;
|
|
||||||
|
|
||||||
pub trait HasClips {
|
|
||||||
fn clips <'a> (&'a self) -> std::sync::RwLockReadGuard<'a, ClipPool>;
|
|
||||||
fn clips_mut <'a> (&'a self) -> std::sync::RwLockWriteGuard<'a, ClipPool>;
|
|
||||||
fn add_clip (&self) -> (usize, Arc<RwLock<MidiClip>>) {
|
|
||||||
let clip = Arc::new(RwLock::new(MidiClip::new("Clip", true, 384, None, None)));
|
|
||||||
self.clips_mut().push(clip.clone());
|
|
||||||
(self.clips().len() - 1, clip)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export] macro_rules! has_clips {
|
|
||||||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
|
||||||
impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? {
|
|
||||||
fn clips <'a> (&'a $self) -> std::sync::RwLockReadGuard<'a, ClipPool> {
|
|
||||||
$cb.read().unwrap()
|
|
||||||
}
|
|
||||||
fn clips_mut <'a> (&'a $self) -> std::sync::RwLockWriteGuard<'a, ClipPool> {
|
|
||||||
$cb.write().unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,190 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct MidiPool {
|
|
||||||
pub visible: bool,
|
|
||||||
/// Collection of clips
|
|
||||||
pub clips: Arc<RwLock<Vec<Arc<RwLock<MidiClip>>>>>,
|
|
||||||
/// Selected clip
|
|
||||||
pub clip: AtomicUsize,
|
|
||||||
/// Mode switch
|
|
||||||
pub mode: Option<PoolMode>,
|
|
||||||
|
|
||||||
pub keys: InputMap<'static, Self, PoolCommand, TuiIn, SourceIter<'static>>,
|
|
||||||
//pub keys: SourceIter<'static>,
|
|
||||||
//pub keys_rename: SourceIter<'static>,
|
|
||||||
//pub keys_length: SourceIter<'static>,
|
|
||||||
//pub keys_file: SourceIter<'static>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for MidiPool {
|
|
||||||
fn default () -> Self {
|
|
||||||
use PoolMode::*;
|
|
||||||
Self {
|
|
||||||
visible: true,
|
|
||||||
clips: Arc::from(RwLock::from(vec![])),
|
|
||||||
clip: 0.into(),
|
|
||||||
mode: None,
|
|
||||||
keys: InputMap::new(SourceIter(include_str!("../../../../config/keys_pool.edn")))
|
|
||||||
.layer_if(|pool: &Self|matches!(pool.mode, Some(Import(..))|Some(Export(..))),
|
|
||||||
SourceIter(include_str!("../../../../config/keys_pool_file.edn")))
|
|
||||||
.layer_if(|pool: &Self|matches!(pool.mode, Some(Rename(..))),
|
|
||||||
SourceIter(include_str!("../../../../config/keys_clip_rename.edn")))
|
|
||||||
.layer_if(|pool: &Self|matches!(pool.mode, Some(Length(..))),
|
|
||||||
SourceIter(include_str!("../../../../config/keys_clip_length.edn")))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
has_clips!(|self: MidiPool|self.clips);
|
|
||||||
|
|
||||||
has_clip!(|self: MidiPool|self.clips().get(self.clip_index()).map(|c|c.clone()));
|
|
||||||
|
|
||||||
from!(|clip:&Arc<RwLock<MidiClip>>|MidiPool = {
|
|
||||||
let model = Self::default();
|
|
||||||
model.clips.write().unwrap().push(clip.clone());
|
|
||||||
model.clip.store(1, Relaxed);
|
|
||||||
model
|
|
||||||
});
|
|
||||||
|
|
||||||
impl MidiPool {
|
|
||||||
pub fn clip_index (&self) -> usize {
|
|
||||||
self.clip.load(Relaxed)
|
|
||||||
}
|
|
||||||
pub fn set_clip_index (&self, value: usize) {
|
|
||||||
self.clip.store(value, Relaxed);
|
|
||||||
}
|
|
||||||
pub fn mode (&self) -> &Option<PoolMode> {
|
|
||||||
&self.mode
|
|
||||||
}
|
|
||||||
pub fn mode_mut (&mut self) -> &mut Option<PoolMode> {
|
|
||||||
&mut self.mode
|
|
||||||
}
|
|
||||||
pub fn begin_clip_length (&mut self) {
|
|
||||||
let length = self.clips()[self.clip_index()].read().unwrap().length;
|
|
||||||
*self.mode_mut() = Some(PoolMode::Length(
|
|
||||||
self.clip_index(),
|
|
||||||
length,
|
|
||||||
ClipLengthFocus::Bar
|
|
||||||
));
|
|
||||||
}
|
|
||||||
pub fn begin_clip_rename (&mut self) {
|
|
||||||
let name = self.clips()[self.clip_index()].read().unwrap().name.clone();
|
|
||||||
*self.mode_mut() = Some(PoolMode::Rename(
|
|
||||||
self.clip_index(),
|
|
||||||
name
|
|
||||||
));
|
|
||||||
}
|
|
||||||
pub fn begin_import (&mut self) -> Usually<()> {
|
|
||||||
*self.mode_mut() = Some(PoolMode::Import(
|
|
||||||
self.clip_index(),
|
|
||||||
FileBrowser::new(None)?
|
|
||||||
));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
pub fn begin_export (&mut self) -> Usually<()> {
|
|
||||||
*self.mode_mut() = Some(PoolMode::Export(
|
|
||||||
self.clip_index(),
|
|
||||||
FileBrowser::new(None)?
|
|
||||||
));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
pub fn new_clip (&self) -> MidiClip {
|
|
||||||
MidiClip::new("Clip", true, 4 * PPQ, None, Some(ItemTheme::random()))
|
|
||||||
}
|
|
||||||
pub fn cloned_clip (&self) -> MidiClip {
|
|
||||||
let index = self.clip_index();
|
|
||||||
let mut clip = self.clips()[index].read().unwrap().duplicate();
|
|
||||||
clip.color = ItemTheme::random_near(clip.color, 0.25);
|
|
||||||
clip
|
|
||||||
}
|
|
||||||
pub fn add_new_clip (&self) -> (usize, Arc<RwLock<MidiClip>>) {
|
|
||||||
let clip = Arc::new(RwLock::new(self.new_clip()));
|
|
||||||
let index = {
|
|
||||||
let mut clips = self.clips.write().unwrap();
|
|
||||||
clips.push(clip.clone());
|
|
||||||
clips.len().saturating_sub(1)
|
|
||||||
};
|
|
||||||
self.clip.store(index, Relaxed);
|
|
||||||
(index, clip)
|
|
||||||
}
|
|
||||||
pub fn delete_clip (&mut self, clip: &MidiClip) -> bool {
|
|
||||||
let index = self.clips.read().unwrap().iter().position(|x|*x.read().unwrap()==*clip);
|
|
||||||
if let Some(index) = index {
|
|
||||||
self.clips.write().unwrap().remove(index);
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Displays and edits clip length.
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct ClipLength {
|
|
||||||
/// Pulses per beat (quaver)
|
|
||||||
ppq: usize,
|
|
||||||
/// Beats per bar
|
|
||||||
bpb: usize,
|
|
||||||
/// Length of clip in pulses
|
|
||||||
pulses: usize,
|
|
||||||
/// Selected subdivision
|
|
||||||
focus: Option<ClipLengthFocus>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ClipLength {
|
|
||||||
fn _new (pulses: usize, focus: Option<ClipLengthFocus>) -> Self {
|
|
||||||
Self { ppq: PPQ, bpb: 4, pulses, focus }
|
|
||||||
}
|
|
||||||
fn bars (&self) -> usize {
|
|
||||||
self.pulses / (self.bpb * self.ppq)
|
|
||||||
}
|
|
||||||
fn beats (&self) -> usize {
|
|
||||||
(self.pulses % (self.bpb * self.ppq)) / self.ppq
|
|
||||||
}
|
|
||||||
fn ticks (&self) -> usize {
|
|
||||||
self.pulses % self.ppq
|
|
||||||
}
|
|
||||||
fn bars_string (&self) -> Arc<str> {
|
|
||||||
format!("{}", self.bars()).into()
|
|
||||||
}
|
|
||||||
fn beats_string (&self) -> Arc<str> {
|
|
||||||
format!("{}", self.beats()).into()
|
|
||||||
}
|
|
||||||
fn ticks_string (&self) -> Arc<str> {
|
|
||||||
format!("{:>02}", self.ticks()).into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Focused field of `ClipLength`
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
|
||||||
pub enum ClipLengthFocus {
|
|
||||||
/// Editing the number of bars
|
|
||||||
Bar,
|
|
||||||
/// Editing the number of beats
|
|
||||||
Beat,
|
|
||||||
/// Editing the number of ticks
|
|
||||||
Tick,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ClipLengthFocus {
|
|
||||||
fn next (&mut self) {
|
|
||||||
*self = match self { Self::Bar => Self::Beat, Self::Beat => Self::Tick, Self::Tick => Self::Bar, }
|
|
||||||
}
|
|
||||||
fn prev (&mut self) {
|
|
||||||
*self = match self { Self::Bar => Self::Tick, Self::Beat => Self::Bar, Self::Tick => Self::Beat, }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Modes for clip pool
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum PoolMode {
|
|
||||||
/// Renaming a pattern
|
|
||||||
Rename(usize, Arc<str>),
|
|
||||||
/// Editing the length of a pattern
|
|
||||||
Length(usize, usize, ClipLengthFocus),
|
|
||||||
/// Load clip from disk
|
|
||||||
Import(usize, FileBrowser),
|
|
||||||
/// Save clip to disk
|
|
||||||
Export(usize, FileBrowser),
|
|
||||||
}
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
pub struct PoolView<'a>(pub bool, pub &'a MidiPool);
|
|
||||||
|
|
||||||
content!(TuiOut: |self: PoolView<'a>| {
|
|
||||||
let Self(compact, model) = self;
|
|
||||||
let MidiPool { clips, .. } = self.1;
|
|
||||||
//let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||Tui::g(32).into());
|
|
||||||
let on_bg = |x|x;//Bsp::b(Repeat(" "), Tui::bg(color.darkest.rgb, x));
|
|
||||||
let border = |x|x;//Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)).enclose(x);
|
|
||||||
let iter = | |model.clips().clone().into_iter();
|
|
||||||
let height = clips.read().unwrap().len() as u16;
|
|
||||||
Tui::bg(Color::Reset, Fixed::y(height, on_bg(border(Map::new(iter, move|clip: Arc<RwLock<MidiClip>>, i|{
|
|
||||||
let item_height = 1;
|
|
||||||
let item_offset = i as u16 * item_height;
|
|
||||||
let selected = i == model.clip_index();
|
|
||||||
let MidiClip { ref name, color, length, .. } = *clip.read().unwrap();
|
|
||||||
let bg = if selected { color.light.rgb } else { color.base.rgb };
|
|
||||||
let fg = color.lightest.rgb;
|
|
||||||
let name = if *compact { format!(" {i:>3}") } else { format!(" {i:>3} {name}") };
|
|
||||||
let length = if *compact { String::default() } else { format!("{length} ") };
|
|
||||||
Fixed::y(1, map_south(item_offset, item_height, Tui::bg(bg, lay!(
|
|
||||||
Fill::x(Align::w(Tui::fg(fg, Tui::bold(selected, name)))),
|
|
||||||
Fill::x(Align::e(Tui::fg(fg, Tui::bold(selected, length)))),
|
|
||||||
Fill::x(Align::w(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "▶"))))),
|
|
||||||
Fill::x(Align::e(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "◀"))))),
|
|
||||||
))))
|
|
||||||
})))))
|
|
||||||
});
|
|
||||||
|
|
||||||
content!(TuiOut: |self: ClipLength| {
|
|
||||||
let bars = ||self.bars_string();
|
|
||||||
let beats = ||self.beats_string();
|
|
||||||
let ticks = ||self.ticks_string();
|
|
||||||
match self.focus {
|
|
||||||
None =>
|
|
||||||
row!(" ", bars(), ".", beats(), ".", ticks()),
|
|
||||||
Some(ClipLengthFocus::Bar) =>
|
|
||||||
row!("[", bars(), "]", beats(), ".", ticks()),
|
|
||||||
Some(ClipLengthFocus::Beat) =>
|
|
||||||
row!(" ", bars(), "[", beats(), "]", ticks()),
|
|
||||||
Some(ClipLengthFocus::Tick) =>
|
|
||||||
row!(" ", bars(), ".", beats(), "[", ticks()),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -2,7 +2,7 @@ use crate::*;
|
||||||
pub(crate) use std::fmt::Write;
|
pub(crate) use std::fmt::Write;
|
||||||
pub(crate) use ::tengri::tui::ratatui::prelude::Position;
|
pub(crate) use ::tengri::tui::ratatui::prelude::Position;
|
||||||
|
|
||||||
view!(TuiOut: |self: Tek| self.size.of(View(self, self.view)); {
|
view!(TuiOut: |self: Tek| self.size.of(View(self, self.config.view)); {
|
||||||
":nil" => Box::new("nil"),
|
":nil" => Box::new("nil"),
|
||||||
":modal" => self.view_modal(),
|
":modal" => self.view_modal(),
|
||||||
":status" => self.view_status(),
|
":status" => self.view_status(),
|
||||||
|
|
@ -42,7 +42,7 @@ impl Tek {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view_modal_help (&self) -> impl Content<TuiOut> + use<'_> {
|
fn view_modal_help (&self) -> impl Content<TuiOut> + use<'_> {
|
||||||
let bindings = ||self.keys.layers.iter()
|
let bindings = ||self.config.keys.layers.iter()
|
||||||
.filter_map(|a|(a.0)(self).then_some(a.1))
|
.filter_map(|a|(a.0)(self).then_some(a.1))
|
||||||
.flat_map(|a|a)
|
.flat_map(|a|a)
|
||||||
.filter_map(|x|if let Value::Exp(_, iter)=x.value{
|
.filter_map(|x|if let Value::Exp(_, iter)=x.value{
|
||||||
|
|
@ -388,16 +388,15 @@ impl<'a> ArrangerView<'a> {
|
||||||
let queued = track.player.next_clip.is_some();
|
let queued = track.player.next_clip.is_some();
|
||||||
let queued_blank = Thunk::new(||Tui::bg(Reset, " ------ "));
|
let queued_blank = Thunk::new(||Tui::bg(Reset, " ------ "));
|
||||||
let queued_clip = Thunk::new(||{
|
let queued_clip = Thunk::new(||{
|
||||||
let title = if let Some((_, clip)) = track.player.next_clip.as_ref() {
|
Tui::bg(Reset, if let Some((_, clip)) = track.player.next_clip.as_ref() {
|
||||||
if let Some(clip) = clip {
|
if let Some(clip) = clip {
|
||||||
clip.read().unwrap().name.as_ref().clone()
|
clip.read().unwrap().name.clone()
|
||||||
} else {
|
} else {
|
||||||
"Stop"
|
"Stop".into()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
""
|
"".into()
|
||||||
};
|
})
|
||||||
Tui::bg(Reset, title)
|
|
||||||
});
|
});
|
||||||
Either(queued, queued_clip, queued_blank)
|
Either(queued, queued_clip, queued_blank)
|
||||||
}))
|
}))
|
||||||
|
|
@ -917,3 +916,408 @@ impl ViewCache {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct PoolView<'a>(pub bool, pub &'a MidiPool);
|
||||||
|
|
||||||
|
content!(TuiOut: |self: PoolView<'a>| {
|
||||||
|
let Self(compact, model) = self;
|
||||||
|
let MidiPool { clips, .. } = self.1;
|
||||||
|
//let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||Tui::g(32).into());
|
||||||
|
let on_bg = |x|x;//Bsp::b(Repeat(" "), Tui::bg(color.darkest.rgb, x));
|
||||||
|
let border = |x|x;//Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)).enclose(x);
|
||||||
|
let iter = | |model.clips().clone().into_iter();
|
||||||
|
let height = clips.read().unwrap().len() as u16;
|
||||||
|
Tui::bg(Color::Reset, Fixed::y(height, on_bg(border(Map::new(iter, move|clip: Arc<RwLock<MidiClip>>, i|{
|
||||||
|
let item_height = 1;
|
||||||
|
let item_offset = i as u16 * item_height;
|
||||||
|
let selected = i == model.clip_index();
|
||||||
|
let MidiClip { ref name, color, length, .. } = *clip.read().unwrap();
|
||||||
|
let bg = if selected { color.light.rgb } else { color.base.rgb };
|
||||||
|
let fg = color.lightest.rgb;
|
||||||
|
let name = if *compact { format!(" {i:>3}") } else { format!(" {i:>3} {name}") };
|
||||||
|
let length = if *compact { String::default() } else { format!("{length} ") };
|
||||||
|
Fixed::y(1, map_south(item_offset, item_height, Tui::bg(bg, lay!(
|
||||||
|
Fill::x(Align::w(Tui::fg(fg, Tui::bold(selected, name)))),
|
||||||
|
Fill::x(Align::e(Tui::fg(fg, Tui::bold(selected, length)))),
|
||||||
|
Fill::x(Align::w(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "▶"))))),
|
||||||
|
Fill::x(Align::e(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "◀"))))),
|
||||||
|
))))
|
||||||
|
})))))
|
||||||
|
});
|
||||||
|
|
||||||
|
content!(TuiOut: |self: ClipLength| {
|
||||||
|
let bars = ||self.bars_string();
|
||||||
|
let beats = ||self.beats_string();
|
||||||
|
let ticks = ||self.ticks_string();
|
||||||
|
match self.focus {
|
||||||
|
None =>
|
||||||
|
row!(" ", bars(), ".", beats(), ".", ticks()),
|
||||||
|
Some(ClipLengthFocus::Bar) =>
|
||||||
|
row!("[", bars(), "]", beats(), ".", ticks()),
|
||||||
|
Some(ClipLengthFocus::Beat) =>
|
||||||
|
row!(" ", bars(), "[", beats(), "]", ticks()),
|
||||||
|
Some(ClipLengthFocus::Tick) =>
|
||||||
|
row!(" ", bars(), ".", beats(), "[", ticks()),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/// A clip, rendered as a horizontal piano roll.
|
||||||
|
#[derive(Clone)]
|
||||||
|
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: MidiRangeModel,
|
||||||
|
/// The note cursor
|
||||||
|
pub point: MidiPointModel,
|
||||||
|
/// The highlight color palette
|
||||||
|
pub color: ItemTheme,
|
||||||
|
/// Width of the keyboard
|
||||||
|
pub keys_width: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PianoHorizontal {
|
||||||
|
pub fn new (clip: Option<&Arc<RwLock<MidiClip>>>) -> Self {
|
||||||
|
let size = Measure::new();
|
||||||
|
let mut range = MidiRangeModel::from((12, true));
|
||||||
|
range.time_axis = size.x.clone();
|
||||||
|
range.note_axis = size.y.clone();
|
||||||
|
let piano = Self {
|
||||||
|
keys_width: 5,
|
||||||
|
size,
|
||||||
|
range,
|
||||||
|
buffer: RwLock::new(Default::default()).into(),
|
||||||
|
point: MidiPointModel::default(),
|
||||||
|
clip: clip.cloned(),
|
||||||
|
color: clip.as_ref().map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]),
|
||||||
|
};
|
||||||
|
piano.redraw();
|
||||||
|
piano
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn note_y_iter (note_lo: usize, note_hi: usize, y0: u16) -> impl Iterator<Item=(usize, u16, usize)> {
|
||||||
|
(note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n))
|
||||||
|
}
|
||||||
|
|
||||||
|
content!(TuiOut:|self: PianoHorizontal| Tui::bg(Tui::g(40), Bsp::s(
|
||||||
|
Bsp::e(
|
||||||
|
Fixed::x(5, format!("{}x{}", self.size.w(), self.size.h())),
|
||||||
|
self.timeline()
|
||||||
|
),
|
||||||
|
Bsp::e(
|
||||||
|
self.keys(),
|
||||||
|
self.size.of(Tui::bg(Tui::g(32), Bsp::b(
|
||||||
|
Fill::xy(self.notes()),
|
||||||
|
Fill::xy(self.cursor()),
|
||||||
|
)))
|
||||||
|
),
|
||||||
|
)));
|
||||||
|
|
||||||
|
impl PianoHorizontal {
|
||||||
|
/// Draw the piano roll background.
|
||||||
|
///
|
||||||
|
/// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄
|
||||||
|
fn draw_bg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize, note_len: usize) {
|
||||||
|
for (y, note) in (0..=127).rev().enumerate() {
|
||||||
|
for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) {
|
||||||
|
let cell = buf.get_mut(x, y).unwrap();
|
||||||
|
cell.set_bg(clip.color.darkest.rgb);
|
||||||
|
if time % 384 == 0 {
|
||||||
|
cell.set_fg(clip.color.darker.rgb);
|
||||||
|
cell.set_char('│');
|
||||||
|
} else if time % 96 == 0 {
|
||||||
|
cell.set_fg(clip.color.dark.rgb);
|
||||||
|
cell.set_char('╎');
|
||||||
|
} else if time % note_len == 0 {
|
||||||
|
cell.set_fg(clip.color.darker.rgb);
|
||||||
|
cell.set_char('┊');
|
||||||
|
} else if (127 - note) % 12 == 0 {
|
||||||
|
cell.set_fg(clip.color.darker.rgb);
|
||||||
|
cell.set_char('=');
|
||||||
|
} else if (127 - note) % 6 == 0 {
|
||||||
|
cell.set_fg(clip.color.darker.rgb);
|
||||||
|
cell.set_char('—');
|
||||||
|
} else {
|
||||||
|
cell.set_fg(clip.color.darker.rgb);
|
||||||
|
cell.set_char('·');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Draw the piano roll foreground.
|
||||||
|
///
|
||||||
|
/// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄
|
||||||
|
fn draw_fg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize) {
|
||||||
|
let style = Style::default().fg(clip.color.base.rgb);//.bg(Rgb(0, 0, 0));
|
||||||
|
let mut notes_on = [false;128];
|
||||||
|
for (x, time_start) in (0..clip.length).step_by(zoom).enumerate() {
|
||||||
|
for (_y, note) in (0..=127).rev().enumerate() {
|
||||||
|
if let Some(cell) = buf.get_mut(x, note) {
|
||||||
|
if notes_on[note] {
|
||||||
|
cell.set_char('▂');
|
||||||
|
cell.set_style(style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let time_end = time_start + zoom;
|
||||||
|
for time in time_start..time_end.min(clip.length) {
|
||||||
|
for event in clip.notes[time].iter() {
|
||||||
|
match event {
|
||||||
|
MidiMessage::NoteOn { key, .. } => {
|
||||||
|
let note = key.as_int() as usize;
|
||||||
|
if let Some(cell) = buf.get_mut(x, note) {
|
||||||
|
cell.set_char('█');
|
||||||
|
cell.set_style(style);
|
||||||
|
}
|
||||||
|
notes_on[note] = true
|
||||||
|
},
|
||||||
|
MidiMessage::NoteOff { key, .. } => {
|
||||||
|
notes_on[key.as_int() as usize] = false
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn notes (&self) -> impl Content<TuiOut> {
|
||||||
|
let time_start = self.time_start().get();
|
||||||
|
let note_lo = self.note_lo().get();
|
||||||
|
let note_hi = self.note_hi();
|
||||||
|
let buffer = self.buffer.clone();
|
||||||
|
ThunkRender::new(move|to: &mut TuiOut|{
|
||||||
|
let source = buffer.read().unwrap();
|
||||||
|
let [x0, y0, w, _h] = to.area().xywh();
|
||||||
|
//if h as usize != note_axis {
|
||||||
|
//panic!("area height mismatch: {h} <> {note_axis}");
|
||||||
|
//}
|
||||||
|
for (area_x, screen_x) in (x0..x0+w).enumerate() {
|
||||||
|
for (area_y, screen_y, _note) in note_y_iter(note_lo, note_hi, y0) {
|
||||||
|
let source_x = time_start + area_x;
|
||||||
|
let source_y = note_hi - area_y;
|
||||||
|
// TODO: enable loop rollover:
|
||||||
|
//let source_x = (time_start + area_x) % source.width.max(1);
|
||||||
|
//let source_y = (note_hi - area_y) % source.height.max(1);
|
||||||
|
let is_in_x = source_x < source.width;
|
||||||
|
let is_in_y = source_y < source.height;
|
||||||
|
if is_in_x && is_in_y {
|
||||||
|
if let Some(source_cell) = source.get(source_x, source_y) {
|
||||||
|
if let Some(cell) = to.buffer.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) {
|
||||||
|
*cell = source_cell.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn cursor (&self) -> impl Content<TuiOut> {
|
||||||
|
let style = Some(Style::default().fg(self.color.lightest.rgb));
|
||||||
|
let note_hi = self.note_hi();
|
||||||
|
let note_lo = self.note_lo().get();
|
||||||
|
let note_pos = self.note_pos();
|
||||||
|
let note_len = self.note_len();
|
||||||
|
let time_pos = self.time_pos();
|
||||||
|
let time_start = self.time_start().get();
|
||||||
|
let time_zoom = self.time_zoom().get();
|
||||||
|
ThunkRender::new(move|to: &mut TuiOut|{
|
||||||
|
let [x0, y0, w, _] = to.area().xywh();
|
||||||
|
for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) {
|
||||||
|
if note == note_pos {
|
||||||
|
for x in 0..w {
|
||||||
|
let screen_x = x0 + x;
|
||||||
|
let time_1 = time_start + x as usize * time_zoom;
|
||||||
|
let time_2 = time_1 + time_zoom;
|
||||||
|
if time_1 <= time_pos && time_pos < time_2 {
|
||||||
|
to.blit(&"█", screen_x, screen_y, style);
|
||||||
|
let tail = note_len as u16 / time_zoom as u16;
|
||||||
|
for x_tail in (screen_x + 1)..(screen_x + tail) {
|
||||||
|
to.blit(&"▂", x_tail, screen_y, style);
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn keys (&self) -> impl Content<TuiOut> {
|
||||||
|
let state = self;
|
||||||
|
let color = state.color;
|
||||||
|
let note_lo = state.note_lo().get();
|
||||||
|
let note_hi = state.note_hi();
|
||||||
|
let note_pos = state.note_pos();
|
||||||
|
let key_style = Some(Style::default().fg(Rgb(192, 192, 192)).bg(Rgb(0, 0, 0)));
|
||||||
|
let off_style = Some(Style::default().fg(Tui::g(255)));
|
||||||
|
let on_style = Some(Style::default().fg(Rgb(255,0,0)).bg(color.base.rgb).bold());
|
||||||
|
Fill::y(Fixed::x(self.keys_width, ThunkRender::new(move|to: &mut TuiOut|{
|
||||||
|
let [x, y0, _w, _h] = to.area().xywh();
|
||||||
|
for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) {
|
||||||
|
to.blit(&to_key(note), x, screen_y, key_style);
|
||||||
|
if note > 127 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if note == note_pos {
|
||||||
|
to.blit(&format!("{:<5}", Note::pitch_to_name(note)), x, screen_y, on_style)
|
||||||
|
} else {
|
||||||
|
to.blit(&Note::pitch_to_name(note), x, screen_y, off_style)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
fn timeline (&self) -> impl Content<TuiOut> + '_ {
|
||||||
|
Fill::x(Fixed::y(1, ThunkRender::new(move|to: &mut TuiOut|{
|
||||||
|
let [x, y, w, _h] = to.area();
|
||||||
|
let style = Some(Style::default().dim());
|
||||||
|
let length = self.clip.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1);
|
||||||
|
for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) {
|
||||||
|
let t = area_x as usize * self.time_zoom().get();
|
||||||
|
if t < length {
|
||||||
|
to.blit(&"|", screen_x, y, style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
has_size!(<TuiOut>|self:PianoHorizontal|&self.size);
|
||||||
|
|
||||||
|
impl TimeRange for PianoHorizontal {
|
||||||
|
fn time_len (&self) -> &AtomicUsize { self.range.time_len() }
|
||||||
|
fn time_zoom (&self) -> &AtomicUsize { self.range.time_zoom() }
|
||||||
|
fn time_lock (&self) -> &AtomicBool { self.range.time_lock() }
|
||||||
|
fn time_start (&self) -> &AtomicUsize { self.range.time_start() }
|
||||||
|
fn time_axis (&self) -> &AtomicUsize { self.range.time_axis() }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NoteRange for PianoHorizontal {
|
||||||
|
fn note_lo (&self) -> &AtomicUsize { self.range.note_lo() }
|
||||||
|
fn note_axis (&self) -> &AtomicUsize { self.range.note_axis() }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NotePoint for PianoHorizontal {
|
||||||
|
fn note_len (&self) -> usize { self.point.note_len() }
|
||||||
|
fn set_note_len (&self, x: usize) -> usize { self.point.set_note_len(x) }
|
||||||
|
fn note_pos (&self) -> usize { self.point.note_pos() }
|
||||||
|
fn set_note_pos (&self, x: usize) -> usize { self.point.set_note_pos(x) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimePoint for PianoHorizontal {
|
||||||
|
fn time_pos (&self) -> usize { self.point.time_pos() }
|
||||||
|
fn set_time_pos (&self, x: usize) -> usize { self.point.set_time_pos(x) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MidiViewer for PianoHorizontal {
|
||||||
|
fn clip (&self) -> &Option<Arc<RwLock<MidiClip>>> {
|
||||||
|
&self.clip
|
||||||
|
}
|
||||||
|
fn clip_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>> {
|
||||||
|
&mut self.clip
|
||||||
|
}
|
||||||
|
/// Determine the required space to render the clip.
|
||||||
|
fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) {
|
||||||
|
(clip.length / self.range.time_zoom().get(), 128)
|
||||||
|
}
|
||||||
|
fn redraw (&self) {
|
||||||
|
*self.buffer.write().unwrap() = if let Some(clip) = self.clip.as_ref() {
|
||||||
|
let clip = clip.read().unwrap();
|
||||||
|
let buf_size = self.buffer_size(&clip);
|
||||||
|
let mut buffer = BigBuffer::from(buf_size);
|
||||||
|
let note_len = self.note_len();
|
||||||
|
let time_zoom = self.time_zoom().get();
|
||||||
|
self.time_len().set(clip.length);
|
||||||
|
PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom, note_len);
|
||||||
|
PianoHorizontal::draw_fg(&mut buffer, &clip, time_zoom);
|
||||||
|
buffer
|
||||||
|
} else {
|
||||||
|
Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn set_clip (&mut self, clip: Option<&Arc<RwLock<MidiClip>>>) {
|
||||||
|
*self.clip_mut() = clip.cloned();
|
||||||
|
self.color = clip.map(|p|p.read().unwrap().color)
|
||||||
|
.unwrap_or(ItemTheme::G[64]);
|
||||||
|
self.redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for PianoHorizontal {
|
||||||
|
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||||
|
let buffer = self.buffer.read().unwrap();
|
||||||
|
f.debug_struct("PianoHorizontal")
|
||||||
|
.field("time_zoom", &self.range.time_zoom)
|
||||||
|
.field("buffer", &format!("{}x{}", buffer.width, buffer.height))
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Update sequencer playhead indicator
|
||||||
|
//self.now().set(0.);
|
||||||
|
//if let Some((ref started_at, Some(ref playing))) = self.player.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 to_key (note: usize) -> &'static str {
|
||||||
|
match note % 12 {
|
||||||
|
11 => "████▌",
|
||||||
|
10 => " ",
|
||||||
|
9 => "████▌",
|
||||||
|
8 => " ",
|
||||||
|
7 => "████▌",
|
||||||
|
6 => " ",
|
||||||
|
5 => "████▌",
|
||||||
|
4 => "████▌",
|
||||||
|
3 => " ",
|
||||||
|
2 => "████▌",
|
||||||
|
1 => " ",
|
||||||
|
0 => "████▌",
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct OctaveVertical {
|
||||||
|
on: [bool; 12],
|
||||||
|
colors: [Color; 3]
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for OctaveVertical {
|
||||||
|
fn default () -> Self {
|
||||||
|
Self {
|
||||||
|
on: [false; 12],
|
||||||
|
colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OctaveVertical {
|
||||||
|
fn color (&self, pitch: usize) -> Color {
|
||||||
|
let pitch = pitch % 12;
|
||||||
|
self.colors[if self.on[pitch] { 2 } else {
|
||||||
|
match pitch { 0 | 2 | 4 | 5 | 6 | 8 | 10 => 0, _ => 1 }
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Content<TuiOut> for OctaveVertical {
|
||||||
|
fn content (&self) -> impl Render<TuiOut> {
|
||||||
|
row!(
|
||||||
|
Tui::fg_bg(self.color(0), self.color(1), "▙"),
|
||||||
|
Tui::fg_bg(self.color(2), self.color(3), "▙"),
|
||||||
|
Tui::fg_bg(self.color(4), self.color(5), "▌"),
|
||||||
|
Tui::fg_bg(self.color(6), self.color(7), "▟"),
|
||||||
|
Tui::fg_bg(self.color(8), self.color(9), "▟"),
|
||||||
|
Tui::fg_bg(self.color(10), self.color(11), "▟"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -100,58 +100,20 @@ impl Cli {
|
||||||
for (index, connect) in midi_tos.iter().enumerate() {
|
for (index, connect) in midi_tos.iter().enumerate() {
|
||||||
let port = JackMidiOut::new(jack, &format!("{index}/M"), &[connect.clone()])?;
|
let port = JackMidiOut::new(jack, &format!("{index}/M"), &[connect.clone()])?;
|
||||||
midi_outs.push(port);
|
midi_outs.push(port);
|
||||||
}
|
};
|
||||||
|
let config_path = match mode {
|
||||||
|
LaunchMode::Clock => "config/config_transport.edn",
|
||||||
|
LaunchMode::Sequencer => "config/config_sequencer.edn",
|
||||||
|
LaunchMode::Groovebox => "config/config_groovebox.edn",
|
||||||
|
LaunchMode::Arranger { .. } => "config/config_arranger.edn",
|
||||||
|
LaunchMode::Sampler => "config/config_sampler.edn",
|
||||||
|
_ => todo!("{mode:?}"),
|
||||||
|
};
|
||||||
|
let config = Configuration::new(&config_path, false)?;
|
||||||
let mut app = Tek {
|
let mut app = Tek {
|
||||||
jack: jack.clone(),
|
jack: jack.clone(),
|
||||||
color: ItemTheme::random(),
|
color: ItemTheme::random(),
|
||||||
clock: Clock::new(jack, self.bpm)?,
|
clock: Clock::new(jack, self.bpm)?,
|
||||||
view: SourceIter(match mode {
|
|
||||||
LaunchMode::Clock =>
|
|
||||||
include_str!("../../config/view_transport.edn"),
|
|
||||||
LaunchMode::Sequencer =>
|
|
||||||
include_str!("../../config/view_sequencer.edn"),
|
|
||||||
LaunchMode::Groovebox =>
|
|
||||||
include_str!("../../config/view_groovebox.edn"),
|
|
||||||
LaunchMode::Arranger { .. } =>
|
|
||||||
include_str!("../../config/view_arranger.edn"),
|
|
||||||
LaunchMode::Sampler =>
|
|
||||||
include_str!("../../config/view_sampler.edn"),
|
|
||||||
_ => todo!("{mode:?}"),
|
|
||||||
}),
|
|
||||||
keys: match mode {
|
|
||||||
LaunchMode::Sampler => InputMap::default()
|
|
||||||
.layer(SourceIter(include_str!("../../config/keys_global.edn")).into())
|
|
||||||
.layer(SourceIter(include_str!("../../config/keys_sampler.edn")).into()),
|
|
||||||
LaunchMode::Clock => InputMap::default()
|
|
||||||
.layer(SourceIter(include_str!("../../config/keys_global.edn")).into())
|
|
||||||
.layer(SourceIter(include_str!("../../config/keys_clock.edn")).into()),
|
|
||||||
LaunchMode::Sequencer => InputMap::default()
|
|
||||||
.layer(SourceIter(include_str!("../../config/keys_global.edn")).into())
|
|
||||||
.layer(SourceIter(include_str!("../../config/keys_editor.edn")).into())
|
|
||||||
.layer(SourceIter(include_str!("../../config/keys_clock.edn")).into())
|
|
||||||
.layer(SourceIter(include_str!("../../config/keys_sequencer.edn")).into()),
|
|
||||||
LaunchMode::Groovebox => InputMap::default()
|
|
||||||
.layer(SourceIter(include_str!("../../config/keys_global.edn")).into())
|
|
||||||
.layer(SourceIter(include_str!("../../config/keys_editor.edn")).into())
|
|
||||||
.layer(SourceIter(include_str!("../../config/keys_clock.edn")).into())
|
|
||||||
.layer(SourceIter(include_str!("../../config/keys_sequencer.edn")).into())
|
|
||||||
.layer(SourceIter(include_str!("../../config/keys_groovebox.edn")).into()),
|
|
||||||
LaunchMode::Arranger {..} => InputMap::default()
|
|
||||||
.layer(SourceIter(include_str!("../../config/keys_global.edn")).into())
|
|
||||||
.layer_if(|state: &Tek|state.is_editing(),
|
|
||||||
SourceIter(include_str!("../../config/keys_editor.edn")).into())
|
|
||||||
.layer_if(|state: &Tek|state.selected.is_clip()&&!state.is_editing(),
|
|
||||||
SourceIter(include_str!("../../config/keys_clip.edn")).into())
|
|
||||||
.layer_if(|state: &Tek|state.selected.is_track()&&!state.is_editing(),
|
|
||||||
SourceIter(include_str!("../../config/keys_track.edn")).into())
|
|
||||||
.layer_if(|state: &Tek|state.selected.is_scene()&&!state.is_editing(),
|
|
||||||
SourceIter(include_str!("../../config/keys_scene.edn")).into())
|
|
||||||
.layer_if(|state: &Tek|state.selected.is_mix()&&!state.is_editing(),
|
|
||||||
SourceIter(include_str!("../../config/keys_mix.edn")).into())
|
|
||||||
.layer(SourceIter(include_str!("../../config/keys_clock.edn")).into())
|
|
||||||
.layer(SourceIter(include_str!("../../config/keys_arranger.edn")).into()),
|
|
||||||
_ => todo!("{mode:?}"),
|
|
||||||
},
|
|
||||||
pool: match mode {
|
pool: match mode {
|
||||||
LaunchMode::Sequencer | LaunchMode::Groovebox => clip.as_ref().map(Into::into),
|
LaunchMode::Sequencer | LaunchMode::Groovebox => clip.as_ref().map(Into::into),
|
||||||
LaunchMode::Arranger { .. } => Some(Default::default()),
|
LaunchMode::Arranger { .. } => Some(Default::default()),
|
||||||
|
|
@ -186,6 +148,7 @@ impl Cli {
|
||||||
},
|
},
|
||||||
scenes,
|
scenes,
|
||||||
selected: Selection::TrackClip { track: 0, scene: 0 },
|
selected: Selection::TrackClip { track: 0, scene: 0 },
|
||||||
|
config,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
if let &LaunchMode::Arranger { scenes, tracks, track_width, .. } = mode {
|
if let &LaunchMode::Arranger { scenes, tracks, track_width, .. } = mode {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,12 @@ wavers = { workspace = true, optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = [ "clock", "sequencer", "sampler" ]
|
default = [ "clock", "sequencer", "sampler" ]
|
||||||
lv2 = [ "livi" ]
|
clock = []
|
||||||
sampler = [ "symphonia", "wavers" ]
|
sampler = [ "symphonia", "wavers" ]
|
||||||
sequencer = [ "clock", "uuid" ]
|
sequencer = [ "clock", "uuid" ]
|
||||||
clock = []
|
plugin = [] # temporary
|
||||||
|
lv2 = [ "livi" ]
|
||||||
|
vst2 = []
|
||||||
|
vst3 = []
|
||||||
|
clap = []
|
||||||
|
sf2 = []
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,12 @@ pub(crate) use ratatui::{prelude::Rect, widgets::{Widget, canvas::{Canvas, Line}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Device {
|
pub enum Device {
|
||||||
#[cfg(feature = "sequencer")]
|
#[cfg(feature = "sequencer")] Sequencer(MidiPlayer),
|
||||||
Sequencer(MidiPlayer),
|
#[cfg(feature = "sampler")] Sampler(Sampler),
|
||||||
#[cfg(feature = "sampler")]
|
#[cfg(feature = "plugin")] Plugin(Plugin),
|
||||||
Sampler(Sampler),
|
#[cfg(feature = "lv2")] Lv2, // TODO
|
||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "vst2")] Vst2, // TODO
|
||||||
Plugin(Plugin),
|
#[cfg(feature = "vst3")] Vst3, // TODO
|
||||||
|
#[cfg(feature = "clap")] Clap, // TODO
|
||||||
|
#[cfg(feature = "sf2")] Sf2, // TODO
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,4 +15,3 @@ pub(crate) use std::sync::{Arc, RwLock};
|
||||||
mod jack_client; pub use self::jack_client::*;
|
mod jack_client; pub use self::jack_client::*;
|
||||||
mod jack_event; pub use self::jack_event::*;
|
mod jack_event; pub use self::jack_event::*;
|
||||||
mod jack_port; pub use self::jack_port::*;
|
mod jack_port; pub use self::jack_port::*;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
#![feature(type_alias_impl_trait)]
|
#![feature(type_alias_impl_trait)]
|
||||||
|
|
||||||
mod time; pub use self::time::*;
|
mod time; pub use self::time::*;
|
||||||
mod note; pub use self::note::*;
|
mod note; pub use self::note::*;
|
||||||
pub mod jack; pub use self::jack::*;
|
pub mod jack; pub use self::jack::*;
|
||||||
pub mod midi; pub use self::midi::*;
|
pub mod midi; pub use self::midi::*;
|
||||||
|
|
||||||
|
|
@ -10,11 +10,7 @@ pub(crate) use std::path::PathBuf;
|
||||||
pub(crate) use std::fmt::Debug;
|
pub(crate) use std::fmt::Debug;
|
||||||
pub(crate) use std::ops::{Add, Sub, Mul, Div, Rem};
|
pub(crate) use std::ops::{Add, Sub, Mul, Div, Rem};
|
||||||
|
|
||||||
pub(crate) use ::tengri::input::*;
|
|
||||||
pub(crate) use ::tengri::output::*;
|
|
||||||
pub(crate) use ::tengri::dsl::*;
|
|
||||||
pub(crate) use ::tengri::tui::*;
|
pub(crate) use ::tengri::tui::*;
|
||||||
pub(crate) use ::tengri::tui::ratatui::style::{Style, Stylize, Color};
|
|
||||||
|
|
||||||
pub use ::atomic_float; pub(crate) use atomic_float::*;
|
pub use ::atomic_float; pub(crate) use atomic_float::*;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
pub use ::midly::{
|
pub use ::midly::{
|
||||||
|
Smf,
|
||||||
|
TrackEventKind,
|
||||||
MidiMessage,
|
MidiMessage,
|
||||||
num::*,
|
num::*,
|
||||||
live::*,
|
live::*,
|
||||||
|
|
|
||||||
2
deps/tengri
vendored
2
deps/tengri
vendored
|
|
@ -1 +1 @@
|
||||||
Subproject commit 2b208e3c497d595f7dad5a6d190dfe08f7fb9dc0
|
Subproject commit 0d4ba4a54ef0528a0a45b58e21a1e4aa7ed5eaf9
|
||||||
Loading…
Add table
Add a link
Reference in a new issue