mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-10-08 18:56:42 +02:00
more reorg; newtype Binds and Views
This commit is contained in:
parent
3e3276bed0
commit
f10fa309e5
27 changed files with 1017 additions and 892 deletions
3
Justfile
3
Justfile
|
|
@ -1,6 +1,5 @@
|
||||||
#export RUSTFLAGS := "--cfg procmacro2_semver_exempt -Zmacro-backtrace -Clink-arg=-fuse-ld=mold"
|
|
||||||
export RUST_BACKTRACE := "1"
|
export RUST_BACKTRACE := "1"
|
||||||
export RUSTFLAGS := "-Zmacro-backtrace -Clink-arg=-fuse-ld=mold -Clink-arg=-Wl,--no-rosegment -Cforce-frame-pointers=yes"
|
export RUSTFLAGS := "-Clink-arg=-fuse-ld=mold -Clink-arg=-Wl,--no-rosegment -Cforce-frame-pointers=yes"
|
||||||
|
|
||||||
[default]
|
[default]
|
||||||
list:
|
list:
|
||||||
|
|
|
||||||
104
src/app.rs
104
src/app.rs
|
|
@ -24,28 +24,32 @@ use crate::*;
|
||||||
/// let _ = app.project.h_scenes();
|
/// let _ = app.project.h_scenes();
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug)]
|
||||||
#[namespace(MidiClip)]
|
|
||||||
#[namespace(ItemColor)]
|
|
||||||
#[namespace(ItemTheme)]
|
|
||||||
#[namespace(isize)]
|
|
||||||
#[namespace(u8)]
|
|
||||||
#[namespace(bool get_bool)]
|
|
||||||
#[namespace(u16 get_u16)]
|
|
||||||
#[namespace(usize get_usize)]
|
|
||||||
#[namespace(Arc<[Connect]> get_arc_array_connect)]
|
#[namespace(Arc<[Connect]> get_arc_array_connect)]
|
||||||
#[namespace(Arc<str> get_arc_str)]
|
#[namespace(Arc<str> get_arc_str)]
|
||||||
#[namespace(Color get_color)]
|
#[namespace(Color get_color)]
|
||||||
#[namespace(ControlAxis get_axis)]
|
#[namespace(ControlAxis get_axis)]
|
||||||
#[namespace(Dialog get_dialog)]
|
#[namespace(Dialog get_dialog)]
|
||||||
|
#[namespace(ItemColor)]
|
||||||
|
#[namespace(ItemTheme)]
|
||||||
|
#[namespace(MidiClip)]
|
||||||
#[namespace(Option<Arc<[Connect]>> get_opt_arc_array_connect)]
|
#[namespace(Option<Arc<[Connect]>> get_opt_arc_array_connect)]
|
||||||
#[namespace(Option<Arc<str>> get_opt_arc_str)]
|
#[namespace(Option<Arc<str>> get_opt_arc_str)]
|
||||||
#[namespace(Option<ItemTheme> get_opt_itemtheme)]
|
#[namespace(Option<ItemTheme> get_opt_itemtheme)]
|
||||||
|
#[namespace(Option<Arc<RwLock<MidiClip>>> get_opt_arc_rwlock_midiclip)]
|
||||||
#[namespace(Option<Vec<Option<Arc<RwLock<MidiClip>>>>> get_opt_vec_opt_arc_rwlock_midiclip)]
|
#[namespace(Option<Vec<Option<Arc<RwLock<MidiClip>>>>> get_opt_vec_opt_arc_rwlock_midiclip)]
|
||||||
#[namespace(Option<bool> get_opt_bool)]
|
#[namespace(Option<bool> get_opt_bool)]
|
||||||
#[namespace(Option<u16> get_opt_u16)]
|
#[namespace(Option<u16> get_opt_u16)]
|
||||||
|
#[namespace(Option<u32> get_opt_u32)]
|
||||||
#[namespace(Option<usize> get_opt_usize)]
|
#[namespace(Option<usize> get_opt_usize)]
|
||||||
#[namespace(PathBuf get_path_buf)]
|
#[namespace(PathBuf get_path_buf)]
|
||||||
#[namespace(Vec<Arc<str>> get_vec_arc_str)]
|
#[namespace(Vec<Arc<str>> get_vec_arc_str)]
|
||||||
|
#[namespace(bool get_bool)]
|
||||||
|
#[namespace(f64 get_f64)]
|
||||||
|
#[namespace(isize)]
|
||||||
|
#[namespace(u16 get_u16)]
|
||||||
|
#[namespace(u32)]
|
||||||
|
#[namespace(u8)]
|
||||||
|
#[namespace(usize get_usize)]
|
||||||
pub struct App {
|
pub struct App {
|
||||||
/// Exit flag
|
/// Exit flag
|
||||||
pub exit: Exit,
|
pub exit: Exit,
|
||||||
|
|
@ -83,7 +87,7 @@ impl App {
|
||||||
/// ```
|
/// ```
|
||||||
/// let mut proj = tek::Project::default();
|
/// let mut proj = tek::Project::default();
|
||||||
/// proj.jack = tek::tengri::Jack::new(&"test_tek").expect("failed to connect to jack");
|
/// proj.jack = tek::tengri::Jack::new(&"test_tek").expect("failed to connect to jack");
|
||||||
/// let mut conf = tek::config_load(tek::Config::default(), "(mode hello)").unwrap();
|
/// let mut conf = tek::Config::load(tek::Config::default(), "(mode hello)").unwrap();
|
||||||
/// let tek = tek::App::new(None, proj, conf.into(), "hello");
|
/// let tek = tek::App::new(None, proj, conf.into(), "hello");
|
||||||
/// ```
|
/// ```
|
||||||
pub fn new (
|
pub fn new (
|
||||||
|
|
@ -128,6 +132,10 @@ fn get_usize <'a> (state: &App, src: impl Language) -> Perhaps<usize> {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_f64 <'a> (_state: &App, _src: impl Language) -> Perhaps<f64> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
fn get_bool <'a> (state: &App, src: impl Language) -> Perhaps<bool> {
|
fn get_bool <'a> (state: &App, src: impl Language) -> Perhaps<bool> {
|
||||||
src.word()?.map(|word|Ok(match word {
|
src.word()?.map(|word|Ok(match word {
|
||||||
"Y" => true,
|
"Y" => true,
|
||||||
|
|
@ -145,7 +153,7 @@ fn get_bool <'a> (state: &App, src: impl Language) -> Perhaps<bool> {
|
||||||
":focused/track" => !state.editor_focused() && matches!(state.selection(), Selection::Track(..)),
|
":focused/track" => !state.editor_focused() && matches!(state.selection(), Selection::Track(..)),
|
||||||
":focused/scene" => !state.editor_focused() && matches!(state.selection(), Selection::Scene(..)),
|
":focused/scene" => !state.editor_focused() && matches!(state.selection(), Selection::Scene(..)),
|
||||||
":focused/mix" => !state.editor_focused() && matches!(state.selection(), Selection::Mix),
|
":focused/mix" => !state.editor_focused() && matches!(state.selection(), Selection::Mix),
|
||||||
_ => return Err(format!("not bool: {word}").into())
|
_ => return err!("not bool: {word}")
|
||||||
})).transpose()
|
})).transpose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -158,7 +166,7 @@ fn get_selection <'a> (state: &App, src: impl Language) -> Perhaps<Selection> {
|
||||||
":select/track" => state.selection().select_track(state.tracks().len()),
|
":select/track" => state.selection().select_track(state.tracks().len()),
|
||||||
":select/track/next" => state.selection().select_track_next(state.tracks().len()),
|
":select/track/next" => state.selection().select_track_next(state.tracks().len()),
|
||||||
":select/track/prev" => state.selection().select_track_prev(),
|
":select/track/prev" => state.selection().select_track_prev(),
|
||||||
_ => return Err(format!("not selection: {word}").into())
|
_ => return err!("not selection: {word}")
|
||||||
})).transpose()
|
})).transpose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -179,64 +187,64 @@ fn get_color <'a> (_: &App, src: impl Language) -> Perhaps<Color> {
|
||||||
.ok_or(LanguageError::domain("not blue"))?;
|
.ok_or(LanguageError::domain("not blue"))?;
|
||||||
Ok(Some(Color::Rgb(r, g, b)))
|
Ok(Some(Color::Rgb(r, g, b)))
|
||||||
},
|
},
|
||||||
(Some(_), _) => return Err(format!("not a color expression: {expr}").into()),
|
(Some(_), _) => return err!("not a color expression: {expr}"),
|
||||||
(None, _) => return Err(format!("not a color expression: {expr}").into()),
|
(None, _) => return err!("not a color expression: {expr}"),
|
||||||
}
|
}
|
||||||
} else if let Ok(Some(sym)) = src.word() {
|
} else if let Ok(Some(sym)) = src.word() {
|
||||||
Ok(match sym {
|
Ok(match sym {
|
||||||
":color/bg" => Some(Color::Rgb(28, 32, 36)),
|
":color/bg" => Some(Color::Rgb(28, 32, 36)),
|
||||||
":color/fg" => Some(Color::Rgb(98, 92, 96)),
|
":color/fg" => Some(Color::Rgb(98, 92, 96)),
|
||||||
_ => return Err(format!("not a color: {sym}").into())
|
_ => return err!("not a color: {sym}")
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
return Err(format!("not a color: {:?}", src.src()?).into())
|
return err!("not a color: {:?}", src.src()?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
fn get_opt_u7 <'a> (state: &App, src: impl Language) -> Perhaps<Option<u7>> {
|
fn get_opt_u7 <'a> (state: &App, src: impl Language) -> Perhaps<Option<u7>> {
|
||||||
src.word()?.map(|word|Ok(match word {
|
eval_word(src, |word|Ok(match word {
|
||||||
|
|
||||||
":editor/pitch" => Some((
|
":editor/pitch" => Some((
|
||||||
state.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8
|
state.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8
|
||||||
).into()),
|
).into()),
|
||||||
_ => return Err(format!("unknown midi note: {word}").into())
|
|
||||||
})).transpose()
|
_ => return err!("unknown midi note: {word}")
|
||||||
|
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_opt_usize <'a> (state: &App, src: impl Language) -> Perhaps<Option<usize>> {
|
fn get_opt_usize <'a> (state: &App, src: impl Language) -> Perhaps<Option<usize>> {
|
||||||
src.word()?.map(|word|Ok(match word {
|
eval_word(src, |word|Ok(match word {
|
||||||
|
|
||||||
":selected/scene" => state.selection().scene(),
|
":selected/scene" => state.selection().scene(),
|
||||||
":selected/track" => state.selection().track(),
|
":selected/track" => state.selection().track(),
|
||||||
_ => return Err(format!("unknown opt<usize>: {word}").into())
|
|
||||||
})).transpose()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused)]
|
_ => return err!("unknown opt<usize>: {word}")
|
||||||
fn get_clip <'a> (state: &App, src: impl Language) -> Perhaps<Option<Arc<RwLock<MidiClip>>>> {
|
|
||||||
src.word()?.map(|word|Ok(match word {
|
}))
|
||||||
":selected/clip" if let Selection::TrackClip { track, scene } = state.selection() =>
|
|
||||||
state.scenes()[*scene].clips[*track].clone(),
|
|
||||||
_ => return Err(format!("not a clip: {word}").into())
|
|
||||||
})).transpose()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_axis <'a> (_: &App, src: impl Language) -> Perhaps<ControlAxis> {
|
fn get_axis <'a> (_: &App, src: impl Language) -> Perhaps<ControlAxis> {
|
||||||
Ok(src.word()?.map(|word|Ok(match word {
|
eval_word(src, |word|Ok(match word {
|
||||||
|
|
||||||
":x" => ControlAxis::X,
|
":x" => ControlAxis::X,
|
||||||
":y" => ControlAxis::Y,
|
":y" => ControlAxis::Y,
|
||||||
_ => return Err(format!("unknown axis {word}"))
|
|
||||||
})).transpose()?)
|
_ => return err!("unknown axis {word}")
|
||||||
|
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_opt_u16 <'a> (_: &App, _src: impl Language) -> Perhaps<Option<u16>> {
|
fn get_opt_u16 <'a> (_: &App, _src: impl Language) -> Perhaps<Option<u16>> {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_opt_itemtheme <'a> (_: &App, _: impl Language) -> Perhaps<Option<ItemTheme>> {
|
fn get_opt_u32 <'a> (_: &App, _src: impl Language) -> Perhaps<Option<u32>> {
|
||||||
todo!()
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_opt_vec_opt_arc_rwlock_midiclip <'a> (_: &App, _: impl Language) -> Perhaps<Option<Vec<Option<Arc<RwLock<MidiClip>>>>>> {
|
fn get_opt_itemtheme <'a> (_: &App, _: impl Language) -> Perhaps<Option<ItemTheme>> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -264,14 +272,14 @@ fn get_vec_arc_str <'a> (_: &App, _: impl Language) -> Perhaps<Vec<Arc<str>>> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_has!(Clock: |self: App|self.project.clock);
|
impl_has!(Clock: |self: App|self.project.clock);
|
||||||
impl_has!(Dialog: |self: App|self.dialog);
|
impl_has!(Dialog: |self: App|self.dialog);
|
||||||
impl_has!(Jack<'static>: |self: App|self.jack);
|
impl_has!(Jack<'static>: |self: App|self.jack);
|
||||||
impl_has!(Pool: |self: App|self.pool);
|
impl_has!(Pool: |self: App|self.pool);
|
||||||
impl_has!(Selection: |self: App|self.project.selection);
|
impl_has!(Selection: |self: App|self.project.selection);
|
||||||
impl_as_ref_opt!(MidiEditor: |self: App|self.project.as_ref_opt());
|
impl_has_clips!( |self: App|self.pool.clips);
|
||||||
impl_as_mut_opt!(MidiEditor: |self: App|self.project.as_mut_opt());
|
impl_as_ref!(Option<MidiEditor>: |self: App|self.project.as_ref());
|
||||||
impl_has_clips!( |self: App|self.pool.clips);
|
impl_as_mut!(Option<MidiEditor>: |self: App|self.project.as_mut());
|
||||||
|
|
||||||
primitive!(u8: try_to_u8);
|
primitive!(u8: try_to_u8);
|
||||||
primitive!(u16: try_to_u16);
|
primitive!(u16: try_to_u16);
|
||||||
|
|
@ -298,7 +306,7 @@ impl App {
|
||||||
if let Some(mode) = mode {
|
if let Some(mode) = mode {
|
||||||
let binds = state.config.binds.clone();
|
let binds = state.config.binds.clone();
|
||||||
for id in mode.keys.iter() {
|
for id in mode.keys.iter() {
|
||||||
if let Some(event_map) = binds.try_read().unwrap().get(id.as_ref())
|
if let Some(event_map) = binds.find(id)
|
||||||
&& let Some(bindings) = event_map.query(input) {
|
&& let Some(bindings) = event_map.query(input) {
|
||||||
return Some(bindings.into())
|
return Some(bindings.into())
|
||||||
}
|
}
|
||||||
|
|
@ -379,7 +387,7 @@ impl App {
|
||||||
/// Swap currently active modal dialog.
|
/// Swap currently active modal dialog.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// let _ = tek::App::default().set_dialog(tek::Dialog::welcome());
|
/// let _ = tek::App::default().command_dialog(tek::Dialog::welcome());
|
||||||
/// ```
|
/// ```
|
||||||
#[command(Dialog = "dialog")]
|
#[command(Dialog = "dialog")]
|
||||||
pub fn command_dialog (&mut self, dialog: Dialog) -> Perhaps<AppCommand> {
|
pub fn command_dialog (&mut self, dialog: Dialog) -> Perhaps<AppCommand> {
|
||||||
|
|
@ -413,6 +421,11 @@ impl App {
|
||||||
Ok(command.dispatch(self)?.map(Into::into))
|
Ok(command.dispatch(self)?.map(Into::into))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[command(MidiEdit = "edit")]
|
||||||
|
pub fn command_midi_edit (&mut self, command: MidiEditCommand) -> Perhaps<AppCommand> {
|
||||||
|
Ok(command.dispatch(self)?.map(Into::into))
|
||||||
|
}
|
||||||
|
|
||||||
#[command(Pool = "pool")]
|
#[command(Pool = "pool")]
|
||||||
pub fn command_pool (&mut self, command: PoolCommand) -> Perhaps<AppCommand> {
|
pub fn command_pool (&mut self, command: PoolCommand) -> Perhaps<AppCommand> {
|
||||||
Ok(command.dispatch(self)?.map(Into::into))
|
Ok(command.dispatch(self)?.map(Into::into))
|
||||||
|
|
@ -480,6 +493,7 @@ impl App {
|
||||||
impl_from!(AppCommand: |x: AxisCommand| AppCommand::Axis { command: x });
|
impl_from!(AppCommand: |x: AxisCommand| AppCommand::Axis { command: x });
|
||||||
impl_from!(AppCommand: |x: BrowseCommand| AppCommand::Browse { command: x });
|
impl_from!(AppCommand: |x: BrowseCommand| AppCommand::Browse { command: x });
|
||||||
impl_from!(AppCommand: |x: MidiClipCommand| AppCommand::MidiClip { command: x });
|
impl_from!(AppCommand: |x: MidiClipCommand| AppCommand::MidiClip { command: x });
|
||||||
|
impl_from!(AppCommand: |x: MidiEditCommand| AppCommand::MidiEdit { command: x });
|
||||||
impl_from!(AppCommand: |x: ModeCommand| AppCommand::Mode { command: x });
|
impl_from!(AppCommand: |x: ModeCommand| AppCommand::Mode { command: x });
|
||||||
impl_from!(AppCommand: |x: PoolCommand| AppCommand::Pool { command: x });
|
impl_from!(AppCommand: |x: PoolCommand| AppCommand::Pool { command: x });
|
||||||
impl_from!(AppCommand: |x: ProjectCommand| AppCommand::Project { command: x });
|
impl_from!(AppCommand: |x: ProjectCommand| AppCommand::Project { command: x });
|
||||||
|
|
|
||||||
38
src/binds.rs
38
src/binds.rs
|
|
@ -1,38 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
/// Collection of input bindings.
|
|
||||||
pub type Binds = Arc<RwLock<BTreeMap<Arc<str>, Bind<TuiEvent, Arc<str>>>>>;
|
|
||||||
|
|
||||||
pub fn binds_add <'a> (binds: &Binds, expr: impl Language) -> UsuallyRef<'a, ()> {
|
|
||||||
//println!("\n\rbinds_add: {expr:?}");
|
|
||||||
let name = expr.head()?.ok_or("bind: missing name")?;
|
|
||||||
let body = expr.tail()?.unwrap_or("");
|
|
||||||
binds.try_write().unwrap().insert(name.into(), {
|
|
||||||
let mut map = Bind::new();
|
|
||||||
body.each((), |_, item: &str|if matches!(item.expr().head(), Ok(Some("see"))) {
|
|
||||||
// TODO
|
|
||||||
Ok(())
|
|
||||||
} else if let Ok(Some(_word)) = item.expr().head().word() {
|
|
||||||
let expr = item.expr()?;
|
|
||||||
let head = expr.head()?;
|
|
||||||
if let Some(event) = TuiKey::from_dsl(&head)?.to_crossterm() {
|
|
||||||
map.add(TuiEvent(event), Binding {
|
|
||||||
commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(),
|
|
||||||
condition: None,
|
|
||||||
description: None,
|
|
||||||
source: None
|
|
||||||
});
|
|
||||||
Ok(())
|
|
||||||
} else if Some(":char") == item.expr()?.head()? {
|
|
||||||
// TODO
|
|
||||||
return Ok(())
|
|
||||||
} else {
|
|
||||||
return Err(format!("binds_add: invalid key: {:?}", item.expr()?.head()?).into())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Err(format!("binds_add: unexpected: {item:?}").into())
|
|
||||||
})?;
|
|
||||||
map
|
|
||||||
});
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
11
src/cli.rs
11
src/cli.rs
|
|
@ -215,10 +215,12 @@ pub fn print_status (project: &Project) {
|
||||||
pub fn print_config (config: &Config) {
|
pub fn print_config (config: &Config) {
|
||||||
use ::ansi_term::Color::*;
|
use ::ansi_term::Color::*;
|
||||||
println!("{:?}", config.dirs);
|
println!("{:?}", config.dirs);
|
||||||
for (k, v) in config.views.try_read().unwrap().iter() {
|
|
||||||
|
config.views.for_each(|k, v|{
|
||||||
println!("{} {} {}", Green.paint("VIEW"), Green.bold().paint(format!("{k:<16}")), v.source);
|
println!("{} {} {}", Green.paint("VIEW"), Green.bold().paint(format!("{k:<16}")), v.source);
|
||||||
}
|
});
|
||||||
for (k, v) in config.binds.try_read().unwrap().iter() {
|
|
||||||
|
config.binds.for_each(|k, v|{
|
||||||
println!("{} {}", Green.paint("BIND"), Green.bold().paint(format!("{k:<16}")));
|
println!("{} {}", Green.paint("BIND"), Green.bold().paint(format!("{k:<16}")));
|
||||||
for (k, v) in v.0.iter() {
|
for (k, v) in v.0.iter() {
|
||||||
print!("{} ", &Yellow.paint(match &k.0 {
|
print!("{} ", &Yellow.paint(match &k.0 {
|
||||||
|
|
@ -238,7 +240,8 @@ pub fn print_config (config: &Config) {
|
||||||
//println!(" {:?}", v.source);
|
//println!(" {:?}", v.source);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
config.modes.for_each(|k, v|{
|
config.modes.for_each(|k, v|{
|
||||||
println!();
|
println!();
|
||||||
for v in v.name.iter() { print!("{}", Green.bold().paint(format!("{v} "))); }
|
for v in v.name.iter() { print!("{}", Green.bold().paint(format!("{v} "))); }
|
||||||
|
|
|
||||||
136
src/clip.rs
136
src/clip.rs
|
|
@ -1,136 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
impl Project {
|
|
||||||
/// Toggle looping for the active clip
|
|
||||||
pub fn toggle_loop (&mut self) {
|
|
||||||
if let Some(clip) = self.selected_clip() {
|
|
||||||
clip.try_write().unwrap().toggle_loop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Put a clip in a slot
|
|
||||||
pub fn clip_put (
|
|
||||||
&mut self, track: usize, scene: usize, clip: Option<Arc<RwLock<MidiClip>>>
|
|
||||||
) -> Option<Arc<RwLock<MidiClip>>> {
|
|
||||||
let old = self.scenes[scene].clips[track].clone();
|
|
||||||
self.scenes[scene].clips[track] = clip;
|
|
||||||
old
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Change the color of a clip, returning the previous one
|
|
||||||
pub fn clip_set_color (
|
|
||||||
&self, track: usize, scene: usize, color: ItemTheme
|
|
||||||
) -> Option<ItemTheme> {
|
|
||||||
self.scenes[scene].clips[track].as_ref().map(|clip|{
|
|
||||||
let mut clip = clip.try_write().unwrap();
|
|
||||||
let old = clip.color.clone();
|
|
||||||
clip.color = color.clone();
|
|
||||||
panic!("{color:?} {old:?}");
|
|
||||||
//old
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait HasClipsSize {
|
|
||||||
fn clips_size (&self) -> &Sizer;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HasClipsSize for App {
|
|
||||||
fn clips_size (&self) -> &Sizer { &self.project.size_inner }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HasClipsSize for Project {
|
|
||||||
fn clips_size (&self) -> &Sizer { &self.size_inner }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: TracksView + ScenesView + Send + Sync> ClipsView for T {}
|
|
||||||
|
|
||||||
pub trait ClipsView: TracksView + ScenesView {
|
|
||||||
|
|
||||||
/// Draw clips per scene
|
|
||||||
fn view_scenes_clips (&self) -> impl Draw<Tui> {
|
|
||||||
let select = self.selection();
|
|
||||||
let editor = self.editor();
|
|
||||||
let editing = self.is_editing();
|
|
||||||
with_clips_size(true, self.clips_size(), iter_east(move||self.tracks_with_sizes()
|
|
||||||
.map(move|(track_index, track, _, _)|iter_south(move||self.scenes_with_sizes()
|
|
||||||
.map(move|(scene_index, scene, _, _)|{
|
|
||||||
let (name, theme): (Arc<str>, ItemTheme) = view_scene_name_theme(scene, track_index);
|
|
||||||
let f = theme.lightest.term;
|
|
||||||
let (b, o) = view_scene_bg(theme, select, track_index, scene_index);
|
|
||||||
let is_selected = view_scene_sel(select, track_index, scene_index, editing);
|
|
||||||
below(
|
|
||||||
Outer(true, Style::default().fg(o)).full_wh(),
|
|
||||||
below(
|
|
||||||
below(
|
|
||||||
fg_bg(o, b, "".full_wh()),
|
|
||||||
fg_bg(f, b, bold(true, name)).align_nw().full_wh(),
|
|
||||||
),
|
|
||||||
when(is_selected, editor).full_wh()
|
|
||||||
).full_wh()
|
|
||||||
).exact_wh(
|
|
||||||
view_scene_w(track, select, track_index, editor),
|
|
||||||
view_scene_y(select, scene_index, editor),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.exact_w(track.width as u16))
|
|
||||||
)).align_c()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view_scene_name_theme (scene: &Scene, track_index: usize) -> (Arc<str>, ItemTheme) {
|
|
||||||
if let Some(Some(clip)) = &scene.clips.get(track_index) {
|
|
||||||
let clip = clip.try_read().unwrap();
|
|
||||||
(format!(" ⏹ {}", &clip.name).into(), clip.color)
|
|
||||||
} else {
|
|
||||||
(" ⏹ -- ".into(), ItemTheme::G[32])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn with_clips_size (show: bool, size: &Sizer, content: impl Draw<Tui>) -> impl Draw<Tui> {
|
|
||||||
above(
|
|
||||||
when(show, fg(Green, east!(size.w() as usize, "x", size.h() as usize)).align_se()),
|
|
||||||
size.of(content.align_c().full_wh())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view_scene_bg (
|
|
||||||
theme: ItemTheme, select: &Selection, track_index: usize, scene_index: usize
|
|
||||||
) -> (Color, Color) {
|
|
||||||
let mut outline = theme.base.term;
|
|
||||||
(if select.track() == Some(track_index) && select.scene() == Some(scene_index) {
|
|
||||||
outline = theme.lighter.term;
|
|
||||||
theme.light.term
|
|
||||||
} else if select.track() == Some(track_index) || select.scene() == Some(scene_index) {
|
|
||||||
outline = theme.darkest.term;
|
|
||||||
theme.base.term
|
|
||||||
} else {
|
|
||||||
theme.dark.term
|
|
||||||
}, outline)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view_scene_w (
|
|
||||||
track: &Track, select: &Selection, track_index: usize, editor: Option<&MidiEditor>
|
|
||||||
) -> u16 {
|
|
||||||
if select.track() == Some(track_index) && let Some(editor) = editor {
|
|
||||||
(editor.size.w() as usize).max(24).max(track.width) as u16
|
|
||||||
} else {
|
|
||||||
track.width as u16
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view_scene_y (
|
|
||||||
select: &Selection, scene_index: usize, editor: Option<&MidiEditor>
|
|
||||||
) -> u16 {
|
|
||||||
if select.scene() == Some(scene_index) && let Some(editor) = editor {
|
|
||||||
editor.size.h().max(12)
|
|
||||||
} else {
|
|
||||||
Scene::DEFAULT_HEIGHT as u16
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view_scene_sel (select: &Selection, track_index: usize, scene_index: usize, editing: bool) -> bool {
|
|
||||||
editing && select.track() == Some(track_index) && select.scene() == Some(scene_index)
|
|
||||||
}
|
|
||||||
|
|
@ -5,7 +5,7 @@ use notify_debouncer_full::notify::{RecursiveMode, PollWatcher, Config as Notify
|
||||||
/// Configuration: mode, view, and bind definitions.
|
/// Configuration: mode, view, and bind definitions.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use ::{std::sync::Arc, tek::{Config, Modes::add, Mode}};
|
/// use ::{std::sync::Arc, tek::{Config, Modes, Mode}};
|
||||||
/// let config: Config = Default::default();
|
/// let config: Config = Default::default();
|
||||||
/// Modes::add(&config.modes, stringify!(
|
/// Modes::add(&config.modes, stringify!(
|
||||||
/// (mode :menu (name Menu)
|
/// (mode :menu (name Menu)
|
||||||
|
|
@ -58,24 +58,18 @@ impl Config {
|
||||||
|
|
||||||
pub fn load_item <C: AsRef<Config>, L: Language> (config: C, src: L) -> Usually<C> {
|
pub fn load_item <C: AsRef<Config>, L: Language> (config: C, src: L) -> Usually<C> {
|
||||||
if let Some(expr) = src.expr()? {
|
if let Some(expr) = src.expr()? {
|
||||||
Config::load_kind(config, expr)
|
match expr.head()? {
|
||||||
|
Some("mode") => Modes::add(&config.as_ref().modes, expr.tail()),
|
||||||
|
Some("keys") => Binds::add(&config.as_ref().binds, expr.tail()),
|
||||||
|
Some("view") => Views::add(&config.as_ref().views, expr.tail()),
|
||||||
|
_ => return Err(format!("Config::load: expected view/keys/mode expr, got: {src:?}").into())
|
||||||
|
}?;
|
||||||
|
Ok(config)
|
||||||
} else {
|
} else {
|
||||||
Err(format!("Config::add_one: tried to add empty item").into())
|
Err(format!("Config::add_one: tried to add empty item").into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_kind <C: AsRef<Config>, L: Language> (config: C, src: L) -> Usually<C> {
|
|
||||||
//if let Some(tail) = src.tail()? {
|
|
||||||
match src.head()? {
|
|
||||||
Some("mode") => Modes::add(&config.as_ref().modes, src.tail()),
|
|
||||||
Some("keys") => binds_add(&config.as_ref().binds, src.tail()),
|
|
||||||
Some("view") => views_add(&config.as_ref().views, src.tail()),
|
|
||||||
_ => return Err(format!("Config::load: expected view/keys/mode expr, got: {src:?}").into())
|
|
||||||
}?;
|
|
||||||
//}
|
|
||||||
Ok(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Watch a config's file for changes.
|
/// Watch a config's file for changes.
|
||||||
pub fn watch (
|
pub fn watch (
|
||||||
config: Arc<Config>, poll: Option<Duration>
|
config: Arc<Config>, poll: Option<Duration>
|
||||||
|
|
@ -158,14 +152,14 @@ impl Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Make this configuration empty.
|
/// Make this configuration empty.
|
||||||
fn clear (&self) {
|
pub fn clear (&self) {
|
||||||
*self.modes.0.try_write().unwrap() = Default::default();
|
*self.modes.0.try_write().unwrap() = Default::default();
|
||||||
*self.views.try_write().unwrap() = Default::default();
|
self.views.clear();
|
||||||
*self.binds.try_write().unwrap() = Default::default();
|
self.binds.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_view (&self, name: impl AsRef<str>) -> Option<Arc<View<Tui, App>>> {
|
pub fn get_view (&self, name: impl AsRef<str>) -> Option<Arc<View<Tui, App>>> {
|
||||||
self.views.try_read().map(|views|views.get(name.as_ref()).cloned()).flatten()
|
self.views.get(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Default configuration directory.
|
/// Default configuration directory.
|
||||||
|
|
@ -181,3 +175,12 @@ impl Config {
|
||||||
const DEFAULTS: &'static str = include_str!("tek.edn");
|
const DEFAULTS: &'static str = include_str!("tek.edn");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn eval_word <T> (src: impl Language, eval: impl Fn(&str)->Usually<T>) -> Perhaps<T> {
|
||||||
|
src.word()?.map(eval).transpose()
|
||||||
|
}
|
||||||
|
|
||||||
|
//pub fn eval_expr <T> (src: impl Language, eval: impl Fn(&str)->Usually<T>) -> Perhaps<T> {
|
||||||
|
//Ok(match expr.head()? {
|
||||||
|
//})
|
||||||
|
//}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,63 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
|
/// Collection of input bindings.
|
||||||
|
#[derive(Default, Clone, Debug)]
|
||||||
|
pub struct Binds(pub Arc<RwLock<BTreeMap<
|
||||||
|
Arc<str>,
|
||||||
|
Arc<Bind<TuiEvent, Arc<str>>>
|
||||||
|
>>>);
|
||||||
|
|
||||||
|
impl Binds {
|
||||||
|
pub fn add <'a> (binds: &Binds, expr: impl Language) -> UsuallyRef<'a, ()> {
|
||||||
|
let name = expr.head()?.ok_or("bind: missing name")?;
|
||||||
|
let body = expr.tail()?.unwrap_or("");
|
||||||
|
binds.0.try_write().unwrap().insert(name.into(), {
|
||||||
|
let mut map = Bind::new();
|
||||||
|
body.each((), |_, item: &str|if matches!(item.expr().head(), Ok(Some("see"))) {
|
||||||
|
// TODO
|
||||||
|
Ok(())
|
||||||
|
} else if let Ok(Some(_word)) = item.expr().head().word() {
|
||||||
|
let expr = item.expr()?;
|
||||||
|
let head = expr.head()?;
|
||||||
|
if let Some(event) = TuiKey::from_dsl(&head)?.to_crossterm() {
|
||||||
|
map.add(TuiEvent(event), Binding {
|
||||||
|
commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(),
|
||||||
|
condition: None,
|
||||||
|
description: None,
|
||||||
|
source: None
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
} else if Some(":char") == item.expr()?.head()? {
|
||||||
|
// TODO
|
||||||
|
return Ok(())
|
||||||
|
} else {
|
||||||
|
return err!("Binds::add: invalid key: {:?}", item.expr()?.head()?)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return err!("Binds::add: unexpected: {item:?}")
|
||||||
|
})?;
|
||||||
|
map.into()
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear (&self) {
|
||||||
|
*self.0.write() = Default::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find (&self, id: impl AsRef<str>) -> Option<Arc<Bind<TuiEvent, Arc<str>>>> {
|
||||||
|
self.0.read().get(id.as_ref()).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn for_each <T> (
|
||||||
|
&self, mut cb: impl FnMut(&Arc<str>, &Bind<TuiEvent, Arc<str>>)->T
|
||||||
|
) {
|
||||||
|
for (k, v) in self.0.read().iter() {
|
||||||
|
cb(k, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// An map of input events (e.g. [TuiEvent]) to [Binding]s.
|
/// An map of input events (e.g. [TuiEvent]) to [Binding]s.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
162
src/config_mode.rs
Normal file
162
src/config_mode.rs
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// Collection of UI modes.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let mut modes = tek::Modes::default();
|
||||||
|
/// assert_eq!(modes.len(), 0);
|
||||||
|
/// let _ = modes.add(&":foo", &"(mode (name Foo) (info Bar))");
|
||||||
|
/// assert_eq!(modes.len(), 1);
|
||||||
|
/// ```
|
||||||
|
#[derive(Default, Debug, Clone)]
|
||||||
|
pub struct Modes(pub Arc<RwLock<BTreeMap<
|
||||||
|
Arc<str>,
|
||||||
|
Arc<Mode>
|
||||||
|
>>>);
|
||||||
|
|
||||||
|
impl Modes {
|
||||||
|
|
||||||
|
/// Register a mode.
|
||||||
|
pub fn add <'a> (&self, expr: impl Language) -> UsuallyRef<'a, ()> {
|
||||||
|
let name = expr.head()?.ok_or("mode: missing name")?;
|
||||||
|
let body = expr.tail()?.ok_or("mode: missing body")?;
|
||||||
|
let mode = Mode::default();
|
||||||
|
let mode = body.each(mode, |c, s|Mode::add(c, s))?;
|
||||||
|
self.0.try_write().unwrap().insert(name.into(), Arc::new(mode));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a mode by name.
|
||||||
|
pub fn get (&self, name: impl AsRef<str>) -> Option<Arc<Mode>> {
|
||||||
|
self.0.try_read().unwrap().get(name.as_ref()).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run something for each mode.
|
||||||
|
pub fn for_each <T> (&self, mut ator: impl FnMut(&str, &Mode)->T) {
|
||||||
|
for (k, v) in self.0.try_read().unwrap().iter() {
|
||||||
|
let _ = ator(k.as_ref(), v.as_ref());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Count modes.
|
||||||
|
pub fn len (&self) -> usize {
|
||||||
|
self.0.try_read().unwrap().len()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Group of view and keys definitions.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let mode = tek::Mode::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct Mode {
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub name: Vec<Arc<str>>,
|
||||||
|
pub info: Vec<Arc<str>>,
|
||||||
|
pub view: Vec<Arc<View<Tui, App>>>,
|
||||||
|
pub keys: Vec<Arc<str>>,
|
||||||
|
pub modes: Modes,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mode {
|
||||||
|
|
||||||
|
/// Add a definition to the mode.
|
||||||
|
///
|
||||||
|
/// Supported definitions:
|
||||||
|
///
|
||||||
|
/// - (name ...) -> name
|
||||||
|
/// - (info ...) -> description
|
||||||
|
/// - (keys ...) -> key bindings
|
||||||
|
/// - (mode ...) -> submode
|
||||||
|
/// - ... -> view
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let mut mode: tek::Mode = tek::Mode::add(tek::Mode::default(), "(name hello)").unwrap();
|
||||||
|
/// ```
|
||||||
|
pub fn add <T: AsMut<Mode>> (mut mode: T, dsl: impl Language) -> Usually<T> {
|
||||||
|
Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() {
|
||||||
|
//println!("Mode::add: {head} {:?}", expr.tail());
|
||||||
|
let tail = expr.tail()?.map(|x|x.trim()).unwrap_or("");
|
||||||
|
match head {
|
||||||
|
"mode" => {
|
||||||
|
let name = tail.head()?.ok_or("submode: missing name")?;
|
||||||
|
let body = tail.tail()?.ok_or("submode: missing body")?;
|
||||||
|
let submode = Mode::default();
|
||||||
|
let submode = body.each(submode, |c,s|Mode::add(c,s))?;
|
||||||
|
let modes = mode.as_mut().modes.clone();
|
||||||
|
modes.0.try_write().unwrap().insert(name.into(), Arc::new(submode));
|
||||||
|
mode
|
||||||
|
},
|
||||||
|
"keys" => {
|
||||||
|
tail.each(mode, |mut mode: T, expr: &str|{
|
||||||
|
mode.as_mut().keys.push(expr.trim().into());
|
||||||
|
Ok(mode)
|
||||||
|
})?
|
||||||
|
},
|
||||||
|
"name" => { mode.as_mut().name.push(tail.into()); mode },
|
||||||
|
"info" => { mode.as_mut().info.push(tail.into()); mode },
|
||||||
|
"view" => { mode.as_mut().view.push(View::new(tail)?.into()); mode },
|
||||||
|
_ => { mode.as_mut().view.push(View::new(tail)?.into()); mode },
|
||||||
|
}
|
||||||
|
} else if let Ok(Some(word)) = dsl.word() {
|
||||||
|
mode.as_mut().view.push(View::new(word)?.into());
|
||||||
|
mode
|
||||||
|
} else {
|
||||||
|
return Err(format!("Mode::add: unexpected: {dsl:?}").into());
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsMut<Mode> for Mode {
|
||||||
|
fn as_mut (&mut self) -> &mut Self {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait HasModeStack {
|
||||||
|
fn modes (&self) -> RwLockReadGuard<'_, Vec<Arc<str>>>;
|
||||||
|
fn modes_mut (&mut self) -> RwLockWriteGuard<'_, Vec<Arc<str>>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasModeStack for App {
|
||||||
|
fn modes (&self) -> RwLockReadGuard<'_, Vec<Arc<str>>> {
|
||||||
|
self.modes.try_read().unwrap()
|
||||||
|
}
|
||||||
|
fn modes_mut (&mut self) -> RwLockWriteGuard<'_, Vec<Arc<str>>> {
|
||||||
|
self.modes.try_write().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> ModeController for T where T: HasModeStack
|
||||||
|
+ Namespace<Vec<Arc<str>>>
|
||||||
|
+ Namespace<Arc<str>>
|
||||||
|
+ Namespace<usize>
|
||||||
|
{}
|
||||||
|
|
||||||
|
#[tek_proc::commands(ModeCommand)]
|
||||||
|
pub trait ModeController: HasModeStack
|
||||||
|
+ Namespace<Vec<Arc<str>>>
|
||||||
|
+ Namespace<Arc<str>>
|
||||||
|
+ Namespace<usize>
|
||||||
|
{
|
||||||
|
#[command(Reset = "reset")]
|
||||||
|
fn mode_reset (&mut self, modes: Vec<Arc<str>>) -> Perhaps<ModeCommand> {
|
||||||
|
*self.modes_mut() = modes.into();
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[command(Push = "push")]
|
||||||
|
fn mode_push (&mut self, mode: Arc<str>) -> Perhaps<ModeCommand> {
|
||||||
|
self.modes_mut().push(mode);
|
||||||
|
Ok(Some(ModeCommand::Pop))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[command(Pop = "pop")]
|
||||||
|
fn mode_pop (&mut self) -> Perhaps<ModeCommand> {
|
||||||
|
let mode = self.modes_mut().pop();
|
||||||
|
Ok(mode.map(|mode|ModeCommand::Push { mode }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,36 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
/// Collection of custom view definitions.
|
/// Collection of custom view definitions.
|
||||||
pub type Views = Arc<RwLock<BTreeMap<Arc<str>, Arc<View<Tui, App>>>>>;
|
#[derive(Default, Clone, Debug)]
|
||||||
|
pub struct Views(pub Arc<RwLock<BTreeMap<
|
||||||
|
Arc<str>,
|
||||||
|
Arc<View<Tui, App>>
|
||||||
|
>>>);
|
||||||
|
|
||||||
|
impl Views {
|
||||||
|
/// Load custom view definition.
|
||||||
|
pub fn add <'a> (views: &Views, expr: impl Language) -> UsuallyRef<'a, ()> {
|
||||||
|
views.0.write().insert(
|
||||||
|
expr.head()?.ok_or("view: missing name")?.into(),
|
||||||
|
View::new(expr.tail()?.ok_or("view: missing body")?)?.into()
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear (&self) {
|
||||||
|
*self.0.try_write().unwrap() = Default::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get (&self, name: impl AsRef<str>) -> Option<Arc<View<Tui, App>>> {
|
||||||
|
self.0.try_read().map(|views|views.get(name.as_ref()).cloned()).flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn for_each <T> (&self, cb: impl Fn(&Arc<str>, &Arc<View<Tui, App>>)->T) -> () {
|
||||||
|
for (k, v) in self.0.read().iter() {
|
||||||
|
cb(k, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Custom view definition is a boxed closure emitting a [Draw]able from state `S`.
|
/// Custom view definition is a boxed closure emitting a [Draw]able from state `S`.
|
||||||
pub struct View<S: Screen, T> {
|
pub struct View<S: Screen, T> {
|
||||||
|
|
@ -37,7 +66,7 @@ impl View<Tui, App> {
|
||||||
} else if let Ok(Some(word)) = source.word() {
|
} else if let Ok(Some(word)) = source.word() {
|
||||||
Self::compile_word(word.into())?
|
Self::compile_word(word.into())?
|
||||||
} else {
|
} else {
|
||||||
return Err(format!("not word/expr:\n{source:?}").into())
|
return err!("not word/expr:\n{source:?}")
|
||||||
};
|
};
|
||||||
Ok(Arc::new(Box::new(move|state, screen|layer(state, screen))))
|
Ok(Arc::new(Box::new(move|state, screen|layer(state, screen))))
|
||||||
}
|
}
|
||||||
|
|
@ -94,7 +123,7 @@ impl View<Tui, App> {
|
||||||
Some("w") => Split::West,
|
Some("w") => Split::West,
|
||||||
Some("a") => Split::Above,
|
Some("a") => Split::Above,
|
||||||
Some("b") => Split::Below,
|
Some("b") => Split::Below,
|
||||||
_ => return Err(format!("invalid split: {split:?}").into())
|
_ => return err!("invalid split: {split:?}")
|
||||||
};
|
};
|
||||||
let a = Self::compile(arg!(expr, head, 1, "content A"))?;
|
let a = Self::compile(arg!(expr, head, 1, "content A"))?;
|
||||||
let b = Self::compile(arg!(expr, head, 2, "content B"))?;
|
let b = Self::compile(arg!(expr, head, 2, "content B"))?;
|
||||||
|
|
@ -113,7 +142,7 @@ impl View<Tui, App> {
|
||||||
Some("w") => Split::West,
|
Some("w") => Split::West,
|
||||||
Some("a") => Split::Above,
|
Some("a") => Split::Above,
|
||||||
Some("b") => Split::Below,
|
Some("b") => Split::Below,
|
||||||
_ => return Err(format!("invalid split: {split:?}").into())
|
_ => return err!("invalid split: {split:?}")
|
||||||
};
|
};
|
||||||
let size = Arc::from(arg!(expr, head, 1, "size"));
|
let size = Arc::from(arg!(expr, head, 1, "size"));
|
||||||
let size = move|state: &App|state.namespace(&size)?.ok_or_else(||Box::<dyn Error>::from("bar: no size"));
|
let size = move|state: &App|state.namespace(&size)?.ok_or_else(||Box::<dyn Error>::from("bar: no size"));
|
||||||
|
|
@ -140,7 +169,7 @@ impl View<Tui, App> {
|
||||||
Some("c") => Azimuth::C,
|
Some("c") => Azimuth::C,
|
||||||
Some("x") => Azimuth::X,
|
Some("x") => Azimuth::X,
|
||||||
Some("y") => Azimuth::Y,
|
Some("y") => Azimuth::Y,
|
||||||
_ => return Err(format!("invalid azimuth: {azimuth:?}").into())
|
_ => return err!("invalid azimuth: {azimuth:?}")
|
||||||
};
|
};
|
||||||
let thunk = Self::compile(arg!(expr, head, 1, "content"))?;
|
let thunk = Self::compile(arg!(expr, head, 1, "content"))?;
|
||||||
Self::boxed(move|state, screen|{
|
Self::boxed(move|state, screen|{
|
||||||
|
|
@ -239,12 +268,12 @@ impl View<Tui, App> {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
_ => return Err(format!("compile_expr: unexpected: {expr:?}").into())
|
_ => return err!("compile_expr: unexpected: {expr:?}")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
return Err(format!("compile_expr: invalid expression: {expr:?}").into())
|
return err!("compile_expr: invalid expression: {expr:?}")
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -310,12 +339,3 @@ impl View<Tui, App> {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load custom view definition.
|
|
||||||
pub fn views_add <'a> (views: &Views, expr: impl Language) -> UsuallyRef<'a, ()> {
|
|
||||||
views.try_write().unwrap().insert(
|
|
||||||
expr.head()?.ok_or("view: missing name")?.into(),
|
|
||||||
View::new(expr.tail()?.ok_or("view: missing body")?)?.into()
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
@ -1,35 +1,43 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
pub fn get_dialog <'a> (_: &App, src: impl Language) -> Perhaps<Dialog> {
|
pub fn get_dialog <'a> (_: &App, src: impl Language) -> Perhaps<Dialog> {
|
||||||
src.word()?.map(|word|Ok(match word {
|
|
||||||
":none" => Dialog::None,
|
eval_word(&src, |word|Ok(match word {
|
||||||
":options" => Dialog::Options,
|
|
||||||
":device" => Dialog::Device(0),
|
":none" => Dialog::None,
|
||||||
":device/prev" => Dialog::Device(0),
|
|
||||||
":device/next" => Dialog::Device(0),
|
":options" => Dialog::Options,
|
||||||
":help" => Dialog::Help(0),
|
|
||||||
":save" => Dialog::Browse(
|
":device" => Dialog::Device(0),
|
||||||
|
":device/prev" => Dialog::Device(0),
|
||||||
|
":device/next" => Dialog::Device(0),
|
||||||
|
|
||||||
|
":help" => Dialog::Help(0),
|
||||||
|
":save" => Dialog::Browse(
|
||||||
BrowseTarget::SaveProject,
|
BrowseTarget::SaveProject,
|
||||||
Browse::new(None).unwrap().into()),
|
Browse::new(None).unwrap().into()),
|
||||||
":load" => Dialog::Browse(
|
":load" => Dialog::Browse(
|
||||||
BrowseTarget::LoadProject,
|
BrowseTarget::LoadProject,
|
||||||
Browse::new(None).unwrap().into()),
|
Browse::new(None).unwrap().into()),
|
||||||
":import/clip" => Dialog::Browse(
|
|
||||||
|
":import/clip" => Dialog::Browse(
|
||||||
BrowseTarget::ImportClip(Default::default()),
|
BrowseTarget::ImportClip(Default::default()),
|
||||||
Browse::new(None).unwrap().into()),
|
Browse::new(None).unwrap().into()),
|
||||||
":export/clip" => Dialog::Browse(
|
":export/clip" => Dialog::Browse(
|
||||||
BrowseTarget::ExportClip(Default::default()),
|
BrowseTarget::ExportClip(Default::default()),
|
||||||
Browse::new(None).unwrap().into()),
|
Browse::new(None).unwrap().into()),
|
||||||
|
|
||||||
":import/sample" => Dialog::Browse(
|
":import/sample" => Dialog::Browse(
|
||||||
BrowseTarget::ImportSample(Default::default()),
|
BrowseTarget::ImportSample(Default::default()),
|
||||||
Browse::new(None).unwrap().into()),
|
Browse::new(None).unwrap().into()),
|
||||||
":export/sample" => Dialog::Browse(
|
":export/sample" => Dialog::Browse(
|
||||||
BrowseTarget::ExportSample(Default::default()),
|
BrowseTarget::ExportSample(Default::default()),
|
||||||
Browse::new(None).unwrap().into()),
|
Browse::new(None).unwrap().into()),
|
||||||
_ => return Err(
|
|
||||||
format!("not a dialog: {}", src.src()?.unwrap_or("<???>")).into()
|
_ => return Err(format!("not a dialog: {}", src.src()?.unwrap_or("<???>")).into())
|
||||||
)
|
|
||||||
})).transpose()
|
}))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Various possible dialog modes.
|
/// Various possible dialog modes.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,37 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
|
pub fn get_opt_arc_rwlock_midiclip <'a> (state: &App, src: impl Language)
|
||||||
|
-> Perhaps<Option<Arc<RwLock<MidiClip>>>>
|
||||||
|
{
|
||||||
|
eval_word(src, |word|Ok(match word {
|
||||||
|
|
||||||
|
":selected" if let Selection::TrackClip {
|
||||||
|
track, scene
|
||||||
|
} = state.selection() => {
|
||||||
|
state.scenes()[*scene].clips[*track].clone()
|
||||||
|
},
|
||||||
|
|
||||||
|
//":selected-or-create" if let Selection::TrackClip {
|
||||||
|
//track, scene
|
||||||
|
//} = state.selection() => Some(if let Some(Some(clip)) = state.scenes()[*scene].clips.get(*track) {
|
||||||
|
//clip.clone()
|
||||||
|
//} else {
|
||||||
|
//let clip: Arc<RwLock<MidiClip>> = Arc::new(RwLock::new(Default::default()));
|
||||||
|
//state.scenes()[*scene].clips[*track] = Some(clip.clone());
|
||||||
|
//clip
|
||||||
|
//}),
|
||||||
|
|
||||||
|
_ => return err!("not a clip: {word}")
|
||||||
|
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_opt_vec_opt_arc_rwlock_midiclip <'a> (_: &App, _: impl Language)
|
||||||
|
-> Perhaps<Option<Vec<Option<Arc<RwLock<MidiClip>>>>>>
|
||||||
|
{
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
/// A MIDI sequence.
|
/// A MIDI sequence.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
|
|
@ -29,11 +61,11 @@ use crate::*;
|
||||||
|
|
||||||
impl MidiClip {
|
impl MidiClip {
|
||||||
pub fn new (
|
pub fn new (
|
||||||
name: impl AsRef<str>,
|
name: impl AsRef<str>,
|
||||||
looped: bool,
|
looped: bool,
|
||||||
length: usize,
|
length: usize,
|
||||||
notes: Option<MidiData>,
|
notes: Option<MidiData>,
|
||||||
color: Option<ItemTheme>,
|
color: Option<ItemTheme>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
uuid: uuid::Uuid::new_v4(),
|
uuid: uuid::Uuid::new_v4(),
|
||||||
|
|
@ -93,6 +125,8 @@ impl MidiClip {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type MidiData = Vec<Vec<MidiMessage>>;
|
pub type MidiData = Vec<Vec<MidiMessage>>;
|
||||||
|
impl PartialEq for MidiClip { fn eq (&self, other: &Self) -> bool { self.uuid == other.uuid } }
|
||||||
|
impl Eq for MidiClip {}
|
||||||
|
|
||||||
#[macro_export] macro_rules! has_clip {
|
#[macro_export] macro_rules! has_clip {
|
||||||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||||||
|
|
@ -122,8 +156,12 @@ pub trait MidiClipController: HasMidiClip
|
||||||
+ Namespace<Option<bool>>
|
+ Namespace<Option<bool>>
|
||||||
+ Namespace<Option<ItemTheme>>
|
+ Namespace<Option<ItemTheme>>
|
||||||
{
|
{
|
||||||
|
#[command(Upsert = "upsert")]
|
||||||
|
fn clip_upsert (&mut self) -> Perhaps<MidiClipCommand> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
#[command(SetColor = "clip/color")]
|
#[command(SetColor = "color")]
|
||||||
fn clip_set_color (&mut self, _color: Option<ItemTheme>) -> Perhaps<MidiClipCommand> {
|
fn clip_set_color (&mut self, _color: Option<ItemTheme>) -> Perhaps<MidiClipCommand> {
|
||||||
//(SetColor [t: usize, s: usize, c: ItemTheme]
|
//(SetColor [t: usize, s: usize, c: ItemTheme]
|
||||||
//clip.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o)))));
|
//clip.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o)))));
|
||||||
|
|
@ -131,7 +169,7 @@ pub trait MidiClipController: HasMidiClip
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command(SetLoop = "clip/loop")]
|
#[command(SetLoop = "loop")]
|
||||||
fn clip_toggle_loop (&mut self, _looping: Option<bool>) -> Perhaps<MidiClipCommand> {
|
fn clip_toggle_loop (&mut self, _looping: Option<bool>) -> Perhaps<MidiClipCommand> {
|
||||||
//(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}"))
|
//(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}"))
|
||||||
//("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap())))
|
//("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap())))
|
||||||
|
|
@ -139,11 +177,3 @@ pub trait MidiClipController: HasMidiClip
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for MidiClip {
|
|
||||||
fn eq (&self, other: &Self) -> bool {
|
|
||||||
self.uuid == other.uuid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Eq for MidiClip {}
|
|
||||||
|
|
|
||||||
|
|
@ -26,3 +26,90 @@ pub trait HasClips {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Project {
|
||||||
|
/// Toggle looping for the active clip
|
||||||
|
pub fn toggle_loop (&mut self) {
|
||||||
|
if let Some(clip) = self.selected_clip() {
|
||||||
|
clip.try_write().unwrap().toggle_loop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Put a clip in a slot
|
||||||
|
pub fn clip_put (
|
||||||
|
&mut self, track: usize, scene: usize, clip: Option<Arc<RwLock<MidiClip>>>
|
||||||
|
) -> Option<Arc<RwLock<MidiClip>>> {
|
||||||
|
let old = self.scenes[scene].clips[track].clone();
|
||||||
|
self.scenes[scene].clips[track] = clip;
|
||||||
|
old
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change the color of a clip, returning the previous one
|
||||||
|
pub fn clip_set_color (
|
||||||
|
&self, track: usize, scene: usize, color: ItemTheme
|
||||||
|
) -> Option<ItemTheme> {
|
||||||
|
self.scenes[scene].clips[track].as_ref().map(|clip|{
|
||||||
|
let mut clip = clip.try_write().unwrap();
|
||||||
|
let old = clip.color.clone();
|
||||||
|
clip.color = color.clone();
|
||||||
|
panic!("{color:?} {old:?}");
|
||||||
|
//old
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait HasClipsSize {
|
||||||
|
fn clips_size (&self) -> &Sizer;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasClipsSize for App {
|
||||||
|
fn clips_size (&self) -> &Sizer { &self.project.size_inner }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasClipsSize for Project {
|
||||||
|
fn clips_size (&self) -> &Sizer { &self.size_inner }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: TracksView + ScenesView + Send + Sync> ClipsView for T {}
|
||||||
|
|
||||||
|
pub trait ClipsView: TracksView + ScenesView {
|
||||||
|
|
||||||
|
/// Draw clips per scene
|
||||||
|
fn view_scenes_clips (&self) -> impl Draw<Tui> {
|
||||||
|
let select = self.selection();
|
||||||
|
let editor = self.editor();
|
||||||
|
let editing = self.is_editing();
|
||||||
|
with_clips_size(true, self.clips_size(), iter_east(move||self.tracks_with_sizes()
|
||||||
|
.map(move|(track_index, track, _, _)|iter_south(move||self.scenes_with_sizes()
|
||||||
|
.map(move|(scene_index, scene, _, _)|{
|
||||||
|
let (name, theme): (Arc<str>, ItemTheme) = view_scene_name_theme(scene, track_index);
|
||||||
|
let f = theme.lightest.term;
|
||||||
|
let (b, o) = view_scene_bg(theme, select, track_index, scene_index);
|
||||||
|
let is_selected = view_scene_sel(select, track_index, scene_index, editing);
|
||||||
|
below(
|
||||||
|
Outer(true, Style::default().fg(o)).full_wh(),
|
||||||
|
below(
|
||||||
|
below(
|
||||||
|
fg_bg(o, b, "".full_wh()),
|
||||||
|
fg_bg(f, b, bold(true, name)).align_nw().full_wh(),
|
||||||
|
),
|
||||||
|
when(is_selected, editor).full_wh()
|
||||||
|
).full_wh()
|
||||||
|
).exact_wh(
|
||||||
|
view_scene_w(track, select, track_index, editor.as_ref()),
|
||||||
|
view_scene_y(select, scene_index, editor.as_ref()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.exact_w(track.width as u16))
|
||||||
|
)).align_c()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_clips_size (show: bool, size: &Sizer, content: impl Draw<Tui>) -> impl Draw<Tui> {
|
||||||
|
above(
|
||||||
|
when(show, fg(Green, east!(size.w() as usize, "x", size.h() as usize)).align_se()),
|
||||||
|
size.of(content.align_c().full_wh())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,70 +1,27 @@
|
||||||
#![allow(unused)]
|
#![allow(unused)]
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
impl App {
|
pub trait HasEditor: AsRef<Option<MidiEditor>> + AsMut<Option<MidiEditor>> {
|
||||||
/// Is a MIDI editor currently focused?
|
fn editor (&self) -> &Option<MidiEditor> {
|
||||||
///
|
self.as_ref()
|
||||||
/// ```
|
|
||||||
/// tek::App::default().editor_focused();
|
|
||||||
/// ```
|
|
||||||
pub fn editor_focused (&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
|
fn editor_mut (&mut self) -> &mut Option<MidiEditor> {
|
||||||
/// Toggle MIDI editor.
|
self.as_mut()
|
||||||
///
|
}
|
||||||
/// ```
|
fn is_editing (&self) -> bool {
|
||||||
/// tek::App::default().toggle_editor(None);
|
self.editor().is_some()
|
||||||
/// ```
|
}
|
||||||
pub fn toggle_editor (&mut self, value: Option<bool>) {
|
fn editor_w (&self) -> usize {
|
||||||
//FIXME: self.editing.store(value.unwrap_or_else(||!self.is_editing()), Relaxed);
|
self.editor().as_ref().map(|e|e.size.w()).unwrap_or(0) as usize
|
||||||
let value = value.unwrap_or_else(||!self.editor().is_some());
|
}
|
||||||
if value {
|
fn editor_h (&self) -> usize {
|
||||||
// Create new clip in pool when entering empty cell
|
self.editor().as_ref().map(|e|e.size.h()).unwrap_or(0) as usize
|
||||||
if let Selection::TrackClip { track, scene } = *self.selection()
|
|
||||||
&& let Some(scene) = self.project.scenes.get_mut(scene)
|
|
||||||
&& let Some(slot) = scene.clips.get_mut(track)
|
|
||||||
&& slot.is_none()
|
|
||||||
&& let Some(track) = self.project.tracks.get_mut(track)
|
|
||||||
{
|
|
||||||
let (_index, clip) = self.pool.add_new_clip();
|
|
||||||
// autocolor: new clip colors from scene and track color
|
|
||||||
let color = track.color.base.mix(scene.color.base, 0.5);
|
|
||||||
clip.try_write().unwrap().color = ItemColor::random_near(color, 0.2).into();
|
|
||||||
if let Some(editor) = &mut self.project.editor {
|
|
||||||
editor.set_clip(Some(&clip));
|
|
||||||
}
|
|
||||||
*slot = Some(clip.clone());
|
|
||||||
//Some(clip)
|
|
||||||
} else {
|
|
||||||
//None
|
|
||||||
}
|
|
||||||
} else if let Selection::TrackClip { track, scene } = *self.selection()
|
|
||||||
&& let Some(scene) = self.project.scenes.get_mut(scene)
|
|
||||||
&& let Some(slot) = scene.clips.get_mut(track)
|
|
||||||
&& let Some(clip) = slot.as_mut()
|
|
||||||
{
|
|
||||||
// Remove clip from arrangement when exiting empty clip editor
|
|
||||||
let mut swapped = None;
|
|
||||||
if clip.try_read().unwrap().count_midi_messages() == 0 {
|
|
||||||
std::mem::swap(&mut swapped, slot);
|
|
||||||
}
|
|
||||||
if let Some(clip) = swapped {
|
|
||||||
self.pool.delete_clip(&clip.try_read().unwrap());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait HasEditor: AsRefOpt<MidiEditor> + AsMutOpt<MidiEditor> {
|
impl<T> HasEditor for T where T:
|
||||||
fn editor (&self) -> Option<&MidiEditor> { self.as_ref_opt() }
|
AsRef<Option<MidiEditor>> +
|
||||||
fn editor_mut (&mut self) -> Option<&mut MidiEditor> { self.as_mut_opt() }
|
AsMut<Option<MidiEditor>> {}
|
||||||
fn is_editing (&self) -> bool { self.editor().is_some() }
|
|
||||||
fn editor_w (&self) -> usize { self.editor().map(|e|e.size.w()).unwrap_or(0) as usize }
|
|
||||||
fn editor_h (&self) -> usize { self.editor().map(|e|e.size.h()).unwrap_or(0) as usize }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: AsRefOpt<MidiEditor>+AsMutOpt<MidiEditor>> HasEditor for T {}
|
|
||||||
|
|
||||||
impl<T: HasEditor
|
impl<T: HasEditor
|
||||||
+ Namespace<u32>
|
+ Namespace<u32>
|
||||||
|
|
@ -75,7 +32,7 @@ impl<T: HasEditor
|
||||||
+ Namespace<Option<Arc<RwLock<MidiClip>>>>
|
+ Namespace<Option<Arc<RwLock<MidiClip>>>>
|
||||||
> MidiEditController for T {}
|
> MidiEditController for T {}
|
||||||
|
|
||||||
#[tek_proc::commands(MidiEditCommand = "edit")]
|
#[tek_proc::commands(MidiEditCommand)]
|
||||||
pub trait MidiEditController: HasEditor
|
pub trait MidiEditController: HasEditor
|
||||||
+ Namespace<u32>
|
+ Namespace<u32>
|
||||||
+ Namespace<f64>
|
+ Namespace<f64>
|
||||||
|
|
@ -84,9 +41,19 @@ pub trait MidiEditController: HasEditor
|
||||||
+ Namespace<Option<u32>>
|
+ Namespace<Option<u32>>
|
||||||
+ Namespace<Option<Arc<RwLock<MidiClip>>>>
|
+ Namespace<Option<Arc<RwLock<MidiClip>>>>
|
||||||
{
|
{
|
||||||
|
#[command(Toggle = "toggle")]
|
||||||
|
fn edit_toggle (&mut self) -> Perhaps<MidiEditCommand> {
|
||||||
|
if self.is_editing() {
|
||||||
|
*self.editor_mut() = None;
|
||||||
|
} else {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
Ok(Some(MidiEditCommand::Toggle))
|
||||||
|
}
|
||||||
|
|
||||||
#[command(Show = "show")]
|
#[command(Show = "show")]
|
||||||
fn show (&mut self, clip: Option<Arc<RwLock<MidiClip>>>) -> Perhaps<MidiEditCommand> {
|
fn edit_show (&mut self, clip: Option<Arc<RwLock<MidiClip>>>) -> Perhaps<MidiEditCommand> {
|
||||||
Ok(self.editor_mut().map(|editor|{
|
Ok(self.editor_mut().as_mut().map(|mut editor|{
|
||||||
editor.set_clip(clip.as_ref());
|
editor.set_clip(clip.as_ref());
|
||||||
editor.redraw();
|
editor.redraw();
|
||||||
None
|
None
|
||||||
|
|
@ -94,16 +61,16 @@ pub trait MidiEditController: HasEditor
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command(DeleteNote = "delete")]
|
#[command(DeleteNote = "delete")]
|
||||||
fn note_delete (&mut self) -> Perhaps<MidiEditCommand> {
|
fn edit_note_delete (&mut self) -> Perhaps<MidiEditCommand> {
|
||||||
Ok(self.editor_mut().map(|editor|{
|
Ok(self.editor_mut().as_mut().map(|editor|{
|
||||||
editor.redraw();
|
editor.redraw();
|
||||||
todo!()
|
todo!()
|
||||||
}).flatten())
|
}).flatten())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command(AppendNote = "append")]
|
#[command(AppendNote = "append")]
|
||||||
fn note_append (&mut self, advance: bool) -> Perhaps<MidiEditCommand> {
|
fn edit_note_append (&mut self, advance: bool) -> Perhaps<MidiEditCommand> {
|
||||||
Ok(self.editor_mut().map(|editor|{
|
Ok(self.editor_mut().as_mut().map(|mut editor|{
|
||||||
editor.put_note(advance);
|
editor.put_note(advance);
|
||||||
editor.redraw();
|
editor.redraw();
|
||||||
None
|
None
|
||||||
|
|
@ -111,8 +78,8 @@ pub trait MidiEditController: HasEditor
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command(SetNotePos = "note-pos")]
|
#[command(SetNotePos = "note-pos")]
|
||||||
fn note_set_pos (&mut self, pos: usize) -> Perhaps<MidiEditCommand> {
|
fn edit_note_set_pos (&mut self, pos: usize) -> Perhaps<MidiEditCommand> {
|
||||||
Ok(self.editor_mut().map(|editor|{
|
Ok(self.editor_mut().as_mut().map(|editor|{
|
||||||
editor.set_note_pos((pos).min(127));
|
editor.set_note_pos((pos).min(127));
|
||||||
editor.redraw();
|
editor.redraw();
|
||||||
None
|
None
|
||||||
|
|
@ -120,8 +87,8 @@ pub trait MidiEditController: HasEditor
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command(SetNoteLen = "note-len")]
|
#[command(SetNoteLen = "note-len")]
|
||||||
fn note_set_len (&mut self, len: usize) -> Perhaps<MidiEditCommand> {
|
fn edit_note_set_len (&mut self, len: usize) -> Perhaps<MidiEditCommand> {
|
||||||
Ok(self.editor_mut().map(|editor|{
|
Ok(self.editor_mut().as_mut().map(|editor|{
|
||||||
editor.set_note_len(len);
|
editor.set_note_len(len);
|
||||||
editor.redraw();
|
editor.redraw();
|
||||||
None
|
None
|
||||||
|
|
@ -129,8 +96,8 @@ pub trait MidiEditController: HasEditor
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command(SetNoteScroll = "note-scroll")]
|
#[command(SetNoteScroll = "note-scroll")]
|
||||||
fn note_set_scroll (&mut self, scroll: usize) -> Perhaps<MidiEditCommand> {
|
fn edit_note_set_scroll (&mut self, scroll: usize) -> Perhaps<MidiEditCommand> {
|
||||||
Ok(self.editor_mut().map(|editor|{
|
Ok(self.editor_mut().as_mut().map(|editor|{
|
||||||
editor.set_note_lo((scroll).min(127));
|
editor.set_note_lo((scroll).min(127));
|
||||||
editor.redraw();
|
editor.redraw();
|
||||||
None
|
None
|
||||||
|
|
@ -138,8 +105,8 @@ pub trait MidiEditController: HasEditor
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command(SetTimePos = "time-pos")]
|
#[command(SetTimePos = "time-pos")]
|
||||||
fn time_set_pos (&mut self, pos: usize) -> Perhaps<MidiEditCommand> {
|
fn edit_time_set_pos (&mut self, pos: usize) -> Perhaps<MidiEditCommand> {
|
||||||
Ok(self.editor_mut().map(|editor|{
|
Ok(self.editor_mut().as_mut().map(|editor|{
|
||||||
editor.set_time_pos(pos);
|
editor.set_time_pos(pos);
|
||||||
editor.redraw();
|
editor.redraw();
|
||||||
None
|
None
|
||||||
|
|
@ -147,8 +114,8 @@ pub trait MidiEditController: HasEditor
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command(SetTimeScroll = "time-scroll")]
|
#[command(SetTimeScroll = "time-scroll")]
|
||||||
fn time_set_scroll (&mut self, scroll: usize) -> Perhaps<MidiEditCommand> {
|
fn edit_time_set_scroll (&mut self, scroll: usize) -> Perhaps<MidiEditCommand> {
|
||||||
Ok(self.editor_mut().map(|editor|{
|
Ok(self.editor_mut().as_mut().map(|editor|{
|
||||||
editor.set_time_start(scroll);
|
editor.set_time_start(scroll);
|
||||||
editor.redraw();
|
editor.redraw();
|
||||||
None
|
None
|
||||||
|
|
@ -156,8 +123,8 @@ pub trait MidiEditController: HasEditor
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command(SetTimeZoom = "time-zoom")]
|
#[command(SetTimeZoom = "time-zoom")]
|
||||||
fn time_set_zoom (&mut self, zoom: usize) -> Perhaps<MidiEditCommand> {
|
fn edit_time_set_zoom (&mut self, zoom: usize) -> Perhaps<MidiEditCommand> {
|
||||||
Ok(self.editor_mut().map(|editor|{
|
Ok(self.editor_mut().as_mut().map(|editor|{
|
||||||
editor.set_time_zoom(zoom);
|
editor.set_time_zoom(zoom);
|
||||||
editor.redraw();
|
editor.redraw();
|
||||||
None
|
None
|
||||||
|
|
@ -165,13 +132,14 @@ pub trait MidiEditController: HasEditor
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command(SetTimeLock = "time-lock")]
|
#[command(SetTimeLock = "time-lock")]
|
||||||
fn time_set_lock (&mut self, lock: bool) -> Perhaps<MidiEditCommand> {
|
fn edit_time_set_lock (&mut self, lock: bool) -> Perhaps<MidiEditCommand> {
|
||||||
Ok(self.editor_mut().map(|editor|{
|
Ok(self.editor_mut().as_mut().map(|editor|{
|
||||||
editor.set_time_lock(lock);
|
editor.set_time_lock(lock);
|
||||||
editor.redraw();
|
editor.redraw();
|
||||||
None
|
None
|
||||||
}).flatten())
|
}).flatten())
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: 1-9 seek markers that by default start every 8th of the clip
|
// TODO: 1-9 seek markers that by default start every 8th of the clip
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -432,3 +400,58 @@ impl Draw<Tui> for MidiEditor {
|
||||||
self.size.of(&self.mode).draw(to)
|
self.size.of(&self.mode).draw(to)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
/// Is a MIDI editor currently focused?
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// tek::App::default().editor_focused();
|
||||||
|
/// ```
|
||||||
|
pub fn editor_focused (&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Toggle MIDI editor.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// tek::App::default().toggle_editor(None);
|
||||||
|
/// ```
|
||||||
|
pub fn toggle_editor (&mut self, value: Option<bool>) {
|
||||||
|
//FIXME: self.editing.store(value.unwrap_or_else(||!self.is_editing()), Relaxed);
|
||||||
|
let value = value.unwrap_or_else(||!self.editor().is_some());
|
||||||
|
if value {
|
||||||
|
// Create new clip in pool when entering empty cell
|
||||||
|
if let Selection::TrackClip { track, scene } = *self.selection()
|
||||||
|
&& let Some(scene) = self.project.scenes.get_mut(scene)
|
||||||
|
&& let Some(slot) = scene.clips.get_mut(track)
|
||||||
|
&& slot.is_none()
|
||||||
|
&& let Some(track) = self.project.tracks.get_mut(track)
|
||||||
|
{
|
||||||
|
let (_index, clip) = self.pool.add_new_clip();
|
||||||
|
// autocolor: new clip colors from scene and track color
|
||||||
|
let color = track.color.base.mix(scene.color.base, 0.5);
|
||||||
|
clip.try_write().unwrap().color = ItemColor::random_near(color, 0.2).into();
|
||||||
|
if let Some(editor) = &mut self.project.editor {
|
||||||
|
editor.set_clip(Some(&clip));
|
||||||
|
}
|
||||||
|
*slot = Some(clip.clone());
|
||||||
|
//Some(clip)
|
||||||
|
} else {
|
||||||
|
//None
|
||||||
|
}
|
||||||
|
} else if let Selection::TrackClip { track, scene } = *self.selection()
|
||||||
|
&& let Some(scene) = self.project.scenes.get_mut(scene)
|
||||||
|
&& let Some(slot) = scene.clips.get_mut(track)
|
||||||
|
&& let Some(clip) = slot.as_mut()
|
||||||
|
{
|
||||||
|
// Remove clip from arrangement when exiting empty clip editor
|
||||||
|
let mut swapped = None;
|
||||||
|
if clip.try_read().unwrap().count_midi_messages() == 0 {
|
||||||
|
std::mem::swap(&mut swapped, slot);
|
||||||
|
}
|
||||||
|
if let Some(clip) = swapped {
|
||||||
|
self.pool.delete_clip(&clip.try_read().unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
72
src/mode.rs
72
src/mode.rs
|
|
@ -1,72 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
/// Group of view and keys definitions.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let mode = tek::Mode::default();
|
|
||||||
/// ```
|
|
||||||
#[derive(Default, Debug)]
|
|
||||||
pub struct Mode {
|
|
||||||
pub path: PathBuf,
|
|
||||||
pub name: Vec<Arc<str>>,
|
|
||||||
pub info: Vec<Arc<str>>,
|
|
||||||
pub view: Vec<Arc<View<Tui, App>>>,
|
|
||||||
pub keys: Vec<Arc<str>>,
|
|
||||||
pub modes: Modes,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Mode {
|
|
||||||
|
|
||||||
/// Add a definition to the mode.
|
|
||||||
///
|
|
||||||
/// Supported definitions:
|
|
||||||
///
|
|
||||||
/// - (name ...) -> name
|
|
||||||
/// - (info ...) -> description
|
|
||||||
/// - (keys ...) -> key bindings
|
|
||||||
/// - (mode ...) -> submode
|
|
||||||
/// - ... -> view
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let mut mode: tek::Mode = tek::Mode::add(tek::Mode::default(), "(name hello)").unwrap();
|
|
||||||
/// ```
|
|
||||||
pub fn add <T: AsMut<Mode>> (mut mode: T, dsl: impl Language) -> Usually<T> {
|
|
||||||
Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() {
|
|
||||||
//println!("Mode::add: {head} {:?}", expr.tail());
|
|
||||||
let tail = expr.tail()?.map(|x|x.trim()).unwrap_or("");
|
|
||||||
match head {
|
|
||||||
"mode" => {
|
|
||||||
let name = tail.head()?.ok_or("submode: missing name")?;
|
|
||||||
let body = tail.tail()?.ok_or("submode: missing body")?;
|
|
||||||
let submode = Mode::default();
|
|
||||||
let submode = body.each(submode, |c,s|Mode::add(c,s))?;
|
|
||||||
let modes = mode.as_mut().modes.clone();
|
|
||||||
modes.0.try_write().unwrap().insert(name.into(), Arc::new(submode));
|
|
||||||
mode
|
|
||||||
},
|
|
||||||
"keys" => {
|
|
||||||
tail.each(mode, |mut mode: T, expr: &str|{
|
|
||||||
mode.as_mut().keys.push(expr.trim().into());
|
|
||||||
Ok(mode)
|
|
||||||
})?
|
|
||||||
},
|
|
||||||
"name" => { mode.as_mut().name.push(tail.into()); mode },
|
|
||||||
"info" => { mode.as_mut().info.push(tail.into()); mode },
|
|
||||||
"view" => { mode.as_mut().view.push(View::new(tail)?.into()); mode },
|
|
||||||
_ => { mode.as_mut().view.push(View::new(tail)?.into()); mode },
|
|
||||||
}
|
|
||||||
} else if let Ok(Some(word)) = dsl.word() {
|
|
||||||
mode.as_mut().view.push(View::new(word)?.into());
|
|
||||||
mode
|
|
||||||
} else {
|
|
||||||
return Err(format!("Mode::add: unexpected: {dsl:?}").into());
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsMut<Mode> for Mode {
|
|
||||||
fn as_mut (&mut self) -> &mut Self {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
pub trait HasModeStack {
|
|
||||||
fn modes (&self) -> RwLockReadGuard<Vec<Arc<str>>>;
|
|
||||||
fn modes_mut (&mut self) -> RwLockWriteGuard<Vec<Arc<str>>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HasModeStack for App {
|
|
||||||
fn modes (&self) -> RwLockReadGuard<Vec<Arc<str>>> {
|
|
||||||
self.modes.try_read().unwrap()
|
|
||||||
}
|
|
||||||
fn modes_mut (&mut self) -> RwLockWriteGuard<Vec<Arc<str>>> {
|
|
||||||
self.modes.try_write().unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> ModeController for T where T: HasModeStack
|
|
||||||
+ Namespace<Vec<Arc<str>>>
|
|
||||||
+ Namespace<Arc<str>>
|
|
||||||
+ Namespace<usize>
|
|
||||||
{}
|
|
||||||
|
|
||||||
#[tek_proc::commands(ModeCommand)]
|
|
||||||
pub trait ModeController: HasModeStack
|
|
||||||
+ Namespace<Vec<Arc<str>>>
|
|
||||||
+ Namespace<Arc<str>>
|
|
||||||
+ Namespace<usize>
|
|
||||||
{
|
|
||||||
#[command(Reset = "reset")]
|
|
||||||
fn mode_reset (&mut self, modes: Vec<Arc<str>>) -> Perhaps<ModeCommand> {
|
|
||||||
*self.modes_mut() = modes.into();
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[command(Push = "push")]
|
|
||||||
fn mode_push (&mut self, mode: Arc<str>) -> Perhaps<ModeCommand> {
|
|
||||||
self.modes_mut().push(mode);
|
|
||||||
Ok(Some(ModeCommand::Pop))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[command(Pop = "pop")]
|
|
||||||
fn mode_pop (&mut self) -> Perhaps<ModeCommand> {
|
|
||||||
let mode = self.modes_mut().pop();
|
|
||||||
Ok(mode.map(|mode|ModeCommand::Push { mode }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
45
src/modes.rs
45
src/modes.rs
|
|
@ -1,45 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
/// Collection of UI modes.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let mut modes = tek::Modes::default();
|
|
||||||
/// assert_eq!(modes.len(), 0);
|
|
||||||
/// let _ = modes.add(&":foo", &"(mode (name Foo) (info Bar))");
|
|
||||||
/// assert_eq!(modes.len(), 1);
|
|
||||||
/// ```
|
|
||||||
#[derive(Default, Debug, Clone)]
|
|
||||||
pub struct Modes(
|
|
||||||
pub Arc<RwLock<BTreeMap<Arc<str>, Arc<Mode>>>>
|
|
||||||
);
|
|
||||||
|
|
||||||
impl Modes {
|
|
||||||
|
|
||||||
/// Register a mode.
|
|
||||||
pub fn add <'a> (modes: &Modes, expr: impl Language) -> UsuallyRef<'a, ()> {
|
|
||||||
let name = expr.head()?.ok_or("mode: missing name")?;
|
|
||||||
let body = expr.tail()?.ok_or("mode: missing body")?;
|
|
||||||
let mode = Mode::default();
|
|
||||||
let mode = body.each(mode, |c, s|Mode::add(c, s))?;
|
|
||||||
modes.0.try_write().unwrap().insert(name.into(), Arc::new(mode));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a mode by name.
|
|
||||||
pub fn get (&self, name: impl AsRef<str>) -> Option<Arc<Mode>> {
|
|
||||||
self.0.try_read().unwrap().get(name.as_ref()).cloned()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Run something for each mode.
|
|
||||||
pub fn for_each <T> (&self, mut ator: impl FnMut(&str, &Mode)->T) {
|
|
||||||
for (k, v) in self.0.try_read().unwrap().iter() {
|
|
||||||
let _ = ator(k.as_ref(), v.as_ref());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Count modes.
|
|
||||||
pub fn len (&self) -> usize {
|
|
||||||
self.0.try_read().unwrap().len()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -54,7 +54,7 @@ pub struct Project {
|
||||||
/// Display size of clips area
|
/// Display size of clips area
|
||||||
pub size_inner: Sizer,
|
pub size_inner: Sizer,
|
||||||
/// Selected UI element
|
/// Selected UI element
|
||||||
pub selection: Selection,
|
pub selection: Selection,
|
||||||
/// Source of time
|
/// Source of time
|
||||||
#[cfg(feature = "clock")] pub clock: Clock,
|
#[cfg(feature = "clock")] pub clock: Clock,
|
||||||
/// Allows one MIDI clip to be edited
|
/// Allows one MIDI clip to be edited
|
||||||
|
|
@ -138,12 +138,15 @@ impl Project {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HasJack<'static> for Project { fn jack (&self) -> &Jack<'static> { &self.jack } }
|
impl HasJack<'static> for Project { fn jack (&self) -> &Jack<'static> { &self.jack } }
|
||||||
impl_has!(Jack<'static>: |self: Project| self.jack);
|
impl_has!(Jack<'static>: |self: Project| self.jack);
|
||||||
impl_has!(Sizer: |self: Project| self.size);
|
impl_has!(Sizer: |self: Project| self.size);
|
||||||
impl_has!(Clock: |self: Project| self.clock);
|
impl_has!(Clock: |self: Project| self.clock);
|
||||||
impl_has!(Selection: |self: Project| self.selection);
|
impl_has!(Selection: |self: Project| self.selection);
|
||||||
impl_as_ref_opt!(MidiEditor: |self: Project| self.editor.as_ref());
|
impl_has!(Vec<Scene>: |self: Project| self.scenes);
|
||||||
impl_as_mut_opt!(MidiEditor: |self: Project| self.editor.as_mut());
|
impl_as_ref_opt!(Scene: |self: Project| self.selected_scene());
|
||||||
|
impl_as_mut_opt!(Scene: |self: Project| self.selected_scene_mut());
|
||||||
|
impl_as_ref!(Option<MidiEditor>: |self: Project| &self.editor);
|
||||||
|
impl_as_mut!(Option<MidiEditor>: |self: Project| &mut self.editor);
|
||||||
|
|
||||||
pub fn swap_value <T: Clone + PartialEq, U> (
|
pub fn swap_value <T: Clone + PartialEq, U> (
|
||||||
target: &mut T, value: &T, returned: impl Fn(T)->U
|
target: &mut T, value: &T, returned: impl Fn(T)->U
|
||||||
|
|
|
||||||
144
src/sample.rs
Normal file
144
src/sample.rs
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// A sound cut.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let sample = tek::Sample::default();
|
||||||
|
/// let sample = tek::Sample::new("test", 0, 0, vec![]);
|
||||||
|
/// ```
|
||||||
|
#[derive(Default, Debug)] pub struct Sample {
|
||||||
|
pub name: Arc<str>,
|
||||||
|
pub start: usize,
|
||||||
|
pub end: usize,
|
||||||
|
pub channels: Vec<Vec<f32>>,
|
||||||
|
pub rate: Option<usize>,
|
||||||
|
pub gain: f32,
|
||||||
|
pub color: ItemTheme,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sample {
|
||||||
|
pub fn new (name: impl AsRef<str>, start: usize, end: usize, channels: Vec<Vec<f32>>) -> Self {
|
||||||
|
Self {
|
||||||
|
name: name.as_ref().into(),
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
channels,
|
||||||
|
rate: None,
|
||||||
|
gain: 1.0,
|
||||||
|
color: ItemTheme::random(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn play (sample: &Arc<RwLock<Self>>, after: usize, velocity: &u7) -> Voice {
|
||||||
|
Voice {
|
||||||
|
sample: sample.clone(),
|
||||||
|
after,
|
||||||
|
position: sample.try_read().unwrap().start,
|
||||||
|
velocity: velocity.as_int() as f32 / 127.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn handle_cc (&mut self, controller: u7, value: u7) {
|
||||||
|
let percentage = value.as_int() as f64 / 127.;
|
||||||
|
match controller.as_int() {
|
||||||
|
20 => {
|
||||||
|
self.start = (percentage * self.end as f64) as usize;
|
||||||
|
},
|
||||||
|
21 => {
|
||||||
|
let length = self.channels[0].len();
|
||||||
|
self.end = length.min(
|
||||||
|
self.start + (percentage * (length as f64 - self.start as f64)) as usize
|
||||||
|
);
|
||||||
|
},
|
||||||
|
22 => { /*attack*/ },
|
||||||
|
23 => { /*decay*/ },
|
||||||
|
24 => {
|
||||||
|
self.gain = percentage as f32 * 2.0;
|
||||||
|
},
|
||||||
|
26 => { /* pan */ }
|
||||||
|
25 => { /* pitch */ }
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Read WAV from file
|
||||||
|
pub fn read_data (src: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
|
||||||
|
let mut channels: Vec<wavers::Samples<f32>> = vec![];
|
||||||
|
for channel in wavers::Wav::from_path(src)?.channels() {
|
||||||
|
channels.push(channel);
|
||||||
|
}
|
||||||
|
let mut end = 0;
|
||||||
|
let mut data: Vec<Vec<f32>> = vec![];
|
||||||
|
for samples in channels.iter() {
|
||||||
|
let channel = Vec::from(samples.as_ref());
|
||||||
|
end = end.max(channel.len());
|
||||||
|
data.push(channel);
|
||||||
|
}
|
||||||
|
Ok((end, data))
|
||||||
|
}
|
||||||
|
pub fn from_file (path: &PathBuf) -> Usually<Self> {
|
||||||
|
let name = path.file_name().unwrap().to_string_lossy().into();
|
||||||
|
let mut sample = Self { name, ..Default::default() };
|
||||||
|
// Use file extension if present
|
||||||
|
let mut hint = Hint::new();
|
||||||
|
if let Some(ext) = path.extension() {
|
||||||
|
hint.with_extension(&ext.to_string_lossy());
|
||||||
|
}
|
||||||
|
let probed = symphonia::default::get_probe().format(
|
||||||
|
&hint,
|
||||||
|
MediaSourceStream::new(
|
||||||
|
Box::new(File::open(path)?),
|
||||||
|
Default::default(),
|
||||||
|
),
|
||||||
|
&Default::default(),
|
||||||
|
&Default::default()
|
||||||
|
)?;
|
||||||
|
let mut format = probed.format;
|
||||||
|
let params = &format.tracks().iter()
|
||||||
|
.find(|t| t.codec_params.codec != CODEC_TYPE_NULL)
|
||||||
|
.expect("no tracks found")
|
||||||
|
.codec_params;
|
||||||
|
let mut decoder = get_codecs().make(params, &Default::default())?;
|
||||||
|
loop {
|
||||||
|
match format.next_packet() {
|
||||||
|
Ok(packet) => sample.decode_packet(&mut decoder, packet)?,
|
||||||
|
Err(symphonia::core::errors::Error::IoError(_)) => break decoder.last_decoded(),
|
||||||
|
Err(err) => return Err(err.into()),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
sample.end = sample.channels.iter().fold(0, |l, c|l + c.len());
|
||||||
|
Ok(sample)
|
||||||
|
}
|
||||||
|
fn decode_packet (
|
||||||
|
&mut self, decoder: &mut Box<dyn Decoder>, packet: Packet
|
||||||
|
) -> Usually<()> {
|
||||||
|
// Decode a packet
|
||||||
|
let decoded = decoder
|
||||||
|
.decode(&packet)
|
||||||
|
.map_err(|e|Box::<dyn std::error::Error>::from(e))?;
|
||||||
|
// Determine sample rate
|
||||||
|
let spec = *decoded.spec();
|
||||||
|
if let Some(rate) = self.rate {
|
||||||
|
if rate != spec.rate as usize {
|
||||||
|
panic!("sample rate changed");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.rate = Some(spec.rate as usize);
|
||||||
|
}
|
||||||
|
// Determine channel count
|
||||||
|
while self.channels.len() < spec.channels.count() {
|
||||||
|
self.channels.push(vec![]);
|
||||||
|
}
|
||||||
|
// Load sample
|
||||||
|
let mut samples = SampleBuffer::new(
|
||||||
|
decoded.frames() as u64,
|
||||||
|
spec
|
||||||
|
);
|
||||||
|
if samples.capacity() > 0 {
|
||||||
|
samples.copy_interleaved_ref(decoded);
|
||||||
|
for frame in samples.samples().chunks(spec.channels.count()) {
|
||||||
|
for (chan, frame) in frame.iter().enumerate() {
|
||||||
|
self.channels[chan].push(*frame)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
128
src/sample_add.rs
Normal file
128
src/sample_add.rs
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
#[derive(Default, Debug)] pub struct SampleAdd {
|
||||||
|
pub exited: bool,
|
||||||
|
pub dir: PathBuf,
|
||||||
|
pub subdirs: Vec<OsString>,
|
||||||
|
pub files: Vec<OsString>,
|
||||||
|
pub cursor: usize,
|
||||||
|
pub offset: usize,
|
||||||
|
pub sample: Arc<RwLock<Sample>>,
|
||||||
|
pub voices: Arc<RwLock<Vec<Voice>>>,
|
||||||
|
pub _search: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_draw!(|self: SampleAdd, to: Tui|{ todo!() });
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
impl SampleAdd {
|
||||||
|
fn exited (&self) -> bool {
|
||||||
|
self.exited
|
||||||
|
}
|
||||||
|
fn exit (&mut self) {
|
||||||
|
self.exited = true
|
||||||
|
}
|
||||||
|
pub fn new (
|
||||||
|
sample: &Arc<RwLock<Sample>>,
|
||||||
|
voices: &Arc<RwLock<Vec<Voice>>>
|
||||||
|
) -> Usually<Self> {
|
||||||
|
let dir = std::env::current_dir()?;
|
||||||
|
let (subdirs, files) = scan(&dir)?;
|
||||||
|
Ok(Self {
|
||||||
|
exited: false,
|
||||||
|
dir,
|
||||||
|
subdirs,
|
||||||
|
files,
|
||||||
|
cursor: 0,
|
||||||
|
offset: 0,
|
||||||
|
sample: sample.clone(),
|
||||||
|
voices: voices.clone(),
|
||||||
|
_search: None
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn rescan (&mut self) -> Usually<()> {
|
||||||
|
scan(&self.dir).map(|(subdirs, files)|{
|
||||||
|
self.subdirs = subdirs;
|
||||||
|
self.files = files;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn prev (&mut self) {
|
||||||
|
self.cursor = self.cursor.saturating_sub(1);
|
||||||
|
}
|
||||||
|
fn next (&mut self) {
|
||||||
|
self.cursor = self.cursor + 1;
|
||||||
|
}
|
||||||
|
fn try_preview (&mut self) -> Usually<()> {
|
||||||
|
if let Some(path) = self.cursor_file() {
|
||||||
|
if let Ok(sample) = Sample::from_file(&path) {
|
||||||
|
*self.sample.try_write().unwrap() = sample;
|
||||||
|
self.voices.try_write().unwrap().push(
|
||||||
|
Sample::play(&self.sample, 0, &u7::from(100u8))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
//load_sample(&path)?;
|
||||||
|
//let src = std::fs::File::open(&path)?;
|
||||||
|
//let mss = MediaSourceStream::new(Box::new(src), Default::default());
|
||||||
|
//let mut hint = Hint::new();
|
||||||
|
//if let Some(ext) = path.extension() {
|
||||||
|
//hint.with_extension(&ext.to_string_lossy());
|
||||||
|
//}
|
||||||
|
//let meta_opts: MetadataOptions = Default::default();
|
||||||
|
//let fmt_opts: FormatOptions = Default::default();
|
||||||
|
//if let Ok(mut probed) = symphonia::default::get_probe()
|
||||||
|
//.format(&hint, mss, &fmt_opts, &meta_opts)
|
||||||
|
//{
|
||||||
|
//panic!("{:?}", probed.format.metadata());
|
||||||
|
//};
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn cursor_dir (&self) -> Option<PathBuf> {
|
||||||
|
if self.cursor < self.subdirs.len() {
|
||||||
|
Some(self.dir.join(&self.subdirs[self.cursor]))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn cursor_file (&self) -> Option<PathBuf> {
|
||||||
|
if self.cursor < self.subdirs.len() {
|
||||||
|
return None
|
||||||
|
}
|
||||||
|
let index = self.cursor.saturating_sub(self.subdirs.len());
|
||||||
|
if index < self.files.len() {
|
||||||
|
Some(self.dir.join(&self.files[index]))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn pick (&mut self) -> Usually<bool> {
|
||||||
|
if self.cursor == 0 {
|
||||||
|
if let Some(parent) = self.dir.parent() {
|
||||||
|
self.dir = parent.into();
|
||||||
|
self.rescan()?;
|
||||||
|
self.cursor = 0;
|
||||||
|
return Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(dir) = self.cursor_dir() {
|
||||||
|
self.dir = dir;
|
||||||
|
self.rescan()?;
|
||||||
|
self.cursor = 0;
|
||||||
|
return Ok(false)
|
||||||
|
}
|
||||||
|
if let Some(path) = self.cursor_file() {
|
||||||
|
let (end, channels) = read_sample_data(&path.to_string_lossy())?;
|
||||||
|
let mut sample = self.sample.try_write().unwrap();
|
||||||
|
sample.name = path.file_name().unwrap().to_string_lossy().into();
|
||||||
|
sample.end = end;
|
||||||
|
sample.channels = channels;
|
||||||
|
return Ok(true)
|
||||||
|
}
|
||||||
|
return Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
fn read_sample_data (_: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
26
src/sample_kit.rs
Normal file
26
src/sample_kit.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// Collection of samples, one per slot, fixed number of slots.
|
||||||
|
///
|
||||||
|
/// History: Separated to cleanly implement [Default].
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let samples = tek::SampleKit([None, None, None, None]);
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug)] pub struct SampleKit <const N: usize> (
|
||||||
|
pub [Option<Arc<RwLock<Sample>>>;N]
|
||||||
|
);
|
||||||
|
|
||||||
|
impl<const N: usize> Default for SampleKit<N> {
|
||||||
|
fn default () -> Self { Self([const { None }; N]) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> SampleKit<N> {
|
||||||
|
pub fn get (&self, index: usize) -> &Option<Arc<RwLock<Sample>>> {
|
||||||
|
if index < self.0.len() {
|
||||||
|
&self.0[index]
|
||||||
|
} else {
|
||||||
|
&None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/sample_voice.rs
Normal file
30
src/sample_voice.rs
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// A currently playing instance of a sample.
|
||||||
|
#[derive(Default, Debug, Clone)] pub struct Voice {
|
||||||
|
pub sample: Arc<RwLock<Sample>>,
|
||||||
|
pub after: usize,
|
||||||
|
pub position: usize,
|
||||||
|
pub velocity: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for Voice {
|
||||||
|
type Item = [f32;2];
|
||||||
|
fn next (&mut self) -> Option<Self::Item> {
|
||||||
|
if self.after > 0 {
|
||||||
|
self.after -= 1;
|
||||||
|
return Some([0.0, 0.0])
|
||||||
|
}
|
||||||
|
let sample = self.sample.try_read().unwrap();
|
||||||
|
if self.position < sample.end {
|
||||||
|
let position = self.position;
|
||||||
|
self.position += 1;
|
||||||
|
return sample.channels[0].get(position).map(|_amplitude|[
|
||||||
|
sample.channels[0][position] * self.velocity * sample.gain,
|
||||||
|
sample.channels[0][position] * self.velocity * sample.gain,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
345
src/sampler.rs
345
src/sampler.rs
|
|
@ -8,18 +8,6 @@ pub(crate) use symphonia::{
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Get the first sampler of the active track
|
|
||||||
impl Project {
|
|
||||||
pub fn sampler (&self) -> Option<&Sampler> {
|
|
||||||
self.selected_track()?.sampler(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the first sampler of the active track
|
|
||||||
pub fn sampler_mut (&mut self) -> Option<&mut Sampler> {
|
|
||||||
self.selected_track_mut()?.sampler_mut(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait HasSampler: AsRef<Sampler> + AsMut<Sampler> {
|
pub trait HasSampler: AsRef<Sampler> + AsMut<Sampler> {
|
||||||
fn sampler (&self) -> &Sampler {
|
fn sampler (&self) -> &Sampler {
|
||||||
self.as_ref()
|
self.as_ref()
|
||||||
|
|
@ -441,327 +429,6 @@ fn draw_sample (
|
||||||
Ok(label1.len() + label2.len() + 4)
|
Ok(label1.len() + label2.len() + 4)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A currently playing instance of a sample.
|
|
||||||
#[derive(Default, Debug, Clone)] pub struct Voice {
|
|
||||||
pub sample: Arc<RwLock<Sample>>,
|
|
||||||
pub after: usize,
|
|
||||||
pub position: usize,
|
|
||||||
pub velocity: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Iterator for Voice {
|
|
||||||
type Item = [f32;2];
|
|
||||||
fn next (&mut self) -> Option<Self::Item> {
|
|
||||||
if self.after > 0 {
|
|
||||||
self.after -= 1;
|
|
||||||
return Some([0.0, 0.0])
|
|
||||||
}
|
|
||||||
let sample = self.sample.try_read().unwrap();
|
|
||||||
if self.position < sample.end {
|
|
||||||
let position = self.position;
|
|
||||||
self.position += 1;
|
|
||||||
return sample.channels[0].get(position).map(|_amplitude|[
|
|
||||||
sample.channels[0][position] * self.velocity * sample.gain,
|
|
||||||
sample.channels[0][position] * self.velocity * sample.gain,
|
|
||||||
])
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Collection of samples, one per slot, fixed number of slots.
|
|
||||||
///
|
|
||||||
/// History: Separated to cleanly implement [Default].
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let samples = tek::SampleKit([None, None, None, None]);
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug)] pub struct SampleKit <const N: usize> (
|
|
||||||
pub [Option<Arc<RwLock<Sample>>>;N]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl<const N: usize> Default for SampleKit<N> {
|
|
||||||
fn default () -> Self { Self([const { None }; N]) }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<const N: usize> SampleKit<N> {
|
|
||||||
pub fn get (&self, index: usize) -> &Option<Arc<RwLock<Sample>>> {
|
|
||||||
if index < self.0.len() {
|
|
||||||
&self.0[index]
|
|
||||||
} else {
|
|
||||||
&None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A sound cut.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let sample = tek::Sample::default();
|
|
||||||
/// let sample = tek::Sample::new("test", 0, 0, vec![]);
|
|
||||||
/// ```
|
|
||||||
#[derive(Default, Debug)] pub struct Sample {
|
|
||||||
pub name: Arc<str>,
|
|
||||||
pub start: usize,
|
|
||||||
pub end: usize,
|
|
||||||
pub channels: Vec<Vec<f32>>,
|
|
||||||
pub rate: Option<usize>,
|
|
||||||
pub gain: f32,
|
|
||||||
pub color: ItemTheme,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Sample {
|
|
||||||
pub fn new (name: impl AsRef<str>, start: usize, end: usize, channels: Vec<Vec<f32>>) -> Self {
|
|
||||||
Self {
|
|
||||||
name: name.as_ref().into(),
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
channels,
|
|
||||||
rate: None,
|
|
||||||
gain: 1.0,
|
|
||||||
color: ItemTheme::random(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn play (sample: &Arc<RwLock<Self>>, after: usize, velocity: &u7) -> Voice {
|
|
||||||
Voice {
|
|
||||||
sample: sample.clone(),
|
|
||||||
after,
|
|
||||||
position: sample.try_read().unwrap().start,
|
|
||||||
velocity: velocity.as_int() as f32 / 127.0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn handle_cc (&mut self, controller: u7, value: u7) {
|
|
||||||
let percentage = value.as_int() as f64 / 127.;
|
|
||||||
match controller.as_int() {
|
|
||||||
20 => {
|
|
||||||
self.start = (percentage * self.end as f64) as usize;
|
|
||||||
},
|
|
||||||
21 => {
|
|
||||||
let length = self.channels[0].len();
|
|
||||||
self.end = length.min(
|
|
||||||
self.start + (percentage * (length as f64 - self.start as f64)) as usize
|
|
||||||
);
|
|
||||||
},
|
|
||||||
22 => { /*attack*/ },
|
|
||||||
23 => { /*decay*/ },
|
|
||||||
24 => {
|
|
||||||
self.gain = percentage as f32 * 2.0;
|
|
||||||
},
|
|
||||||
26 => { /* pan */ }
|
|
||||||
25 => { /* pitch */ }
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Read WAV from file
|
|
||||||
pub fn read_data (src: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
|
|
||||||
let mut channels: Vec<wavers::Samples<f32>> = vec![];
|
|
||||||
for channel in wavers::Wav::from_path(src)?.channels() {
|
|
||||||
channels.push(channel);
|
|
||||||
}
|
|
||||||
let mut end = 0;
|
|
||||||
let mut data: Vec<Vec<f32>> = vec![];
|
|
||||||
for samples in channels.iter() {
|
|
||||||
let channel = Vec::from(samples.as_ref());
|
|
||||||
end = end.max(channel.len());
|
|
||||||
data.push(channel);
|
|
||||||
}
|
|
||||||
Ok((end, data))
|
|
||||||
}
|
|
||||||
pub fn from_file (path: &PathBuf) -> Usually<Self> {
|
|
||||||
let name = path.file_name().unwrap().to_string_lossy().into();
|
|
||||||
let mut sample = Self { name, ..Default::default() };
|
|
||||||
// Use file extension if present
|
|
||||||
let mut hint = Hint::new();
|
|
||||||
if let Some(ext) = path.extension() {
|
|
||||||
hint.with_extension(&ext.to_string_lossy());
|
|
||||||
}
|
|
||||||
let probed = symphonia::default::get_probe().format(
|
|
||||||
&hint,
|
|
||||||
MediaSourceStream::new(
|
|
||||||
Box::new(File::open(path)?),
|
|
||||||
Default::default(),
|
|
||||||
),
|
|
||||||
&Default::default(),
|
|
||||||
&Default::default()
|
|
||||||
)?;
|
|
||||||
let mut format = probed.format;
|
|
||||||
let params = &format.tracks().iter()
|
|
||||||
.find(|t| t.codec_params.codec != CODEC_TYPE_NULL)
|
|
||||||
.expect("no tracks found")
|
|
||||||
.codec_params;
|
|
||||||
let mut decoder = get_codecs().make(params, &Default::default())?;
|
|
||||||
loop {
|
|
||||||
match format.next_packet() {
|
|
||||||
Ok(packet) => sample.decode_packet(&mut decoder, packet)?,
|
|
||||||
Err(symphonia::core::errors::Error::IoError(_)) => break decoder.last_decoded(),
|
|
||||||
Err(err) => return Err(err.into()),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
sample.end = sample.channels.iter().fold(0, |l, c|l + c.len());
|
|
||||||
Ok(sample)
|
|
||||||
}
|
|
||||||
fn decode_packet (
|
|
||||||
&mut self, decoder: &mut Box<dyn Decoder>, packet: Packet
|
|
||||||
) -> Usually<()> {
|
|
||||||
// Decode a packet
|
|
||||||
let decoded = decoder
|
|
||||||
.decode(&packet)
|
|
||||||
.map_err(|e|Box::<dyn std::error::Error>::from(e))?;
|
|
||||||
// Determine sample rate
|
|
||||||
let spec = *decoded.spec();
|
|
||||||
if let Some(rate) = self.rate {
|
|
||||||
if rate != spec.rate as usize {
|
|
||||||
panic!("sample rate changed");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.rate = Some(spec.rate as usize);
|
|
||||||
}
|
|
||||||
// Determine channel count
|
|
||||||
while self.channels.len() < spec.channels.count() {
|
|
||||||
self.channels.push(vec![]);
|
|
||||||
}
|
|
||||||
// Load sample
|
|
||||||
let mut samples = SampleBuffer::new(
|
|
||||||
decoded.frames() as u64,
|
|
||||||
spec
|
|
||||||
);
|
|
||||||
if samples.capacity() > 0 {
|
|
||||||
samples.copy_interleaved_ref(decoded);
|
|
||||||
for frame in samples.samples().chunks(spec.channels.count()) {
|
|
||||||
for (chan, frame) in frame.iter().enumerate() {
|
|
||||||
self.channels[chan].push(*frame)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug)] pub struct SampleAdd {
|
|
||||||
pub exited: bool,
|
|
||||||
pub dir: PathBuf,
|
|
||||||
pub subdirs: Vec<OsString>,
|
|
||||||
pub files: Vec<OsString>,
|
|
||||||
pub cursor: usize,
|
|
||||||
pub offset: usize,
|
|
||||||
pub sample: Arc<RwLock<Sample>>,
|
|
||||||
pub voices: Arc<RwLock<Vec<Voice>>>,
|
|
||||||
pub _search: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl_draw!(|self: SampleAdd, to: Tui|{ todo!() });
|
|
||||||
|
|
||||||
impl SampleAdd {
|
|
||||||
fn exited (&self) -> bool {
|
|
||||||
self.exited
|
|
||||||
}
|
|
||||||
fn exit (&mut self) {
|
|
||||||
self.exited = true
|
|
||||||
}
|
|
||||||
pub fn new (
|
|
||||||
sample: &Arc<RwLock<Sample>>,
|
|
||||||
voices: &Arc<RwLock<Vec<Voice>>>
|
|
||||||
) -> Usually<Self> {
|
|
||||||
let dir = std::env::current_dir()?;
|
|
||||||
let (subdirs, files) = scan(&dir)?;
|
|
||||||
Ok(Self {
|
|
||||||
exited: false,
|
|
||||||
dir,
|
|
||||||
subdirs,
|
|
||||||
files,
|
|
||||||
cursor: 0,
|
|
||||||
offset: 0,
|
|
||||||
sample: sample.clone(),
|
|
||||||
voices: voices.clone(),
|
|
||||||
_search: None
|
|
||||||
})
|
|
||||||
}
|
|
||||||
fn rescan (&mut self) -> Usually<()> {
|
|
||||||
scan(&self.dir).map(|(subdirs, files)|{
|
|
||||||
self.subdirs = subdirs;
|
|
||||||
self.files = files;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
fn prev (&mut self) {
|
|
||||||
self.cursor = self.cursor.saturating_sub(1);
|
|
||||||
}
|
|
||||||
fn next (&mut self) {
|
|
||||||
self.cursor = self.cursor + 1;
|
|
||||||
}
|
|
||||||
fn try_preview (&mut self) -> Usually<()> {
|
|
||||||
if let Some(path) = self.cursor_file() {
|
|
||||||
if let Ok(sample) = Sample::from_file(&path) {
|
|
||||||
*self.sample.try_write().unwrap() = sample;
|
|
||||||
self.voices.try_write().unwrap().push(
|
|
||||||
Sample::play(&self.sample, 0, &u7::from(100u8))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
//load_sample(&path)?;
|
|
||||||
//let src = std::fs::File::open(&path)?;
|
|
||||||
//let mss = MediaSourceStream::new(Box::new(src), Default::default());
|
|
||||||
//let mut hint = Hint::new();
|
|
||||||
//if let Some(ext) = path.extension() {
|
|
||||||
//hint.with_extension(&ext.to_string_lossy());
|
|
||||||
//}
|
|
||||||
//let meta_opts: MetadataOptions = Default::default();
|
|
||||||
//let fmt_opts: FormatOptions = Default::default();
|
|
||||||
//if let Ok(mut probed) = symphonia::default::get_probe()
|
|
||||||
//.format(&hint, mss, &fmt_opts, &meta_opts)
|
|
||||||
//{
|
|
||||||
//panic!("{:?}", probed.format.metadata());
|
|
||||||
//};
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
fn cursor_dir (&self) -> Option<PathBuf> {
|
|
||||||
if self.cursor < self.subdirs.len() {
|
|
||||||
Some(self.dir.join(&self.subdirs[self.cursor]))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn cursor_file (&self) -> Option<PathBuf> {
|
|
||||||
if self.cursor < self.subdirs.len() {
|
|
||||||
return None
|
|
||||||
}
|
|
||||||
let index = self.cursor.saturating_sub(self.subdirs.len());
|
|
||||||
if index < self.files.len() {
|
|
||||||
Some(self.dir.join(&self.files[index]))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn pick (&mut self) -> Usually<bool> {
|
|
||||||
if self.cursor == 0 {
|
|
||||||
if let Some(parent) = self.dir.parent() {
|
|
||||||
self.dir = parent.into();
|
|
||||||
self.rescan()?;
|
|
||||||
self.cursor = 0;
|
|
||||||
return Ok(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(dir) = self.cursor_dir() {
|
|
||||||
self.dir = dir;
|
|
||||||
self.rescan()?;
|
|
||||||
self.cursor = 0;
|
|
||||||
return Ok(false)
|
|
||||||
}
|
|
||||||
if let Some(path) = self.cursor_file() {
|
|
||||||
let (end, channels) = read_sample_data(&path.to_string_lossy())?;
|
|
||||||
let mut sample = self.sample.try_write().unwrap();
|
|
||||||
sample.name = path.file_name().unwrap().to_string_lossy().into();
|
|
||||||
sample.end = end;
|
|
||||||
sample.channels = channels;
|
|
||||||
return Ok(true)
|
|
||||||
}
|
|
||||||
return Ok(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_sample_data (_: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
|
|
||||||
todo!();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn view_sample_info (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> + use<'_> {
|
pub fn view_sample_info (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> + use<'_> {
|
||||||
when(sample.is_some(), draw(move|to: &mut Tui|{
|
when(sample.is_some(), draw(move|to: &mut Tui|{
|
||||||
let sample = sample.unwrap().try_read().unwrap();
|
let sample = sample.unwrap().try_read().unwrap();
|
||||||
|
|
@ -809,6 +476,18 @@ pub fn view_sample_status (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tu
|
||||||
.unwrap_or_else(||"No sample".to_string())))
|
.unwrap_or_else(||"No sample".to_string())))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the first sampler of the active track
|
||||||
|
impl Project {
|
||||||
|
pub fn sampler (&self) -> Option<&Sampler> {
|
||||||
|
self.selected_track()?.sampler(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the first sampler of the active track
|
||||||
|
pub fn sampler_mut (&mut self) -> Option<&mut Sampler> {
|
||||||
|
self.selected_track_mut()?.sampler_mut(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "track")]
|
#[cfg(feature = "track")]
|
||||||
impl Track {
|
impl Track {
|
||||||
/// Create a new track connecting the [Sequencer] to a [Sampler].
|
/// Create a new track connecting the [Sequencer] to a [Sampler].
|
||||||
|
|
|
||||||
57
src/scene.rs
57
src/scene.rs
|
|
@ -55,10 +55,6 @@ impl Scene {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_as_ref_opt!(Scene: |self: App| self.project.as_ref_opt());
|
|
||||||
impl_as_mut_opt!(Scene: |self: App| self.project.as_mut_opt());
|
|
||||||
impl_as_ref_opt!(Scene: |self: Project| self.selected_scene());
|
|
||||||
impl_as_mut_opt!(Scene: |self: Project| self.selected_scene_mut());
|
|
||||||
|
|
||||||
impl<T: AsRefOpt<Scene> + AsMutOpt<Scene> + Send + Sync> HasScene for T {}
|
impl<T: AsRefOpt<Scene> + AsMutOpt<Scene> + Send + Sync> HasScene for T {}
|
||||||
|
|
||||||
|
|
@ -123,9 +119,10 @@ pub trait SceneController: HasScene
|
||||||
pub type SceneWith<'a, T> = (usize, &'a Scene, usize, usize, T);
|
pub type SceneWith<'a, T> = (usize, &'a Scene, usize, usize, T);
|
||||||
|
|
||||||
def_sizes_iter!(ScenesSizes => Scene);
|
def_sizes_iter!(ScenesSizes => Scene);
|
||||||
impl_has!(Vec<Scene>: |self: Project| self.scenes);
|
|
||||||
impl_as_ref!(Vec<Scene>: |self: App| self.project.as_ref());
|
impl_as_ref!(Vec<Scene>: |self: App| self.project.as_ref());
|
||||||
impl_as_mut!(Vec<Scene>: |self: App| self.project.as_mut());
|
impl_as_mut!(Vec<Scene>: |self: App| self.project.as_mut());
|
||||||
|
impl_as_ref_opt!(Scene: |self: App| self.project.as_ref_opt());
|
||||||
|
impl_as_mut_opt!(Scene: |self: App| self.project.as_mut_opt());
|
||||||
|
|
||||||
impl<T: AsRef<Vec<Scene>> + AsMut<Vec<Scene>>> HasScenes for T {}
|
impl<T: AsRef<Vec<Scene>> + AsMut<Vec<Scene>>> HasScenes for T {}
|
||||||
|
|
||||||
|
|
@ -236,7 +233,7 @@ pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize +
|
||||||
let editor = self.editor();
|
let editor = self.editor();
|
||||||
let editing = self.is_editing();
|
let editing = self.is_editing();
|
||||||
iter_south(move||self.scenes_with_sizes().map(move|(index, scene, ..)|{
|
iter_south(move||self.scenes_with_sizes().map(move|(index, scene, ..)|{
|
||||||
view_scene_name(select, editor, index, scene, editing)
|
view_scene_name(select, editor.as_ref(), index, scene, editing)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -322,3 +319,51 @@ impl HasSceneScroll for App {
|
||||||
self.project.scene_scroll()
|
self.project.scene_scroll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn view_scene_name_theme (scene: &Scene, track_index: usize) -> (Arc<str>, ItemTheme) {
|
||||||
|
if let Some(Some(clip)) = &scene.clips.get(track_index) {
|
||||||
|
let clip = clip.try_read().unwrap();
|
||||||
|
(format!(" ⏹ {}", &clip.name).into(), clip.color)
|
||||||
|
} else {
|
||||||
|
(" ⏹ -- ".into(), ItemTheme::G[32])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn view_scene_bg (
|
||||||
|
theme: ItemTheme, select: &Selection, track_index: usize, scene_index: usize
|
||||||
|
) -> (Color, Color) {
|
||||||
|
let mut outline = theme.base.term;
|
||||||
|
(if select.track() == Some(track_index) && select.scene() == Some(scene_index) {
|
||||||
|
outline = theme.lighter.term;
|
||||||
|
theme.light.term
|
||||||
|
} else if select.track() == Some(track_index) || select.scene() == Some(scene_index) {
|
||||||
|
outline = theme.darkest.term;
|
||||||
|
theme.base.term
|
||||||
|
} else {
|
||||||
|
theme.dark.term
|
||||||
|
}, outline)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn view_scene_w (
|
||||||
|
track: &Track, select: &Selection, track_index: usize, editor: Option<&MidiEditor>
|
||||||
|
) -> u16 {
|
||||||
|
if select.track() == Some(track_index) && let Some(editor) = editor {
|
||||||
|
(editor.size.w() as usize).max(24).max(track.width) as u16
|
||||||
|
} else {
|
||||||
|
track.width as u16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn view_scene_y (
|
||||||
|
select: &Selection, scene_index: usize, editor: Option<&MidiEditor>
|
||||||
|
) -> u16 {
|
||||||
|
if select.scene() == Some(scene_index) && let Some(editor) = editor {
|
||||||
|
editor.size.h().max(12)
|
||||||
|
} else {
|
||||||
|
Scene::DEFAULT_HEIGHT as u16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn view_scene_sel (select: &Selection, track_index: usize, scene_index: usize, editing: bool) -> bool {
|
||||||
|
editing && select.track() == Some(track_index) && select.scene() == Some(scene_index)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@
|
||||||
(bar/n 2 :status
|
(bar/n 2 :status
|
||||||
(bar/s 8
|
(bar/s 8
|
||||||
(bar/e 16 (align/nw (pad/x 1 :transport))
|
(bar/e 16 (align/nw (pad/x 1 :transport))
|
||||||
(bar/w 16 (bar/s 2 (bar/w 4 :tracks/outputs/add :tracks/outputs/count)
|
(bar/w 20 (bar/s 2 (bar/w 4 :tracks/outputs/add :tracks/outputs/count)
|
||||||
(bar/s 2 (bar/w 4 :tracks/devices/add :tracks/devices/count)
|
(bar/s 2 (bar/w 4 :tracks/devices/add :tracks/devices/count)
|
||||||
(bar/s 2 (bar/w 4 :tracks/names/add :tracks/names/count)
|
(bar/s 2 (bar/w 4 :tracks/names/add :tracks/names/count)
|
||||||
(bar/w 4 :tracks/inputs/add :tracks/inputs/count))))
|
(bar/w 4 :tracks/inputs/add :tracks/inputs/count))))
|
||||||
|
|
@ -52,7 +52,7 @@
|
||||||
(bar/s 2 :tracks/devices/cells
|
(bar/s 2 :tracks/devices/cells
|
||||||
(bar/s 2 :tracks/names/cells
|
(bar/s 2 :tracks/names/cells
|
||||||
:tracks/inputs/cells))))))
|
:tracks/inputs/cells))))))
|
||||||
(bar/w 16 (text)
|
(bar/w 20 (text)
|
||||||
(bar/e 16 :scenes/names
|
(bar/e 16 :scenes/names
|
||||||
:scenes/clips))))))
|
:scenes/clips))))))
|
||||||
|
|
||||||
|
|
@ -89,7 +89,7 @@
|
||||||
|
|
||||||
(keys :clip (see :color :launch :axis/z :axis/z2 :delete)
|
(keys :clip (see :color :launch :axis/z :axis/z2 :delete)
|
||||||
(@l (toggle :loop))
|
(@l (toggle :loop))
|
||||||
(@tab (toggle :edit)))
|
(@tab (clip upsert) (edit toggle)))
|
||||||
|
|
||||||
(view :ports/out
|
(view :ports/out
|
||||||
(bsp/s (align/w (text L-AUDIO-OUT))
|
(bsp/s (align/w (text L-AUDIO-OUT))
|
||||||
|
|
|
||||||
16
src/tek.rs
16
src/tek.rs
|
|
@ -9,26 +9,22 @@ macro_rules! def_sizes_iter {
|
||||||
}
|
}
|
||||||
|
|
||||||
mod app; pub use self::app::*;
|
mod app; pub use self::app::*;
|
||||||
mod bind; pub use self::bind::*;
|
|
||||||
mod binds; pub use self::binds::*;
|
|
||||||
mod browse; pub use self::browse::*;
|
mod browse; pub use self::browse::*;
|
||||||
mod cli; pub use self::cli::*;
|
mod cli; pub use self::cli::*;
|
||||||
mod clip; pub use self::clip::*;
|
|
||||||
mod clock; pub use self::clock::*;
|
mod clock; pub use self::clock::*;
|
||||||
mod clock_view; pub use self::clock_view::*;
|
mod clock_view; pub use self::clock_view::*;
|
||||||
mod config; pub use self::config::*;
|
mod config; pub use self::config::*;
|
||||||
|
mod config_bind; pub use self::config_bind::*;
|
||||||
|
mod config_mode; pub use self::config_mode::*;
|
||||||
|
mod config_view; pub use self::config_view::*;
|
||||||
mod deps; pub use self::deps::*;
|
mod deps; pub use self::deps::*;
|
||||||
mod device; pub use self::device::*;
|
mod device; pub use self::device::*;
|
||||||
mod dialog; pub use self::dialog::*;
|
mod dialog; pub use self::dialog::*;
|
||||||
mod editor; pub use self::editor::*;
|
|
||||||
mod field; pub use self::field::*;
|
mod field; pub use self::field::*;
|
||||||
mod memo; pub use self::memo::*;
|
mod memo; pub use self::memo::*;
|
||||||
mod menu_item; pub use self::menu_item::*;
|
mod menu_item; pub use self::menu_item::*;
|
||||||
mod meter; pub use self::meter::*;
|
mod meter; pub use self::meter::*;
|
||||||
mod mix; pub use self::mix::*;
|
mod mix; pub use self::mix::*;
|
||||||
mod mode; pub use self::mode::*;
|
|
||||||
mod mode_stack; pub use self::mode_stack::*;
|
|
||||||
mod modes; pub use self::modes::*;
|
|
||||||
mod moment; pub use self::moment::*;
|
mod moment; pub use self::moment::*;
|
||||||
mod octave; pub use self::octave::*;
|
mod octave; pub use self::octave::*;
|
||||||
mod piano; pub use self::piano::*;
|
mod piano; pub use self::piano::*;
|
||||||
|
|
@ -38,10 +34,15 @@ mod port; pub use self::port::*;
|
||||||
mod project; pub use self::project::*;
|
mod project; pub use self::project::*;
|
||||||
mod range; pub use self::range::*;
|
mod range; pub use self::range::*;
|
||||||
mod sampler; pub use self::sampler::*;
|
mod sampler; pub use self::sampler::*;
|
||||||
|
mod sample; pub use self::sample::*;
|
||||||
|
mod sample_add; pub use self::sample_add::*;
|
||||||
|
mod sample_kit; pub use self::sample_kit::*;
|
||||||
|
mod sample_voice; pub use self::sample_voice::*;
|
||||||
mod scene; pub use self::scene::*;
|
mod scene; pub use self::scene::*;
|
||||||
mod select; pub use self::select::*;
|
mod select; pub use self::select::*;
|
||||||
mod midi_clip; pub use self::midi_clip::*;
|
mod midi_clip; pub use self::midi_clip::*;
|
||||||
mod midi_clips; pub use self::midi_clips::*;
|
mod midi_clips; pub use self::midi_clips::*;
|
||||||
|
mod midi_edit; pub use self::midi_edit::*;
|
||||||
mod midi_mon; pub use self::midi_mon::*;
|
mod midi_mon; pub use self::midi_mon::*;
|
||||||
mod midi_play; pub use self::midi_play::*;
|
mod midi_play; pub use self::midi_play::*;
|
||||||
mod midi_rec; pub use self::midi_rec::*;
|
mod midi_rec; pub use self::midi_rec::*;
|
||||||
|
|
@ -49,7 +50,6 @@ mod midi_seq; pub use self::midi_seq::*;
|
||||||
mod ticker; pub use self::ticker::*;
|
mod ticker; pub use self::ticker::*;
|
||||||
mod timebase; pub use self::timebase::*;
|
mod timebase; pub use self::timebase::*;
|
||||||
mod track; pub use self::track::*;
|
mod track; pub use self::track::*;
|
||||||
mod views; pub use self::views::*;
|
|
||||||
|
|
||||||
#[cfg(feature = "lv2")] mod lv2;
|
#[cfg(feature = "lv2")] mod lv2;
|
||||||
#[cfg(feature = "lv2")] pub use self::lv2::*;
|
#[cfg(feature = "lv2")] pub use self::lv2::*;
|
||||||
|
|
|
||||||
|
|
@ -352,7 +352,7 @@ pub trait TracksView: HasTracks + ScenesView + HasMidiIns + HasMidiOuts + HasTra
|
||||||
|
|
||||||
/// Iterate over tracks with their corresponding sizes.
|
/// Iterate over tracks with their corresponding sizes.
|
||||||
fn tracks_with_sizes (&self) -> impl TracksSizes<'_> {
|
fn tracks_with_sizes (&self) -> impl TracksSizes<'_> {
|
||||||
let _editor_width = self.editor().map(|e|e.size.w());
|
let _editor_width = self.editor().as_ref().map(|e|e.size.w());
|
||||||
let _active_track = self.selection().track();
|
let _active_track = self.selection().track();
|
||||||
let mut x = 0;
|
let mut x = 0;
|
||||||
let w = self.clips_size().w() as usize;
|
let w = self.clips_size().w() as usize;
|
||||||
|
|
|
||||||
2
tengri
2
tengri
|
|
@ -1 +1 @@
|
||||||
Subproject commit a8ee66a8cbdc33eb162b31b8da36b4ec203fd5ff
|
Subproject commit 79f9cfcbf93db626d031ab86abbbdf5c29d80a48
|
||||||
Loading…
Add table
Add a link
Reference in a new issue