mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-07-17 15:56:57 +02:00
restruct: 92e
This commit is contained in:
parent
4ec2165e3d
commit
ae347eeef7
31 changed files with 2924 additions and 2663 deletions
626
src/app.rs
Normal file
626
src/app.rs
Normal file
|
|
@ -0,0 +1,626 @@
|
|||
use crate::*;
|
||||
pub mod bind; pub use self::bind::*;
|
||||
pub mod cli; pub use self::cli::*;
|
||||
pub mod config; pub use self::config::*;
|
||||
pub mod view; pub use self::view::*;
|
||||
|
||||
/// Total application state.
|
||||
///
|
||||
/// ```
|
||||
/// use tek::{HasTracks, HasScenes, TracksView, ScenesView};
|
||||
/// let mut app = tek::App::default();
|
||||
/// let _ = app.scene_add(None, None).unwrap();
|
||||
/// let _ = app.update_clock();
|
||||
/// app.project.editor = Some(Default::default());
|
||||
/// //let _: Vec<_> = app.project.inputs_with_sizes().collect();
|
||||
/// //let _: Vec<_> = app.project.outputs_with_sizes().collect();
|
||||
/// let _: Vec<_> = app.project.tracks_with_sizes().collect();
|
||||
/// //let _: Vec<_> = app.project.scenes_with_sizes(true, 10, 10).collect();
|
||||
/// //let _: Vec<_> = app.scenes_with_colors(true, 10).collect();
|
||||
/// //let _: Vec<_> = app.scenes_with_track_colors(true, 10, 10).collect();
|
||||
/// let _ = app.project.w();
|
||||
/// //let _ = app.project.w_sidebar();
|
||||
/// //let _ = app.project.w_tracks_area();
|
||||
/// let _ = app.project.h();
|
||||
/// //let _ = app.project.h_tracks_area();
|
||||
/// //let _ = app.project.h_inputs();
|
||||
/// //let _ = app.project.h_outputs();
|
||||
/// let _ = app.project.h_scenes();
|
||||
/// ```
|
||||
#[derive(Default, Debug)] pub struct App {
|
||||
/// Base color.
|
||||
pub color: ItemTheme,
|
||||
/// Must not be dropped for the duration of the process
|
||||
pub jack: Jack<'static>,
|
||||
/// Display size
|
||||
pub size: Size,
|
||||
/// Performance counter
|
||||
pub perf: PerfModel,
|
||||
/// Available view modes and input bindings
|
||||
pub config: Config,
|
||||
/// Currently selected mode
|
||||
pub mode: Arc<Mode<Arc<str>>>,
|
||||
/// Undo history
|
||||
pub history: Vec<(AppCommand, Option<AppCommand>)>,
|
||||
/// Dialog overlay
|
||||
pub dialog: Dialog,
|
||||
/// Contains all recently created clips.
|
||||
pub pool: Pool,
|
||||
/// Contains the currently edited musical arrangement
|
||||
pub project: Arrangement,
|
||||
/// Error, if any
|
||||
pub error: Arc<RwLock<Option<Arc<str>>>>
|
||||
}
|
||||
|
||||
impl App {
|
||||
|
||||
/// Create a new application instance from a backend, project, config, and mode
|
||||
///
|
||||
/// ```
|
||||
/// let jack = tek::tengri::Jack::new(&"test_tek").expect("failed to connect to jack");
|
||||
/// let proj = tek::Arrangement::default();
|
||||
/// let mut conf = tek::Config::default();
|
||||
/// conf.add("(mode hello)");
|
||||
/// let tek = tek::tek(&jack, proj, conf, "hello");
|
||||
/// ```
|
||||
pub fn new (
|
||||
jack: &Jack<'static>, project: Arrangement, config: Config, mode: impl AsRef<str>
|
||||
) -> Self {
|
||||
let mode: &str = mode.as_ref();
|
||||
App {
|
||||
color: ItemTheme::random(),
|
||||
dialog: Dialog::welcome(),
|
||||
jack: jack.clone(),
|
||||
mode: config.get_mode(mode).expect(&format!("failed to find mode '{mode}'")),
|
||||
config,
|
||||
project,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_shared (
|
||||
jack: &Jack<'static>, project: Arrangement, config: Config, mode: impl AsRef<str>
|
||||
) -> Arc<RwLock<Self>> {
|
||||
Arc::new(RwLock::new(App::new(jack, ":menu", config, project)))
|
||||
}
|
||||
|
||||
pub fn confirm (&mut self) -> Perhaps<AppCommand> {
|
||||
Ok(match &self.dialog {
|
||||
Dialog::Menu(index, items) => {
|
||||
let callback = items.0[*index].1.clone();
|
||||
callback(self)?;
|
||||
None
|
||||
},
|
||||
_ => todo!(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn inc (&mut self, axis: &ControlAxis) -> Perhaps<AppCommand> {
|
||||
Ok(match (&self.dialog, axis) {
|
||||
(Dialog::None, _) => todo!(),
|
||||
(Dialog::Menu(_, _), ControlAxis::Y) =>
|
||||
AppCommand::SetDialog { dialog: self.dialog.menu_next() }.act(self)?,
|
||||
_ => todo!()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn dec (&mut self, axis: &ControlAxis) -> Perhaps<AppCommand> {
|
||||
Ok(match (&self.dialog, axis) {
|
||||
(Dialog::None, _) => None,
|
||||
(Dialog::Menu(_, _), ControlAxis::Y) =>
|
||||
AppCommand::SetDialog { dialog: self.dialog.menu_prev() }.act(self)?,
|
||||
_ => todo!()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn jack_process (
|
||||
&mut self, client: &Client, scope: &ProcessScope
|
||||
) -> Control {
|
||||
let t0 = self.perf.get_t0();
|
||||
self.clock().update_from_scope(scope).unwrap();
|
||||
let midi_in = self.project.midi_input_collect(scope);
|
||||
if let Some(editor) = &self.editor() {
|
||||
let mut pitch: Option<u7> = None;
|
||||
for port in midi_in.iter() {
|
||||
for event in port.iter() {
|
||||
if let (_, Ok(LiveEvent::Midi {message: MidiMessage::NoteOn {key, ..}, ..}))
|
||||
= event
|
||||
{
|
||||
pitch = Some(key.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(pitch) = pitch {
|
||||
editor.set_note_pos(pitch.as_int() as usize);
|
||||
}
|
||||
}
|
||||
let result = self.project.process_tracks(client, scope);
|
||||
self.perf.update_from_jack_scope(t0, scope);
|
||||
result
|
||||
}
|
||||
|
||||
pub fn jack_event (
|
||||
&mut self, event: JackEvent
|
||||
) {
|
||||
use JackEvent::*;
|
||||
match event {
|
||||
SampleRate(sr) => { self.clock().timebase.sr.set(sr as f64); },
|
||||
PortRegistration(_id, true) => {
|
||||
//let port = self.jack().port_by_id(id);
|
||||
//println!("\rport add: {id} {port:?}");
|
||||
//println!("\rport add: {id}");
|
||||
},
|
||||
PortRegistration(_id, false) => {
|
||||
/*println!("\rport del: {id}")*/
|
||||
},
|
||||
PortsConnected(_a, _b, true) => { /*println!("\rport conn: {a} {b}")*/ },
|
||||
PortsConnected(_a, _b, false) => { /*println!("\rport disc: {a} {b}")*/ },
|
||||
ClientRegistration(_id, true) => {},
|
||||
ClientRegistration(_id, false) => {},
|
||||
ThreadInit => {},
|
||||
XRun => {},
|
||||
GraphReorder => {},
|
||||
_ => { panic!("{event:?}"); }
|
||||
}
|
||||
}
|
||||
|
||||
/// Update memoized render of clock values.
|
||||
/// ```
|
||||
/// tek::App::default().update_clock();
|
||||
/// ```
|
||||
pub fn update_clock (&self) {
|
||||
ClockView::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80)
|
||||
}
|
||||
|
||||
/// Set modal dialog.
|
||||
///
|
||||
/// ```
|
||||
/// let previous: tek::Dialog = tek::App::default().set_dialog(tek::Dialog::welcome());
|
||||
/// ```
|
||||
pub fn set_dialog (&mut self, mut dialog: Dialog) -> Dialog {
|
||||
std::mem::swap(&mut self.dialog, &mut dialog);
|
||||
dialog
|
||||
}
|
||||
|
||||
/// FIXME: generalize. Set picked device in device pick dialog.
|
||||
///
|
||||
/// ```
|
||||
/// tek::App::default().device_pick(0);
|
||||
/// ```
|
||||
pub fn device_pick (&mut self, index: usize) {
|
||||
self.dialog = Dialog::Device(index);
|
||||
}
|
||||
|
||||
/// FIXME: generalize. Add device to current track.
|
||||
pub fn add_device (&mut self, index: usize) -> Usually<()> {
|
||||
match index {
|
||||
0 => {
|
||||
let name = self.jack.with_client(|c|c.name().to_string());
|
||||
let midi = self.project.track().expect("no active track").sequencer.midi_outs[0].port_name();
|
||||
let track = self.track().expect("no active track");
|
||||
let port = format!("{}/Sampler", &track.name);
|
||||
let connect = Connect::exact(format!("{name}:{midi}"));
|
||||
let sampler = if let Ok(sampler) = Sampler::new(
|
||||
&self.jack, &port, &[connect], &[&[], &[]], &[&[], &[]]
|
||||
) {
|
||||
self.dialog = Dialog::None;
|
||||
Device::Sampler(sampler)
|
||||
} else {
|
||||
self.dialog = Dialog::Message("Failed to add device.".into());
|
||||
return Err("failed to add device".into())
|
||||
};
|
||||
let track = self.track_mut().expect("no active track");
|
||||
track.devices.push(sampler);
|
||||
Ok(())
|
||||
},
|
||||
1 => {
|
||||
todo!();
|
||||
//Ok(())
|
||||
},
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return reference to content browser if open.
|
||||
///
|
||||
/// ```
|
||||
/// assert_eq!(tek::App::default().browser(), None);
|
||||
/// ```
|
||||
pub fn browser (&self) -> Option<&Browse> {
|
||||
if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None }
|
||||
}
|
||||
|
||||
/// 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.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.read().unwrap().count_midi_messages() == 0 {
|
||||
std::mem::swap(&mut swapped, slot);
|
||||
}
|
||||
if let Some(clip) = swapped {
|
||||
self.pool.delete_clip(&clip.read().unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The [Draw] implementation for [App] handles the loaded view,
|
||||
/// which is defined in terms of [dizzle] DSL.
|
||||
///
|
||||
/// If there is an error, the error is displayed. FIXME: overlay it
|
||||
/// Then, every top-level form of the DSL description is rendered.
|
||||
impl Draw<Tui> for App {
|
||||
fn draw (self, to: &mut Tui) -> Usually<XYWH<u16>> {
|
||||
let xywh = to.area().into();
|
||||
if let Some(e) = self.error.read().unwrap().as_ref() {
|
||||
to.show(xywh, e.as_ref());
|
||||
}
|
||||
for (index, dsl) in self.mode.view.iter().enumerate() {
|
||||
if let Err(e) = self.interpret(to, dsl) {
|
||||
*self.error.write().unwrap() = Some(format!("view #{index}: {e}").into());
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(xywh)
|
||||
}
|
||||
}
|
||||
|
||||
impl HasClipsSize for App {
|
||||
fn clips_size (&self) -> &Size { &self.project.size_inner }
|
||||
}
|
||||
|
||||
impl HasJack<'static> for App {
|
||||
fn jack (&self) -> &Jack<'static> { &self.jack }
|
||||
}
|
||||
|
||||
//impl_handle!(TuiIn: |self: App, input|{
|
||||
//let commands = tek_collect_commands(self, input)?;
|
||||
//let history = tek_execute_commands(self, commands)?;
|
||||
//self.history.extend(history.into_iter());
|
||||
//Ok(None)
|
||||
//});
|
||||
|
||||
//fn tek_collect_commands (app: &App, input: &TuiIn) -> Usually<Vec<AppCommand>> {
|
||||
//let mut commands = vec![];
|
||||
//for id in app.mode.keys.iter() {
|
||||
//if let Some(event_map) = app.config.binds.clone().read().unwrap().get(id.as_ref())
|
||||
//&& let Some(bindings) = event_map.query(input.event()) {
|
||||
//for binding in bindings {
|
||||
//for command in binding.commands.iter() {
|
||||
//if let Some(command) = app.namespace(command)? as Option<AppCommand> {
|
||||
//commands.push(command)
|
||||
//}
|
||||
//}
|
||||
//}
|
||||
//}
|
||||
//}
|
||||
//Ok(commands)
|
||||
//}
|
||||
|
||||
//fn tek_execute_commands (
|
||||
//app: &mut App, commands: Vec<AppCommand>
|
||||
//) -> Usually<Vec<(AppCommand, Option<AppCommand>)>> {
|
||||
//let mut history = vec![];
|
||||
//for command in commands.into_iter() {
|
||||
//let result = command.act(app);
|
||||
//match result { Err(err) => { history.push((command, None)); return Err(err) }
|
||||
//Ok(undo) => { history.push((command, undo)); } };
|
||||
//}
|
||||
//Ok(history)
|
||||
//}
|
||||
|
||||
def_command!(AppCommand: |app: App| {
|
||||
Nop => Ok(None),
|
||||
Confirm => tek_confirm(app),
|
||||
Cancel => todo!(), // TODO delegate:
|
||||
Inc { axis: ControlAxis } => tek_inc(app, axis),
|
||||
Dec { axis: ControlAxis } => tek_dec(app, axis),
|
||||
SetDialog { dialog: Dialog } => {
|
||||
swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog })
|
||||
},
|
||||
});
|
||||
|
||||
impl_default!(AppCommand: Self::Nop);
|
||||
|
||||
impl_has!(Clock: |self: App|self.project.clock);
|
||||
impl_has!(Vec<MidiInput>: |self: App|self.project.midi_ins);
|
||||
impl_has!(Vec<MidiOutput>: |self: App|self.project.midi_outs);
|
||||
impl_has!(Dialog: |self: App|self.dialog);
|
||||
impl_has!(Jack<'static>: |self: App|self.jack);
|
||||
impl_has!(Size: |self: App|self.size);
|
||||
impl_has!(Pool: |self: App|self.pool);
|
||||
impl_has!(Selection: |self: App|self.project.selection);
|
||||
|
||||
impl_as_ref!(Vec<Scene>: |self: App|self.project.as_ref());
|
||||
impl_as_mut!(Vec<Scene>: |self: App|self.project.as_mut());
|
||||
|
||||
impl_as_ref_opt!(MidiEditor: |self: App|self.project.as_ref_opt());
|
||||
impl_as_mut_opt!(MidiEditor: |self: App|self.project.as_mut_opt());
|
||||
|
||||
impl_has_clips!( |self: App|self.pool.clips);
|
||||
|
||||
impl_audio!(App: tek_jack_process, tek_jack_event);
|
||||
|
||||
namespace!(App: Arc<str> { literal = |dsl|Ok(dsl.src()?.map(|x|x.into())); });
|
||||
|
||||
namespace!(App: u8 { literal = |dsl|try_to_u8(dsl); });
|
||||
|
||||
namespace!(App: u16 { literal = |dsl|try_to_u16(dsl); symbol = |app| {
|
||||
":w/sidebar" => app.project.w_sidebar(app.editor().is_some()),
|
||||
":h/sample-detail" => 6.max(app.size.h() as u16 * 3 / 9), }; });
|
||||
|
||||
namespace!(App: isize { literal = |dsl|try_to_isize(dsl); });
|
||||
|
||||
namespace!(App: usize { literal = |dsl|try_to_usize(dsl); symbol = |app| {
|
||||
":scene-count" => app.scenes().len(),
|
||||
":track-count" => app.tracks().len(),
|
||||
":device-kind" => app.dialog.device_kind().unwrap_or(0),
|
||||
":device-kind/next" => app.dialog.device_kind_next().unwrap_or(0),
|
||||
":device-kind/prev" => app.dialog.device_kind_prev().unwrap_or(0), }; });
|
||||
|
||||
namespace!(App: bool { symbol = |app| { // Provide boolean values.
|
||||
":mode/editor" => app.project.editor.is_some(),
|
||||
":focused/dialog" => !matches!(app.dialog, Dialog::None),
|
||||
":focused/message" => matches!(app.dialog, Dialog::Message(..)),
|
||||
":focused/add_device" => matches!(app.dialog, Dialog::Device(..)),
|
||||
":focused/browser" => app.dialog.browser().is_some(),
|
||||
":focused/pool/import" => matches!(app.pool.mode, Some(PoolMode::Import(..))),
|
||||
":focused/pool/export" => matches!(app.pool.mode, Some(PoolMode::Export(..))),
|
||||
":focused/pool/rename" => matches!(app.pool.mode, Some(PoolMode::Rename(..))),
|
||||
":focused/pool/length" => matches!(app.pool.mode, Some(PoolMode::Length(..))),
|
||||
":focused/clip" => !app.editor_focused() && matches!(app.selection(), Selection::TrackClip{..}),
|
||||
":focused/track" => !app.editor_focused() && matches!(app.selection(), Selection::Track(..)),
|
||||
":focused/scene" => !app.editor_focused() && matches!(app.selection(), Selection::Scene(..)),
|
||||
":focused/mix" => !app.editor_focused() && matches!(app.selection(), Selection::Mix),
|
||||
}; });
|
||||
|
||||
namespace!(App: ItemTheme {}); // TODO: provide colors here
|
||||
//
|
||||
namespace!(App: Selection { symbol = |app| {
|
||||
":select/scene" => app.selection().select_scene(app.tracks().len()),
|
||||
":select/scene/next" => app.selection().select_scene_next(app.scenes().len()),
|
||||
":select/scene/prev" => app.selection().select_scene_prev(),
|
||||
":select/track" => app.selection().select_track(app.tracks().len()),
|
||||
":select/track/next" => app.selection().select_track_next(app.tracks().len()),
|
||||
":select/track/prev" => app.selection().select_track_prev(),
|
||||
}; });
|
||||
|
||||
namespace!(App: Color {
|
||||
symbol = |app| {
|
||||
":color/bg" => Color::Rgb(28, 32, 36),
|
||||
};
|
||||
expression = |app| {
|
||||
"g" (n: u8) => Color::Rgb(n, n, n),
|
||||
"rgb" (r: u8, g: u8, b: u8) => Color::Rgb(r, g, b),
|
||||
};
|
||||
});
|
||||
|
||||
namespace!(App: Option<u7> { symbol = |app| {
|
||||
":editor/pitch" => Some((app.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into())
|
||||
}; });
|
||||
|
||||
namespace!(App: Option<usize> { symbol = |app| {
|
||||
":selected/scene" => app.selection().scene(),
|
||||
":selected/track" => app.selection().track(),
|
||||
}; });
|
||||
|
||||
namespace!(App: Option<Arc<RwLock<MidiClip>>> {
|
||||
symbol = |app| {
|
||||
":selected/clip" => if let Selection::TrackClip { track, scene } = app.selection() {
|
||||
app.scenes()[*scene].clips[*track].clone()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
impl<'a> Namespace<'a, AppCommand> for App {
|
||||
symbols!('a |app| -> AppCommand {
|
||||
"x/inc" => AppCommand::Inc { axis: ControlAxis::X },
|
||||
"x/dec" => AppCommand::Dec { axis: ControlAxis::X },
|
||||
"y/inc" => AppCommand::Inc { axis: ControlAxis::Y },
|
||||
"y/dec" => AppCommand::Dec { axis: ControlAxis::Y },
|
||||
"confirm" => AppCommand::Confirm,
|
||||
"cancel" => AppCommand::Cancel,
|
||||
});
|
||||
}
|
||||
|
||||
impl Interpret<Tui, ()> for App {
|
||||
fn interpret_expr <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Usually<()> {
|
||||
app_interpret_expr(self, to, lang)
|
||||
}
|
||||
fn interpret_word <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Usually<()> {
|
||||
app_interpret_word(self, to, lang)
|
||||
}
|
||||
}
|
||||
|
||||
fn app_interpret_expr (state: &App, to: &mut Tui, lang: &impl Expression) -> Usually<()> {
|
||||
if eval_view(state, to, lang)? || eval_view_tui(state, to, lang)? {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("App::interpret_expr: unexpected: {lang:?}").into())
|
||||
}
|
||||
}
|
||||
|
||||
fn app_interpret_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usually<()> {
|
||||
let mut frags = dsl.src()?.unwrap().split("/");
|
||||
match frags.next() {
|
||||
|
||||
Some(":logo") => view_logo().draw(to),
|
||||
|
||||
Some(":status") => h_exact(1, "TODO: Status Bar").draw(to),
|
||||
|
||||
Some(":meters") => match frags.next() {
|
||||
Some("input") => bg(Rgb(30, 30, 30), h_full(origin_s("Input Meters"))).draw(to),
|
||||
Some("output") => bg(Rgb(30, 30, 30), h_full(origin_s("Output Meters"))).draw(to),
|
||||
_ => panic!()
|
||||
},
|
||||
|
||||
Some(":tracks") => match frags.next() {
|
||||
None => "TODO tracks".draw(to),
|
||||
Some("names") => state.project.view_track_names(state.color.clone()).draw(to),//bg(Rgb(40, 40, 40), w_full(origin_w("Track Names")))),
|
||||
Some("inputs") => bg(Rgb(40, 40, 40), w_full(origin_w("Track Inputs"))).draw(to),
|
||||
Some("devices") => bg(Rgb(40, 40, 40), w_full(origin_w("Track Devices"))).draw(to),
|
||||
Some("outputs") => bg(Rgb(40, 40, 40), w_full(origin_w("Track Outputs"))).draw(to),
|
||||
_ => panic!()
|
||||
},
|
||||
|
||||
Some(":scenes") => match frags.next() {
|
||||
None => "TODO scenes".draw(to),
|
||||
Some(":scenes/names") => "TODO Scene Names".draw(to),
|
||||
_ => panic!()
|
||||
},
|
||||
|
||||
Some(":editor") => "TODO Editor".draw(to),
|
||||
|
||||
Some(":dialog") => match frags.next() {
|
||||
Some("menu") => if let Dialog::Menu(selected, items) = &state.dialog {
|
||||
let items = items.clone();
|
||||
let selected = selected;
|
||||
Some(wh_full(thunk(move|to: &mut Tui|{
|
||||
for (index, MenuItem(item, _)) in items.0.iter().enumerate() {
|
||||
to.place(&y_push((2 * index) as u16,
|
||||
fg_bg(
|
||||
if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) },
|
||||
if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) },
|
||||
h_exact(2, origin_n(w_full(item)))
|
||||
)));
|
||||
}
|
||||
})))
|
||||
} else {
|
||||
None
|
||||
}.draw(to),
|
||||
_ => unimplemented!("App::interpret_word: {dsl:?} ({frags:?})"),
|
||||
},
|
||||
|
||||
Some(":templates") => {
|
||||
let modes = state.config.modes.clone();
|
||||
let height = (modes.read().unwrap().len() * 2) as u16;
|
||||
h_exact(height, w_min(Some(30), thunk(move |to: &mut Tui|{
|
||||
for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() {
|
||||
let bg = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) };
|
||||
let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or("<no name>");
|
||||
let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or("<no info>");
|
||||
let fg1 = Rgb(224, 192, 128);
|
||||
let fg2 = Rgb(224, 128, 32);
|
||||
let field_name = w_full(origin_w(fg(fg1, name)));
|
||||
let field_id = w_full(origin_e(fg(fg2, id)));
|
||||
let field_info = w_full(origin_w(info));
|
||||
y_push((2 * index) as u16,
|
||||
h_exact(2, w_full(bg(bg, south(
|
||||
above(field_name, field_id), field_info))))).draw(to);
|
||||
}
|
||||
})))
|
||||
}.draw(to),
|
||||
|
||||
Some(":sessions") => h_exact(Some(6), w_min(30, thunk(|to: &mut Tui|{
|
||||
let fg = Rgb(224, 192, 128);
|
||||
for (index, name) in ["session1", "session2", "session3"].iter().enumerate() {
|
||||
let bg = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) };
|
||||
y_push((2 * index) as u16,
|
||||
&h_exact(2, w_full(bg(bg, origin_w(fg(fg, name)))))).draw(to);
|
||||
}
|
||||
}))).draw(to),
|
||||
|
||||
Some(":browse/title") => w_full(origin_w(field_v(ItemColor::default(),
|
||||
match state.dialog.browser_target().unwrap() {
|
||||
BrowseTarget::SaveProject => "Save project:",
|
||||
BrowseTarget::LoadProject => "Load project:",
|
||||
BrowseTarget::ImportSample(_) => "Import sample:",
|
||||
BrowseTarget::ExportSample(_) => "Export sample:",
|
||||
BrowseTarget::ImportClip(_) => "Import clip:",
|
||||
BrowseTarget::ExportClip(_) => "Export clip:",
|
||||
}, w_shrink(3, h_exact(1, fg(g(96), x_repeat("🭻"))))))).draw(to),
|
||||
|
||||
Some(":device") => {
|
||||
let selected = state.dialog.device_kind().unwrap();
|
||||
south(bold(true, "Add device"), iter_south(
|
||||
move||device_kinds().iter(),
|
||||
move|_label: &&'static str, i|{
|
||||
let b = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) };
|
||||
let l = if i == selected { "[ " } else { " " };
|
||||
let r = if i == selected { " ]" } else { " " };
|
||||
w_full(bg(b, east(l, west(r, "FIXME device name")))) })).draw(to)
|
||||
},
|
||||
|
||||
Some(":debug") => h_exact(1, format!("[{:?}]", to.area())).draw(to),
|
||||
|
||||
Some(_) => {
|
||||
let views = state.config.views.read().unwrap();
|
||||
if let Some(dsl) = views.get(dsl.src()?.unwrap()) {
|
||||
let dsl = dsl.clone();
|
||||
std::mem::drop(views);
|
||||
state.interpret(to, &dsl)?
|
||||
} else {
|
||||
unimplemented!("{dsl:?}");
|
||||
}
|
||||
},
|
||||
|
||||
_ => unreachable!()
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl HasTrackScroll for App {
|
||||
fn track_scroll (&self) -> usize {
|
||||
self.project.track_scroll()
|
||||
}
|
||||
}
|
||||
|
||||
impl HasSceneScroll for App {
|
||||
fn scene_scroll (&self) -> usize {
|
||||
self.project.scene_scroll()
|
||||
}
|
||||
}
|
||||
|
||||
impl ScenesView for App {
|
||||
fn w_mid (&self) -> u16 {
|
||||
(self.size.w() as u16).saturating_sub(self.w_side())
|
||||
}
|
||||
fn w_side (&self) -> u16 {
|
||||
20
|
||||
}
|
||||
fn h_scenes (&self) -> u16 {
|
||||
(self.size.h() as u16).saturating_sub(20)
|
||||
}
|
||||
}
|
||||
|
|
@ -61,7 +61,7 @@ impl Bind<TuiEvent, Arc<str>> {
|
|||
// TODO
|
||||
Ok(())
|
||||
} else if let Ok(Some(_word)) = item.expr().head().word() {
|
||||
if let Some(key) = TuiEvent::from_dsl(item.expr()?.head()?)? {
|
||||
if let Some(key) = TuiEvent::named(item.expr()?.head()?)? {
|
||||
map.add(key, Binding {
|
||||
commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(),
|
||||
condition: None,
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{*, arrange::*, clock::*, config::*, device::*};
|
||||
use crate::*;
|
||||
|
||||
/// The command-line interface descriptor.
|
||||
///
|
||||
|
|
@ -61,7 +61,7 @@ use crate::{*, arrange::*, clock::*, config::*, device::*};
|
|||
#[arg(short='L', long)] left_to: Vec<String>,
|
||||
/// Audio ins to connect from right output
|
||||
#[arg(short='R', long)] right_to: Vec<String>,
|
||||
/// Tracks to create
|
||||
/// Tracks to creat
|
||||
#[arg(short='t', long)] tracks: Option<usize>,
|
||||
/// Scenes to create
|
||||
#[arg(short='s', long)] scenes: Option<usize>,
|
||||
|
|
@ -75,96 +75,103 @@ use crate::{*, arrange::*, clock::*, config::*, device::*};
|
|||
}
|
||||
|
||||
/// Command-line configuration.
|
||||
#[cfg(feature = "cli")] impl Cli {
|
||||
#[cfg(feature = "cli")]
|
||||
impl Cli {
|
||||
pub fn run (&self) -> Usually<()> {
|
||||
if let Action::Version = self.action {
|
||||
return Ok(tek_show_version())
|
||||
}
|
||||
self.action.run(&Config::init_new(None)?)
|
||||
}
|
||||
}
|
||||
|
||||
let mut config = Config::new(None);
|
||||
config.init()?;
|
||||
|
||||
if let Action::Config = self.action {
|
||||
tek_print_config(&config);
|
||||
} else if let Action::List = self.action {
|
||||
todo!("list sessions")
|
||||
} else if let Action::Resume = self.action {
|
||||
todo!("resume session")
|
||||
} else if let Action::New {
|
||||
name, bpm, tracks, scenes, sync_lead, sync_follow,
|
||||
midi_from, midi_from_re, midi_to, midi_to_re,
|
||||
left_from, right_from, left_to, right_to, ..
|
||||
} = &self.action {
|
||||
|
||||
// Connect to JACK
|
||||
let name = name.as_ref().map_or("tek", |x|x.as_str());
|
||||
let jack = Jack::new(&name)?;
|
||||
|
||||
// TODO: Collect audio IO:
|
||||
let empty = &[] as &[&str];
|
||||
let left_froms = Connect::collect(&left_from, empty, empty);
|
||||
let left_tos = Connect::collect(&left_to, empty, empty);
|
||||
let right_froms = Connect::collect(&right_from, empty, empty);
|
||||
let right_tos = Connect::collect(&right_to, empty, empty);
|
||||
let _audio_froms = &[left_froms.as_slice(), right_froms.as_slice()];
|
||||
let _audio_tos = &[left_tos.as_slice(), right_tos.as_slice()];
|
||||
|
||||
// Create initial project:
|
||||
let clock = Clock::new(&jack, *bpm)?;
|
||||
let mut project = Arrangement::new(
|
||||
&jack,
|
||||
None,
|
||||
clock,
|
||||
vec![],
|
||||
vec![],
|
||||
Connect::collect(&midi_from, &[] as &[&str], &midi_from_re).iter().enumerate()
|
||||
.map(|(index, connect)|jack.midi_in(&format!("M/{index}"), &[connect.clone()]))
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
Connect::collect(&midi_to, &[] as &[&str], &midi_to_re).iter().enumerate()
|
||||
.map(|(index, connect)|jack.midi_out(&format!("{index}/M"), &[connect.clone()]))
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
);
|
||||
project.tracks_add(tracks.unwrap_or(0), None, &[], &[])?;
|
||||
project.scenes_add(scenes.unwrap_or(0))?;
|
||||
|
||||
if matches!(self.action, Action::Status) {
|
||||
// Show status and exit
|
||||
tek_print_status(&project);
|
||||
return Ok(())
|
||||
#[cfg(feature = "cli")]
|
||||
impl Action {
|
||||
fn run (&self, config: &Config) -> Usually<()> {
|
||||
use Action::*;
|
||||
match self {
|
||||
Version => tek_show_version(),
|
||||
Config => tek_print_config(&config),
|
||||
List => todo!("list sessions"),
|
||||
Resume => todo!("resume session"),
|
||||
New {
|
||||
name, bpm, tracks, scenes, sync_lead, sync_follow,
|
||||
midi_from: mf, midi_from_re: mfr, midi_to: mt, midi_to_re: mtr,
|
||||
left_from: lf, right_from: rf, left_to: lt, right_to: rt, ..
|
||||
} => {
|
||||
let name = name.as_ref().map_or("tek", |x|x.as_str());
|
||||
let jack = Jack::new(&name)?;
|
||||
let proj = tek_project_new(
|
||||
&jack, Clock::new(&jack, *bpm)?, &lf, <, &rf, &rt, &mf, &mt, &mfr, &mtr
|
||||
)?;
|
||||
proj.tracks_add(tracks.unwrap_or(0), None, &[], &[])?;
|
||||
proj.scenes_add(scenes.unwrap_or(0))?;
|
||||
//if matches!(self, Action::Status) {
|
||||
//// Show status and exit
|
||||
//tek_print_status(&proj);
|
||||
//return Ok(())
|
||||
//}
|
||||
// Initialize the app state
|
||||
let app = tek(&jack, proj, config, ":menu");
|
||||
//if matches!(self, Action::Headless) {
|
||||
//// TODO: Headless mode (daemon + client over IPC, then over network...)
|
||||
//println!("todo headless");
|
||||
//return Ok(())
|
||||
//}
|
||||
// Run the [Tui] and [Jack] threads with the [App] state.
|
||||
Tui::new(Box::new(std::io::stdout()))?.run(true, &jack.run(move|jack|{
|
||||
// Between jack init and app's first cycle:
|
||||
jack.sync_lead(sync_lead, |mut state|{
|
||||
let clock = app.clock();
|
||||
clock.playhead.update_from_sample(state.position.frame() as f64);
|
||||
state.position.bbt = Some(clock.bbt());
|
||||
state.position
|
||||
})?;
|
||||
jack.sync_follow(sync_follow)?;
|
||||
// FIXME: They don't work properly.
|
||||
Ok(app)
|
||||
})?)?;
|
||||
}
|
||||
|
||||
// Initialize the app state
|
||||
let app = tek(&jack, project, config, ":menu");
|
||||
if matches!(self.action, Action::Headless) {
|
||||
// TODO: Headless mode (daemon + client over IPC, then over network...)
|
||||
println!("todo headless");
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
// Run the [Tui] and [Jack] threads with the [App] state.
|
||||
Tui::new(Box::new(std::io::stdout()))?.run(true, &jack.run(move|jack|{
|
||||
|
||||
// Between jack init and app's first cycle:
|
||||
|
||||
jack.sync_lead(*sync_lead, |mut state|{
|
||||
let clock = app.clock();
|
||||
clock.playhead.update_from_sample(state.position.frame() as f64);
|
||||
state.position.bbt = Some(clock.bbt());
|
||||
state.position
|
||||
})?;
|
||||
|
||||
jack.sync_follow(*sync_follow)?;
|
||||
|
||||
// FIXME: They don't work properly.
|
||||
|
||||
Ok(app)
|
||||
|
||||
})?)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tek_project_new (
|
||||
jack: &Jack,
|
||||
clock: Clock,
|
||||
left_from: &[impl AsRef<str>],
|
||||
left_to: &[impl AsRef<str>],
|
||||
right_from: &[impl AsRef<str>],
|
||||
right_to: &[impl AsRef<str>],
|
||||
midi_from: &[impl AsRef<str>],
|
||||
midi_to: &[impl AsRef<str>],
|
||||
midi_from_re: &[impl AsRef<str>],
|
||||
midi_to_re: &[impl AsRef<str>],
|
||||
) -> Usually<Arrangement> {
|
||||
// TODO: Collect audio IO:
|
||||
let empty = &[] as &[&str];
|
||||
let left_froms = Connect::collect(left_from, empty, empty);
|
||||
let left_tos = Connect::collect(left_to, empty, empty);
|
||||
let right_froms = Connect::collect(right_from, empty, empty);
|
||||
let right_tos = Connect::collect(right_to, empty, empty);
|
||||
let _audio_froms = &[left_froms.as_slice(), right_froms.as_slice()];
|
||||
let _audio_tos = &[left_tos.as_slice(), right_tos.as_slice()];
|
||||
|
||||
// Create initial project:
|
||||
let mut project = Arrangement::new(
|
||||
jack,
|
||||
None,
|
||||
clock,
|
||||
vec![],
|
||||
vec![],
|
||||
Connect::collect(&midi_from, &[] as &[&str], &midi_from_re).iter().enumerate()
|
||||
.map(|(index, connect)|jack.midi_in(&format!("M/{index}"), &[connect.clone()]))
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
Connect::collect(&midi_to, &[] as &[&str], &midi_to_re).iter().enumerate()
|
||||
.map(|(index, connect)|jack.midi_out(&format!("{index}/M"), &[connect.clone()]))
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
);
|
||||
Ok(project)
|
||||
}
|
||||
|
||||
pub fn tek_show_version () {
|
||||
println!("todo version");
|
||||
}
|
||||
192
src/app/config.rs
Normal file
192
src/app/config.rs
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
use crate::{*, bind::*, view::*};
|
||||
|
||||
/// Configuration: mode, view, and bind definitions.
|
||||
///
|
||||
/// ```
|
||||
/// let config = tek::Config::default();
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
/// // Some dizzle.
|
||||
/// // What indentation to use here lol?
|
||||
/// let source = stringify!((mode :menu (name Menu)
|
||||
/// (info Mode selector.) (keys :axis/y :confirm)
|
||||
/// (view (bg (g 0) (bsp/s :ports/out
|
||||
/// (bsp/n :ports/in
|
||||
/// (bg (g 30) (bsp/s (fixed/y 7 :logo)
|
||||
/// (fill :dialog/menu)))))))));
|
||||
/// // Add this definition to the config and try to load it.
|
||||
/// // A "mode" is basically a state machine
|
||||
/// // with associated input and output definitions.
|
||||
/// tek::Config::default().add(&source).unwrap().get_mode(":menu").unwrap();
|
||||
/// ```
|
||||
#[derive(Default, Debug)] pub struct Config {
|
||||
/// XDG base directories of running user.
|
||||
pub dirs: BaseDirectories,
|
||||
/// Active collection of interaction modes.
|
||||
pub modes: Modes,
|
||||
/// Active collection of event bindings.
|
||||
pub binds: Binds,
|
||||
/// Active collection of view definitions.
|
||||
pub views: Views,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
const CONFIG_DIR: &'static str = "tek";
|
||||
const CONFIG_SUB: &'static str = "v0";
|
||||
const CONFIG: &'static str = "tek.edn";
|
||||
const DEFAULTS: &'static str = include_str!("../tek.edn");
|
||||
pub fn init_new (dirs: Option<BaseDirectories>) -> Usually<Self> {
|
||||
let mut config = Self::new(None);
|
||||
config.init()?;
|
||||
Ok(config)
|
||||
}
|
||||
/// Create a new app configuration from a set of XDG base directories,
|
||||
pub fn new (dirs: Option<BaseDirectories>) -> Self {
|
||||
let default = ||BaseDirectories::with_profile(Self::CONFIG_DIR, Self::CONFIG_SUB);
|
||||
let dirs = dirs.unwrap_or_else(default);
|
||||
Self { dirs, ..Default::default() }
|
||||
}
|
||||
/// Write initial contents of configuration.
|
||||
pub fn init (&mut self) -> Usually<()> {
|
||||
self.init_one(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|{
|
||||
cfgs.add(&dsl)?;
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
/// Write initial contents of a configuration file.
|
||||
pub fn init_one (
|
||||
&mut self, path: &str, defaults: &str, mut each: impl FnMut(&mut Self, &str)->Usually<()>
|
||||
) -> Usually<()> {
|
||||
if self.dirs.find_config_file(path).is_none() {
|
||||
//println!("Creating {path:?}");
|
||||
std::fs::write(self.dirs.place_config_file(path)?, defaults)?;
|
||||
}
|
||||
Ok(if let Some(path) = self.dirs.find_config_file(path) {
|
||||
//println!("Loading {path:?}");
|
||||
let src = std::fs::read_to_string(&path)?;
|
||||
src.as_str().each(move|item|each(self, item))?;
|
||||
} else {
|
||||
return Err(format!("{path}: not found").into())
|
||||
})
|
||||
}
|
||||
/// Add statements to configuration from [Dsl] source.
|
||||
pub fn add (&mut self, dsl: impl Language) -> Usually<&mut Self> {
|
||||
dsl.each(|item|self.add_one(item))?;
|
||||
Ok(self)
|
||||
}
|
||||
fn add_one (&self, item: impl Language) -> Usually<()> {
|
||||
if let Some(expr) = item.expr()? {
|
||||
let head = expr.head()?;
|
||||
let tail = expr.tail()?;
|
||||
let name = tail.head()?;
|
||||
let body = tail.tail()?;
|
||||
//println!("Config::load: {} {} {}", head.unwrap_or_default(), name.unwrap_or_default(), body.unwrap_or_default());
|
||||
match head {
|
||||
Some("mode") if let Some(name) = name => load_mode(&self.modes, &name, &body)?,
|
||||
Some("keys") if let Some(name) = name => load_bind(&self.binds, &name, &body)?,
|
||||
Some("view") if let Some(name) = name => load_view(&self.views, &name, &body)?,
|
||||
_ => return Err(format!("Config::load: expected view/keys/mode, got: {item:?}").into())
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
return Err(format!("Config::load: expected expr, got: {item:?}").into())
|
||||
}
|
||||
}
|
||||
pub fn get_mode (&self, mode: impl AsRef<str>) -> Option<Arc<Mode<Arc<str>>>> {
|
||||
self.modes.clone().read().unwrap().get(mode.as_ref()).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn load_mode (modes: &Modes, name: &impl AsRef<str>, body: &impl Language) -> Usually<()> {
|
||||
let mut mode = Mode::default();
|
||||
body.each(|item|mode.add(item))?;
|
||||
modes.write().unwrap().insert(name.as_ref().into(), Arc::new(mode));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Collection of interaction modes.
|
||||
pub type Modes = Arc<RwLock<BTreeMap<Arc<str>, Arc<Mode<Arc<str>>>>>>;
|
||||
|
||||
impl Mode<Arc<str>> {
|
||||
/// Add a definition to the mode.
|
||||
///
|
||||
/// Supported definitions:
|
||||
///
|
||||
/// - (name ...) -> name
|
||||
/// - (info ...) -> description
|
||||
/// - (keys ...) -> key bindings
|
||||
/// - (mode ...) -> submode
|
||||
/// - ... -> view
|
||||
///
|
||||
/// ```
|
||||
/// let mut mode: tek::Mode<std::sync::Arc<str>> = Default::default();
|
||||
/// mode.add("(name hello)").unwrap();
|
||||
/// ```
|
||||
pub fn add (&mut self, dsl: impl Language) -> Usually<()> {
|
||||
Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() {
|
||||
//println!("Mode::add: {head} {:?}", expr.tail());
|
||||
let tail = expr.tail()?.map(|x|x.trim()).unwrap_or("");
|
||||
match head {
|
||||
"name" => self.add_name(tail)?,
|
||||
"info" => self.add_info(tail)?,
|
||||
"keys" => self.add_keys(tail)?,
|
||||
"mode" => self.add_mode(tail)?,
|
||||
_ => self.add_view(tail)?,
|
||||
};
|
||||
} else if let Ok(Some(word)) = dsl.word() {
|
||||
self.add_view(word);
|
||||
} else {
|
||||
return Err(format!("Mode::add: unexpected: {dsl:?}").into());
|
||||
})
|
||||
|
||||
//DslParse(dsl, ||Err(format!("Mode::add: unexpected: {dsl:?}").into()))
|
||||
//.word(|word|self.add_view(word))
|
||||
//.expr(|expr|expr.head(|head|{
|
||||
////println!("Mode::add: {head} {:?}", expr.tail());
|
||||
//let tail = expr.tail()?.map(|x|x.trim()).unwrap_or("");
|
||||
//match head {
|
||||
//"name" => self.add_name(tail),
|
||||
//"info" => self.add_info(tail),
|
||||
//"keys" => self.add_keys(tail)?,
|
||||
//"mode" => self.add_mode(tail)?,
|
||||
//_ => self.add_view(tail),
|
||||
//};
|
||||
//}))
|
||||
}
|
||||
|
||||
fn add_name (&mut self, dsl: impl Language) -> Perhaps<()> {
|
||||
Ok(dsl.src()?.map(|src|self.name.push(src.into())))
|
||||
}
|
||||
fn add_info (&mut self, dsl: impl Language) -> Perhaps<()> {
|
||||
Ok(dsl.src()?.map(|src|self.info.push(src.into())))
|
||||
}
|
||||
fn add_view (&mut self, dsl: impl Language) -> Perhaps<()> {
|
||||
Ok(dsl.src()?.map(|src|self.view.push(src.into())))
|
||||
}
|
||||
fn add_keys (&mut self, dsl: impl Language) -> Perhaps<()> {
|
||||
Ok(Some(dsl.each(|expr|{ self.keys.push(expr.trim().into()); Ok(()) })?))
|
||||
}
|
||||
fn add_mode (&mut self, dsl: impl Language) -> Perhaps<()> {
|
||||
Ok(Some(if let Some(id) = dsl.head()? {
|
||||
load_mode(&self.modes, &id, &dsl.tail())?;
|
||||
} else {
|
||||
return Err(format!("Mode::add: self: incomplete: {dsl:?}").into());
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// Group of view and keys definitions.
|
||||
///
|
||||
/// ```
|
||||
/// let mode = tek::Mode::<std::sync::Arc<str>>::default();
|
||||
/// ```
|
||||
#[derive(Default, Debug)] pub struct Mode<D: Language + Ord> {
|
||||
pub path: PathBuf,
|
||||
pub name: Vec<D>,
|
||||
pub info: Vec<D>,
|
||||
pub view: Vec<D>,
|
||||
pub keys: Vec<D>,
|
||||
pub modes: Modes,
|
||||
}
|
||||
590
src/app/view.rs
Normal file
590
src/app/view.rs
Normal file
|
|
@ -0,0 +1,590 @@
|
|||
use crate::*;
|
||||
|
||||
/// Collection of custom view definitions.
|
||||
pub type Views = Arc<RwLock<BTreeMap<Arc<str>, Arc<str>>>>;
|
||||
|
||||
/// Load custom view definition.
|
||||
pub(crate) fn load_view (views: &Views, name: &impl AsRef<str>, body: &impl Language) -> Usually<()> {
|
||||
views.write().unwrap().insert(name.as_ref().into(), body.src()?.unwrap_or_default().into());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// let _ = tek::view_logo();
|
||||
/// ```
|
||||
pub fn view_logo () -> impl Draw<Tui> {
|
||||
wh_exact(Some(32), Some(7), bold(true, fg(Rgb(240, 200, 180), south!{
|
||||
h_exact(1, ""),
|
||||
h_exact(1, ""),
|
||||
h_exact(1, "~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~"),
|
||||
h_exact(1, east("~~~~ ║ ~ ╟─╌ ~╟─< ~~ ", east(fg(Rgb(230,100,40), "v0.3.0"), " ~~"))),
|
||||
h_exact(1, "~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~"),
|
||||
})))
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// let x = std::sync::Arc::<std::sync::RwLock<String>>::default();
|
||||
/// let _ = tek::view_transport(true, x.clone(), x.clone(), x.clone());
|
||||
/// let _ = tek::view_transport(false, x.clone(), x.clone(), x.clone());
|
||||
/// ```
|
||||
pub fn view_transport (play: bool, bpm: &str, beat: &str, time: &str) -> impl Draw<Tui> {
|
||||
let theme = ItemTheme::G[96];
|
||||
bg(Black, east!(above(
|
||||
wh_full(origin_w(button_play_pause(play))),
|
||||
wh_full(origin_e(east!(
|
||||
field_h(theme, "BPM", bpm),
|
||||
field_h(theme, "Beat", beat),
|
||||
field_h(theme, "Time", time),
|
||||
)))
|
||||
)))
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// let x = std::sync::Arc::<std::sync::RwLock<String>>::default();
|
||||
/// let _ = tek::view_status(None, x.clone(), x.clone(), x.clone());
|
||||
/// let _ = tek::view_status(Some("".into()), x.clone(), x.clone(), x.clone());
|
||||
/// ```
|
||||
pub fn view_status (sel: Option<&str>, sr: &str, buf: &str, lat: &str) -> impl Draw<Tui> {
|
||||
let theme = ItemTheme::G[96];
|
||||
let sr = field_h(theme, "SR", sr);
|
||||
let buf = field_h(theme, "Buf", buf);
|
||||
let lat = field_h(theme, "Lat", lat);
|
||||
bg(Black, east!(above(
|
||||
wh_full(origin_w(sel.map(|sel|field_h(theme, "Selected", sel)))),
|
||||
wh_full(origin_e(east!(sr, buf, lat))),
|
||||
)))
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// let _ = tek::button_play_pause(true);
|
||||
/// ```
|
||||
pub fn button_play_pause (playing: bool) -> impl Draw<Tui> {
|
||||
let compact = true;//self.is_editing();
|
||||
bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) },
|
||||
either(compact,
|
||||
thunk(move|to: &mut Tui|w_exact(9, either(playing,
|
||||
fg(Rgb(0, 255, 0), " PLAYING "),
|
||||
fg(Rgb(255, 128, 0), " STOPPED "))
|
||||
).draw(to)),
|
||||
thunk(move|to: &mut Tui|w_exact(5, either(playing,
|
||||
fg(Rgb(0, 255, 0), south(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)),
|
||||
fg(Rgb(255, 128, 0), south(" ▗▄▖ ", " ▝▀▘ ",)))
|
||||
).draw(to)),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "track")] pub fn view_track_row_section (
|
||||
_theme: ItemTheme,
|
||||
button: impl Draw<Tui>,
|
||||
button_add: impl Draw<Tui>,
|
||||
content: impl Draw<Tui>,
|
||||
) -> impl Draw<Tui> {
|
||||
west(h_full(w_exact(4, origin_nw(button_add))),
|
||||
east(w_exact(20, h_full(origin_nw(button))), wh_full(origin_c(content))))
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// let bg = tengri::ratatui::style::Color::Red;
|
||||
/// let fg = tengri::ratatui::style::Color::Green;
|
||||
/// let _ = tek::view_wrap(bg, fg, "and then blue, too!");
|
||||
/// ```
|
||||
pub fn view_wrap (bg: Color, fg: Color, content: impl Draw<Tui>) -> impl Draw<Tui> {
|
||||
let left = fg_bg(bg, Reset, w_exact(1, y_repeat("▐")));
|
||||
let right = fg_bg(bg, Reset, w_exact(1, y_repeat("▌")));
|
||||
east(left, west(right, fg_bg(fg, bg, content)))
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// let _ = tek::view_meter("", 0.0);
|
||||
/// let _ = tek::view_meters(&[0.0, 0.0]);
|
||||
/// ```
|
||||
pub fn view_meter <'a> (label: &'a str, value: f32) -> impl Draw<Tui> + 'a {
|
||||
let f = field_h(ItemTheme::G[128], label, format!("{:>+9.3}", value));
|
||||
let w = if value >= 0.0 { 13 }
|
||||
else if value >= -1.0 { 12 }
|
||||
else if value >= -2.0 { 11 }
|
||||
else if value >= -3.0 { 10 }
|
||||
else if value >= -4.0 { 9 }
|
||||
else if value >= -6.0 { 8 }
|
||||
else if value >= -9.0 { 7 }
|
||||
else if value >= -12.0 { 6 }
|
||||
else if value >= -15.0 { 5 }
|
||||
else if value >= -20.0 { 4 }
|
||||
else if value >= -25.0 { 3 }
|
||||
else if value >= -30.0 { 2 }
|
||||
else if value >= -40.0 { 1 }
|
||||
else { 0 };
|
||||
let c = if value >= 0.0 { Red }
|
||||
else if value >= -3.0 { Yellow }
|
||||
else { Green };
|
||||
south!(f, wh_exact(Some(w), Some(1), bg(c, ())))
|
||||
}
|
||||
|
||||
pub fn view_meters (values: &[f32;2]) -> impl Draw<Tui> + use<'_> {
|
||||
let left = format!("L/{:>+9.3}", values[0]);
|
||||
let right = format!("R/{:>+9.3}", values[1]);
|
||||
south(left, right)
|
||||
}
|
||||
|
||||
pub fn view_sample_info (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> + use<'_> {
|
||||
when(sample.is_some(), thunk(move|to: &mut Tui|{
|
||||
let sample = sample.unwrap().read().unwrap();
|
||||
let theme = sample.color;
|
||||
east!(
|
||||
field_h(theme, "Name", format!("{:<10}", sample.name.clone())),
|
||||
field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())),
|
||||
field_h(theme, "Start", format!("{:<8}", sample.start)),
|
||||
field_h(theme, "End", format!("{:<8}", sample.end)),
|
||||
field_h(theme, "Trans", "0"),
|
||||
field_h(theme, "Gain", format!("{}", sample.gain)),
|
||||
).draw(to)
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn view_sample_info_v (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> + use<'_> {
|
||||
let a = thunk(move|to: &mut Tui|{
|
||||
let sample = sample.unwrap().read().unwrap();
|
||||
let theme = sample.color;
|
||||
w_exact(20, south!(
|
||||
w_full(origin_w(field_h(theme, "Name ", format!("{:<10}", sample.name.clone())))),
|
||||
w_full(origin_w(field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())))),
|
||||
w_full(origin_w(field_h(theme, "Start ", format!("{:<8}", sample.start)))),
|
||||
w_full(origin_w(field_h(theme, "End ", format!("{:<8}", sample.end)))),
|
||||
w_full(origin_w(field_h(theme, "Trans ", "0"))),
|
||||
w_full(origin_w(field_h(theme, "Gain ", format!("{}", sample.gain)))),
|
||||
)).draw(to)
|
||||
});
|
||||
|
||||
let b = thunk(|to: &mut Tui|fg(Red, south!(
|
||||
bold(true, "× No sample."),
|
||||
"[r] record",
|
||||
"[Shift-F9] import",
|
||||
)).draw(to));
|
||||
|
||||
either(sample.is_some(), a, b)
|
||||
}
|
||||
|
||||
pub fn view_sample_status (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> {
|
||||
bold(true, fg(g(224), sample
|
||||
.map(|sample|{
|
||||
let sample = sample.read().unwrap();
|
||||
format!("Sample {}-{}", sample.start, sample.end)
|
||||
})
|
||||
.unwrap_or_else(||"No sample".to_string())))
|
||||
}
|
||||
|
||||
pub fn view_track_header (theme: ItemTheme, content: impl Draw<Tui>) -> impl Draw<Tui> {
|
||||
w_exact(12, bg(theme.darker.term, w_full(origin_e(content))))
|
||||
}
|
||||
|
||||
pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, ports: &'a [T])
|
||||
-> impl Draw<Tui> + use<'a, T>
|
||||
{
|
||||
let ins = ports.len() as u16;
|
||||
let frame = Outer(true, Style::default().fg(g(96)));
|
||||
let iter = move||ports.iter();
|
||||
let names = iter_south(iter, move|port, index|h_full(origin_w(format!(" {index} {}", port.port_name()))));
|
||||
let field = field_v(theme, title, names);
|
||||
wh_exact(Some(20), Some(1 + ins), border(true, frame, wh_exact(Some(20), Some(1 + ins), field)))
|
||||
}
|
||||
|
||||
pub fn view_io_ports <'a, T: PortsSizes<'a>> (
|
||||
fg: Color, bg: Color, items: impl Fn()->T + Send + Sync + 'a
|
||||
) -> impl Draw<Tui> + 'a {
|
||||
type Item<'a> = (usize, &'a Arc<str>, &'a [Connect], usize, usize);
|
||||
iter(items, move|(_index, name, connections, y, y2): Item<'a>, _| {
|
||||
y_push(y as u16, h_exact((y2-y) as u16,
|
||||
south(h_full(bold(true, fg_bg(fg, bg, origin_w(east(" ", name))))),
|
||||
iter(||connections.iter(), move|connect: &'a Connect, index|y_push(
|
||||
index as u16, h_exact(1, origin_w(bold(false, fg_bg(fg, bg, &connect.info))))
|
||||
)))))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn view_scenes_clips (
|
||||
scenes: impl ScenesSizes<'_>,
|
||||
tracks: impl TracksSizes<'_>,
|
||||
select: &Selection,
|
||||
editor: Option<&MidiEditor>,
|
||||
size: &Size,
|
||||
is_editing: bool,
|
||||
) -> impl Draw<Tui> {
|
||||
size.of(wh_full(above(wh_full(origin_se(fg(Green, format!("{}x{}", size.w(), size.h())))),
|
||||
thunk(|to: &mut Tui|{
|
||||
for (index, track, _, _) in tracks {
|
||||
let clips = view_track_clips(scenes, select, editor, index, track, is_editing);
|
||||
w_exact(track.width as u16, h_full(clips)).draw(to);
|
||||
}
|
||||
Ok(XYWH(0, 0, 0, 0))
|
||||
}))))
|
||||
}
|
||||
|
||||
pub fn view_track_clips (
|
||||
scenes: impl ScenesSizes<'_>,
|
||||
select: &Selection,
|
||||
editor: &Option<MidiEditor>,
|
||||
index: usize,
|
||||
track: &Track,
|
||||
is_editing: bool,
|
||||
) -> impl Draw<Tui> {
|
||||
thunk(move|to: &mut Tui|{
|
||||
|
||||
for (scene_index, scene, ..) in scenes {
|
||||
|
||||
let (
|
||||
name, theme
|
||||
): (Arc<str>, ItemTheme) = if let Some(Some(clip)) = &scene.clips.get(index) {
|
||||
let clip = clip.read().unwrap();
|
||||
(format!(" ⏹ {}", &clip.name).into(), clip.color)
|
||||
} else {
|
||||
(" ⏹ -- ".into(), ItemTheme::G[32])
|
||||
};
|
||||
|
||||
let fg = theme.lightest.term;
|
||||
|
||||
let mut outline = theme.base.term;
|
||||
|
||||
let bg = if select.track() == Some(index) && select.scene() == Some(scene_index) {
|
||||
outline = theme.lighter.term;
|
||||
theme.light.term
|
||||
} else if select.track() == Some(index) || select.scene() == Some(scene_index) {
|
||||
outline = theme.darkest.term;
|
||||
theme.base.term
|
||||
} else {
|
||||
theme.dark.term
|
||||
};
|
||||
|
||||
let w = if select.track() == Some(index) && let Some(editor) = editor {
|
||||
(editor.size.w() as usize).max(24).max(track.width)
|
||||
} else {
|
||||
track.width
|
||||
} as u16;
|
||||
|
||||
let y = if select.scene() == Some(scene_index) && let Some(editor) = editor {
|
||||
(editor.size.h() as usize).max(12)
|
||||
} else {
|
||||
Self::H_SCENE as usize
|
||||
} as u16;
|
||||
|
||||
let is_selected = is_editing && select.track() == Some(index) && select.scene() == Some(scene_index);
|
||||
|
||||
wh_exact(Some(w), Some(y), below(
|
||||
wh_full(Outer(true, Style::default().fg(outline))),
|
||||
wh_full(below(
|
||||
below(
|
||||
fg_bg(outline, bg, wh_full("")),
|
||||
wh_full(origin_nw(fg_bg(fg, bg, bold(true, &name)))),
|
||||
),
|
||||
wh_full(when(is_selected, editor.map(|e|e.view()))))))
|
||||
).draw(to);
|
||||
|
||||
}
|
||||
|
||||
Ok(XYWH(0, 0, 0, 0))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn view_track_names (
|
||||
theme: ItemTheme,
|
||||
tracks: impl TracksSizes<'_>,
|
||||
track_count: usize,
|
||||
scene_count: usize,
|
||||
selected: &Selection,
|
||||
) -> impl Draw<Tui> {
|
||||
let button = south(
|
||||
button_3("t", "rack ", format!("{}{track_count}", selected.track()
|
||||
.map(|track|format!("{track}/")).unwrap_or_default()), false),
|
||||
button_3("s", "cene ", format!("{}{scene_count}", selected.scene()
|
||||
.map(|scene|format!("{scene}/")).unwrap_or_default()), false));
|
||||
let button_2 = south(
|
||||
button_2("T", "+", false),
|
||||
button_2("S", "+", false));
|
||||
view_track_row_section(theme, button, button_2, bg(theme.darker.term,
|
||||
h_exact(2, thunk(|to: &mut Tui|{
|
||||
for (index, track, x1, _x2) in tracks {
|
||||
x_push(x1 as u16, w_exact(track_width(index, track),
|
||||
bg(if selected.track() == Some(index) {
|
||||
track.color.light.term
|
||||
} else {
|
||||
track.color.base.term
|
||||
}, south(w_full(origin_nw(east(
|
||||
format!("·t{index:02} "),
|
||||
fg(Rgb(255, 255, 255), bold(true, &track.name))
|
||||
))), ""))) ).draw(to);
|
||||
}
|
||||
Ok(XYWH(0, 0, 0, 0))
|
||||
}))))
|
||||
}
|
||||
|
||||
pub fn view_track_outputs (
|
||||
theme: ItemTheme,
|
||||
tracks: impl TracksSizes<'_>,
|
||||
midi_outs: impl Iterator<Item = ()>,
|
||||
) -> impl Draw<Tui> {
|
||||
view_track_row_section(theme,
|
||||
south(w_full(origin_w(button_2("o", "utput", false))),
|
||||
thunk(|to: &mut Tui|{
|
||||
for port in midi_outs {
|
||||
w_full(origin_w(port.port_name())).draw(to);
|
||||
}
|
||||
Ok(XYWH(0, 0, 0, 0))
|
||||
})),
|
||||
button_2("O", "+", false),
|
||||
bg(theme.darker.term, origin_w(thunk(|to: &mut Tui|{
|
||||
for (index, track, _x1, _x2) in tracks {
|
||||
let iter = ||track.sequencer.midi_outs.iter();
|
||||
let draw = |port: &MidiOutput, _|fg(Rgb(255, 255, 255),
|
||||
h_exact(1, bg(track.color.dark.term, w_full(origin_w(
|
||||
format!("·o{index:02} {}", port.port_name()))))));
|
||||
w_exact(track_width(index, track),
|
||||
origin_nw(h_full(iter_south(iter, draw)))).draw(to);
|
||||
}
|
||||
Ok(XYWH(0, 0, 0, 0))
|
||||
}))))
|
||||
}
|
||||
|
||||
pub fn view_track_inputs (
|
||||
theme: ItemTheme,
|
||||
tracks: impl TracksSizes<'_>,
|
||||
h: u16,
|
||||
) -> impl Draw<Tui> {
|
||||
view_track_row_section(theme, button_2("i", "nput", false), button_2("I", "+", false),
|
||||
bg(theme.darker.term, origin_w(thunk(move|to: &mut Tui|{
|
||||
for (index, track, _x1, _x2) in tracks {
|
||||
wh_exact(Some(track_width(index, track)), Some(h + 1),
|
||||
origin_nw(south(
|
||||
bg(track.color.base.term,
|
||||
w_full(origin_w(east!(
|
||||
either(track.sequencer.monitoring, fg(Green, "●mon "), "·mon "),
|
||||
either(track.sequencer.recording, fg(Red, "●rec "), "·rec "),
|
||||
either(track.sequencer.overdub, fg(Yellow, "●dub "), "·dub "),
|
||||
)))),
|
||||
iter_south(||track.sequencer.midi_ins.iter(),
|
||||
|port, _|fg_bg(Rgb(255, 255, 255), track.color.dark.term,
|
||||
w_full(origin_w(format!("·i{index:02} {}", port.port_name()))))))))
|
||||
.draw(to);
|
||||
}
|
||||
Ok(XYWH(0, 0, 0, 0))
|
||||
}))))
|
||||
}
|
||||
|
||||
pub fn view_scenes_names (
|
||||
scenes: &impl ScenesSizes<'_>
|
||||
) -> impl Draw<Tui> {
|
||||
w_exact(20, thunk(|to: &mut Tui|{
|
||||
for (index, scene, ..) in scenes {
|
||||
view_scene_name(index, scene).draw(to);
|
||||
}
|
||||
Ok(XYWH(1, 1, 1, 1))
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn view_scene_name (
|
||||
select: Option<&Selection>,
|
||||
editor: Option<&MidiEditor>,
|
||||
index: usize,
|
||||
scene: &Scene,
|
||||
editing: bool
|
||||
) -> impl Draw<Tui> {
|
||||
let h = if select.scene() == Some(index) && let Some(_editor) = editor {
|
||||
7
|
||||
} else {
|
||||
Self::H_SCENE as u16
|
||||
};
|
||||
let a = w_full(origin_w(east(format!("·s{index:02} "),
|
||||
fg(g(255), bold(true, &scene.name)))));
|
||||
let b = when(select.scene() == Some(index) && editing,
|
||||
wh_full(origin_nw(south(
|
||||
editor.as_ref().map(|e|e.clip_status()),
|
||||
editor.as_ref().map(|e|e.edit_status())))));
|
||||
let c = if select.scene() == Some(index) {
|
||||
scene.color.light.term
|
||||
} else {
|
||||
scene.color.base.term
|
||||
};
|
||||
wh_exact(Some(20), Some(h), bg(c, origin_nw(south(a, b))))
|
||||
}
|
||||
|
||||
pub fn view_midi_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw<Tui> {
|
||||
track.map(move|track|view_ports_status(theme, "MIDI ins: ", &track.sequencer.midi_ins))
|
||||
}
|
||||
|
||||
pub fn view_midi_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw<Tui> {
|
||||
track.map(move|track|view_ports_status(theme, "MIDI outs: ", &track.sequencer.midi_outs))
|
||||
}
|
||||
|
||||
pub fn view_audio_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw<Tui> {
|
||||
track.map(move|track|view_ports_status(theme, "Audio ins: ", &track.audio_ins()))
|
||||
}
|
||||
|
||||
pub fn view_audio_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw<Tui> {
|
||||
track.map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs()))
|
||||
}
|
||||
|
||||
pub fn view_track_per (
|
||||
tracks: impl TracksSizes<'_>
|
||||
) -> impl Draw<Tui> {
|
||||
iter_east(tracks, move|(index, track, x1, x2): (usize, &Track, usize, usize), _|{
|
||||
w_exact((x2 - x1) as u16, fg_bg(
|
||||
track.color.lightest.term,
|
||||
track.color.base.term,
|
||||
callback(index, track)))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn view_per_track () -> impl Draw<Tui> {}
|
||||
|
||||
pub fn view_per_track_top () -> impl Draw<Tui> {}
|
||||
|
||||
pub fn view_inputs (
|
||||
tracks: impl TracksSizes<'_>,
|
||||
midi_ins: &[MidiIn],
|
||||
) -> impl Draw<Tui> {
|
||||
let header = h_exact(1, view_inputs_header(tracks));
|
||||
south(header, thunk(|to: &mut Tui|{
|
||||
for (index, port) in midi_ins.iter().enumerate() {
|
||||
x_push(index as u16 * 10, h_exact(1, view_inputs_row(tracks, port))).draw(to);
|
||||
}
|
||||
todo!()
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn view_inputs_header (
|
||||
tracks: impl TracksSizes<'_>,
|
||||
midi_ins: &[MidiIn],
|
||||
) -> impl Draw<Tui> {
|
||||
east(w_exact(20, origin_w(button_3("i", "nput ", format!("{}", midi_ins.len()), false))),
|
||||
west(w_exact(4, button_2("I", "+", false)), thunk(move|to: &mut Tui|{
|
||||
for (_index, track, x1, _x2) in tracks {
|
||||
#[cfg(feature = "track")]
|
||||
x_push(x1 as u16, bg(track.color.dark.term, origin_w(w_exact(track.width as u16, east!(
|
||||
either(track.sequencer.monitoring, fg(Green, "mon "), "mon "),
|
||||
either(track.sequencer.recording, fg(Red, "rec "), "rec "),
|
||||
either(track.sequencer.overdub, fg(Yellow, "dub "), "dub "),
|
||||
))))).draw(to);
|
||||
}
|
||||
todo!()
|
||||
})))
|
||||
}
|
||||
|
||||
pub fn view_inputs_row (
|
||||
tracks: impl TracksSizes<'_>,
|
||||
port: ()
|
||||
) -> impl Draw<Tui> {
|
||||
east(w_exact(20, origin_w(east(" ● ", bold(true, fg(Rgb(255,255,255), port.port_name()))))),
|
||||
west(w_exact(4, ()), thunk(move|to: &mut Tui|{
|
||||
for (_index, track, _x1, _x2) in tracks {
|
||||
#[cfg(feature = "track")]
|
||||
bg(track.color.darker.term, origin_w(w_exact(track.width as u16, east!(
|
||||
either(track.sequencer.monitoring, fg(Green, " ● "), " · "),
|
||||
either(track.sequencer.recording, fg(Red, " ● "), " · "),
|
||||
either(track.sequencer.overdub, fg(Yellow, " ● "), " · "),
|
||||
)))).draw(to);
|
||||
}
|
||||
todo!()
|
||||
})))
|
||||
}
|
||||
|
||||
pub fn view_outputs (
|
||||
theme: ItemTheme,
|
||||
tracks: impl TracksSizes<'_>,
|
||||
midi_outs: &[MidiOutput],
|
||||
height: u16,
|
||||
) -> impl Draw<Tui> {
|
||||
|
||||
let list = south(
|
||||
h_exact(1, w_full(origin_w(button_3(
|
||||
"o", "utput", format!("{}", midi_outs.len()), false
|
||||
)))),
|
||||
h_exact(height - 1, wh_full(origin_nw(thunk(|to: &mut Tui|{
|
||||
for (_index, port) in midi_outs.iter().enumerate() {
|
||||
h_exact(1,w_full(east(
|
||||
origin_w(east(" ● ", fg(Rgb(255,255,255), bold(true, port.port_name())))),
|
||||
w_full(origin_e(format!("{}/{} ",
|
||||
port.port().get_connections().len(),
|
||||
port.connections.len())))))).draw(to);
|
||||
for (index, conn) in port.connections.iter().enumerate() {
|
||||
h_exact(1, w_full(origin_w(format!(" c{index:02}{}", conn.info()))))
|
||||
.draw(to);
|
||||
}
|
||||
}
|
||||
todo!();
|
||||
}))))
|
||||
);
|
||||
|
||||
h_exact(height, view_track_row_section(theme, list, button_2("O", "+", false),
|
||||
bg(theme.darker.term, origin_w(w_full(
|
||||
thunk(|to: &mut Tui|{
|
||||
for (index, track, _x1, _x2) in tracks {
|
||||
w_exact(track_width(index, track),
|
||||
thunk(|to: &mut Tui|{
|
||||
h_exact(1, origin_w(east(
|
||||
either(true, fg(Green, "play "), "play "),
|
||||
either(false, fg(Yellow, "solo "), "solo "),
|
||||
))).draw(to);
|
||||
for (_index, port) in midi_outs.iter().enumerate() {
|
||||
h_exact(1, origin_w(east(
|
||||
either(true, fg(Green, " ● "), " · "),
|
||||
either(false, fg(Yellow, " ● "), " · "),
|
||||
))).draw(to);
|
||||
for (_index, _conn) in port.connections.iter().enumerate() {
|
||||
h_exact(1, w_full("")).draw(to);
|
||||
}
|
||||
}
|
||||
todo!()
|
||||
})
|
||||
).draw(to);
|
||||
}
|
||||
todo!()
|
||||
})
|
||||
)))
|
||||
))
|
||||
}
|
||||
|
||||
pub fn view_track_devices (
|
||||
theme: ItemTheme,
|
||||
tracks: &impl TracksSizes<'_>,
|
||||
track: Option<&Track>,
|
||||
h: u16,
|
||||
) -> impl Draw<Tui> {
|
||||
view_track_row_section(theme,
|
||||
button_3("d", "evice", format!("{}", track.map(|t|t.devices.len()).unwrap_or(0)), false),
|
||||
button_2("D", "+", false),
|
||||
iter(tracks, move|(_, track, _x1, _x2), index| wh_exact(
|
||||
Some(track_width(index, track)),
|
||||
Some(h + 1),
|
||||
bg(track.color.dark.term, origin_nw(iter_south(move||0..h,
|
||||
|_, _index|wh_exact(Some(track.width as u16), Some(2),
|
||||
fg_bg(
|
||||
ItemTheme::G[32].lightest.term,
|
||||
ItemTheme::G[32].dark.term,
|
||||
origin_nw(format!(" · {}", "--"))
|
||||
)
|
||||
)))))))
|
||||
}
|
||||
|
||||
pub fn per_track <'a, T: Draw<Tui> + 'a, U: TracksSizes<'a>> (
|
||||
tracks: impl Fn() -> U + Send + Sync + 'a,
|
||||
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
|
||||
) -> impl Draw<Tui> + 'a {
|
||||
per_track_top(tracks, move|index, track|h_full(origin_y(callback(index, track))))
|
||||
}
|
||||
|
||||
pub fn per_track_top <'a, T: Draw<Tui> + 'a, U: TracksSizes<'a>> (
|
||||
tracks: impl Fn() -> U + Send + Sync + 'a,
|
||||
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
|
||||
) -> impl Draw<Tui> + 'a {
|
||||
origin_x(bg(Reset, iter_east(tracks,
|
||||
move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{
|
||||
w_exact((x2 - x1) as u16, fg_bg(
|
||||
track.color.lightest.term,
|
||||
track.color.base.term,
|
||||
callback(index, track))) })))
|
||||
}
|
||||
|
||||
pub fn field_h <T: Screen> (theme: ItemTheme, head: impl Draw<T>, body: impl Draw<T>) -> impl Draw<T> {
|
||||
}
|
||||
|
||||
pub fn field_v <T: Screen> (theme: ItemTheme, head: impl Draw<T>, body: impl Draw<T>) -> impl Draw<T> {
|
||||
}
|
||||
904
src/arrange.rs
904
src/arrange.rs
|
|
@ -1,904 +0,0 @@
|
|||
use ::std::sync::{Arc, RwLock};
|
||||
use ::tengri::{space::east, color::ItemTheme};
|
||||
use ::tengri::{draw::*, term::*};
|
||||
use crate::{*, device::*, editor::*, sequence::*, clock::*, select::*, sample::*};
|
||||
|
||||
/// Arranger.
|
||||
///
|
||||
/// ```
|
||||
/// let arranger = tek::Arrangement::default();
|
||||
/// ```
|
||||
#[derive(Default, Debug)] pub struct Arrangement {
|
||||
/// Project name.
|
||||
pub name: Arc<str>,
|
||||
/// Base color.
|
||||
pub color: ItemTheme,
|
||||
/// JACK client handle.
|
||||
pub jack: Jack<'static>,
|
||||
/// FIXME a render of the project arrangement, redrawn on update.
|
||||
/// TODO rename to "render_cache" or smth
|
||||
pub arranger: Arc<RwLock<Buffer>>,
|
||||
/// Display size
|
||||
pub size: Size,
|
||||
/// Display size of clips area
|
||||
pub size_inner: Size,
|
||||
/// Source of time
|
||||
#[cfg(feature = "clock")] pub clock: Clock,
|
||||
/// Allows one MIDI clip to be edited
|
||||
#[cfg(feature = "editor")] pub editor: Option<MidiEditor>,
|
||||
/// List of global midi inputs
|
||||
#[cfg(feature = "port")] pub midi_ins: Vec<MidiInput>,
|
||||
/// List of global midi outputs
|
||||
#[cfg(feature = "port")] pub midi_outs: Vec<MidiOutput>,
|
||||
/// List of global audio inputs
|
||||
#[cfg(feature = "port")] pub audio_ins: Vec<AudioInput>,
|
||||
/// List of global audio outputs
|
||||
#[cfg(feature = "port")] pub audio_outs: Vec<AudioOutput>,
|
||||
/// Selected UI element
|
||||
#[cfg(feature = "select")] pub selection: Selection,
|
||||
/// Last track number (to avoid duplicate port names)
|
||||
#[cfg(feature = "track")] pub track_last: usize,
|
||||
/// List of tracks
|
||||
#[cfg(feature = "track")] pub tracks: Vec<Track>,
|
||||
/// Scroll offset of tracks
|
||||
#[cfg(feature = "track")] pub track_scroll: usize,
|
||||
/// List of scenes
|
||||
#[cfg(feature = "scene")] pub scenes: Vec<Scene>,
|
||||
/// Scroll offset of scenes
|
||||
#[cfg(feature = "scene")] pub scene_scroll: usize,
|
||||
}
|
||||
|
||||
/// A track consists of a sequencer and zero or more devices chained after it.
|
||||
///
|
||||
/// ```
|
||||
/// let track: tek::Track = Default::default();
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct Track {
|
||||
/// Name of track
|
||||
pub name: Arc<str>,
|
||||
/// Identifying color of track
|
||||
pub color: ItemTheme,
|
||||
/// Preferred width of track column
|
||||
pub width: usize,
|
||||
/// MIDI sequencer state
|
||||
pub sequencer: Sequencer,
|
||||
/// Device chain
|
||||
pub devices: Vec<Device>,
|
||||
}
|
||||
|
||||
/// A scene consists of a set of clips to play together.
|
||||
///
|
||||
/// ```
|
||||
/// let scene: tek::Scene = Default::default();
|
||||
/// let _ = scene.pulses();
|
||||
/// let _ = scene.is_playing(&[]);
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct Scene {
|
||||
/// Name of scene
|
||||
pub name: Arc<str>,
|
||||
/// Identifying color of scene
|
||||
pub color: ItemTheme,
|
||||
/// Clips in scene, one per track
|
||||
pub clips: Vec<Option<Arc<RwLock<MidiClip>>>>,
|
||||
}
|
||||
|
||||
impl_has!(Jack<'static>: |self: Arrangement| self.jack);
|
||||
impl HasJack<'static> for Arrangement {
|
||||
fn jack (&self) -> &Jack<'static> { &self.jack }
|
||||
}
|
||||
|
||||
impl_has!(Size: |self: Arrangement| self.size);
|
||||
impl_has!(Vec<Track>: |self: Arrangement| self.tracks);
|
||||
impl_has!(Vec<Scene>: |self: Arrangement| self.scenes);
|
||||
impl_has!(Vec<MidiInput>: |self: Arrangement| self.midi_ins);
|
||||
impl_has!(Vec<MidiOutput>: |self: Arrangement| self.midi_outs);
|
||||
impl_has!(Clock: |self: Arrangement| self.clock);
|
||||
impl_has!(Selection: |self: Arrangement| self.selection);
|
||||
impl_as_ref_opt!(MidiEditor: |self: Arrangement| self.editor.as_ref());
|
||||
impl_as_mut_opt!(MidiEditor: |self: Arrangement| self.editor.as_mut());
|
||||
impl_as_ref_opt!(Track: |self: Arrangement| self.selected_track());
|
||||
impl_as_mut_opt!(Track: |self: Arrangement| self.selected_track_mut());
|
||||
|
||||
impl <T: AsRef<Vec<Scene>>+AsMut<Vec<Scene>>> HasScenes for T {}
|
||||
impl <T: AsRef<Vec<Track>>+AsMut<Vec<Track>>> HasTracks for T {}
|
||||
impl <T: AsRefOpt<Scene>+AsMutOpt<Scene>+Send+Sync> HasScene for T {}
|
||||
impl <T: AsRefOpt<Track>+AsMutOpt<Track>+Send+Sync> HasTrack for T {}
|
||||
impl <T: ScenesView+HasMidiIns+HasMidiOuts+HasTrackScroll> TracksView for T {}
|
||||
impl <T: TracksView+ScenesView+Send+Sync> ClipsView for T {}
|
||||
|
||||
pub trait ClipsView: TracksView + ScenesView {
|
||||
|
||||
fn view_scenes_clips <'a> (&'a self)
|
||||
-> impl Draw<Tui> + 'a
|
||||
{
|
||||
self.clips_size().of(wh_full(above(
|
||||
wh_full(origin_se(fg(Green, format!("{}x{}", self.clips_size().w(), self.clips_size().h())))),
|
||||
thunk(|to: &mut Tui|{
|
||||
for (track_index, track, _, _) in self.tracks_with_sizes() {
|
||||
let w = track.width as u16;
|
||||
let v = self.view_track_clips(track_index, track);
|
||||
w_exact(w, h_full(v)).draw(to);
|
||||
}
|
||||
Ok(XYWH(0, 0, 0, 0))
|
||||
}))))
|
||||
}
|
||||
|
||||
fn view_track_clips <'a> (&'a self, track_index: usize, track: &'a Track) -> impl Draw<Tui> + 'a {
|
||||
thunk(move|to: &mut Tui|{
|
||||
for (scene_index, scene, ..) in self.scenes_with_sizes() {
|
||||
let (name, theme): (Arc<str>, ItemTheme) = if let Some(Some(clip)) = &scene.clips.get(track_index) {
|
||||
let clip = clip.read().unwrap();
|
||||
(format!(" ⏹ {}", &clip.name).into(), clip.color)
|
||||
} else {
|
||||
(" ⏹ -- ".into(), ItemTheme::G[32])
|
||||
};
|
||||
let fg = theme.lightest.term;
|
||||
let mut outline = theme.base.term;
|
||||
let bg = if self.selection().track() == Some(track_index)
|
||||
&& self.selection().scene() == Some(scene_index)
|
||||
{
|
||||
outline = theme.lighter.term;
|
||||
theme.light.term
|
||||
} else if self.selection().track() == Some(track_index)
|
||||
|| self.selection().scene() == Some(scene_index)
|
||||
{
|
||||
outline = theme.darkest.term;
|
||||
theme.base.term
|
||||
} else {
|
||||
theme.dark.term
|
||||
};
|
||||
let w = if self.selection().track() == Some(track_index)
|
||||
&& let Some(editor) = self.editor ()
|
||||
{
|
||||
(editor.size.w() as usize).max(24).max(track.width)
|
||||
} else {
|
||||
track.width
|
||||
} as u16;
|
||||
let y = if self.selection().scene() == Some(scene_index)
|
||||
&& let Some(editor) = self.editor ()
|
||||
{
|
||||
(editor.size.h() as usize).max(12)
|
||||
} else {
|
||||
Self::H_SCENE as usize
|
||||
} as u16;
|
||||
|
||||
let is_selected =
|
||||
self.selection().track() == Some(track_index) &&
|
||||
self.selection().scene() == Some(scene_index) &&
|
||||
self.is_editing();
|
||||
|
||||
wh_exact(Some(w), Some(y), below(
|
||||
wh_full(Outer(true, Style::default().fg(outline))),
|
||||
wh_full(below(
|
||||
below(
|
||||
fg_bg(outline, bg, wh_full("")),
|
||||
wh_full(origin_nw(fg_bg(fg, bg, bold(true, &name)))),
|
||||
),
|
||||
wh_full(when(is_selected, self.editor().map(|e|e.view()))))))).draw(to);
|
||||
}
|
||||
Ok(XYWH(0, 0, 0, 0))
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll {
|
||||
|
||||
//fn tracks_width_available (&self) -> u16 {
|
||||
//(self.size.width() as u16).saturating_sub(40)
|
||||
//}
|
||||
|
||||
/// Iterate over tracks with their corresponding sizes.
|
||||
fn tracks_with_sizes (&self) -> impl TracksSizes<'_> {
|
||||
let _editor_width = self.editor().map(|e|e.size.w());
|
||||
let _active_track = self.selection().track();
|
||||
let mut x = 0;
|
||||
self.tracks().iter().enumerate().map_while(move |(index, track)|{
|
||||
let width = track.width.max(8);
|
||||
if x + width < self.clips_size().w() as usize {
|
||||
let data = (index, track, x, x + width);
|
||||
x += width + Self::TRACK_SPACING;
|
||||
Some(data)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn view_track_names (&self, theme: ItemTheme) -> impl Draw<Tui> {
|
||||
let track_count = self.tracks().len();
|
||||
let scene_count = self.scenes().len();
|
||||
let selected = self.selection();
|
||||
let button = south(
|
||||
button_3("t", "rack ", format!("{}{track_count}", selected.track()
|
||||
.map(|track|format!("{track}/")).unwrap_or_default()), false),
|
||||
button_3("s", "cene ", format!("{}{scene_count}", selected.scene()
|
||||
.map(|scene|format!("{scene}/")).unwrap_or_default()), false));
|
||||
let button_2 = south(
|
||||
button_2("T", "+", false),
|
||||
button_2("S", "+", false));
|
||||
view_track_row_section(theme, button, button_2, bg(theme.darker.term,
|
||||
h_exact(2, thunk(|to: &mut Tui|{
|
||||
for (index, track, x1, _x2) in self.tracks_with_sizes() {
|
||||
x_push(x1 as u16, w_exact(track_width(index, track),
|
||||
bg(if selected.track() == Some(index) {
|
||||
track.color.light.term
|
||||
} else {
|
||||
track.color.base.term
|
||||
}, south(w_full(origin_nw(east(
|
||||
format!("·t{index:02} "),
|
||||
fg(Rgb(255, 255, 255), bold(true, &track.name))
|
||||
))), ""))) ).draw(to);
|
||||
}
|
||||
Ok(XYWH(0, 0, 0, 0))
|
||||
}))))
|
||||
}
|
||||
|
||||
fn view_track_outputs <'a> (&'a self, theme: ItemTheme, _h: u16) -> impl Draw<Tui> {
|
||||
view_track_row_section(theme,
|
||||
south(w_full(origin_w(button_2("o", "utput", false))),
|
||||
thunk(|to: &mut Tui|{
|
||||
for port in self.midi_outs().iter() {
|
||||
w_full(origin_w(port.port_name())).draw(to);
|
||||
}
|
||||
Ok(XYWH(0, 0, 0, 0))
|
||||
})),
|
||||
button_2("O", "+", false),
|
||||
bg(theme.darker.term, origin_w(thunk(|to: &mut Tui|{
|
||||
for (index, track, _x1, _x2) in self.tracks_with_sizes() {
|
||||
let iter = ||track.sequencer.midi_outs.iter();
|
||||
let draw = |port: &MidiOutput, _|fg(Rgb(255, 255, 255),
|
||||
h_exact(1, bg(track.color.dark.term, w_full(origin_w(
|
||||
format!("·o{index:02} {}", port.port_name()))))));
|
||||
w_exact(track_width(index, track),
|
||||
origin_nw(h_full(iter_south(iter, draw)))).draw(to);
|
||||
}
|
||||
Ok(XYWH(0, 0, 0, 0))
|
||||
}))))
|
||||
}
|
||||
|
||||
fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Draw<Tui> {
|
||||
let mut h = 0u16;
|
||||
for track in self.tracks().iter() {
|
||||
h = h.max(track.sequencer.midi_ins.len() as u16);
|
||||
}
|
||||
let content = thunk(move|to: &mut Tui|{
|
||||
for (index, track, _x1, _x2) in self.tracks_with_sizes() {
|
||||
wh_exact(Some(track_width(index, track)), Some(h + 1),
|
||||
origin_nw(south(
|
||||
bg(track.color.base.term,
|
||||
w_full(origin_w(east!(
|
||||
either(track.sequencer.monitoring, fg(Green, "●mon "), "·mon "),
|
||||
either(track.sequencer.recording, fg(Red, "●rec "), "·rec "),
|
||||
either(track.sequencer.overdub, fg(Yellow, "●dub "), "·dub "),
|
||||
)))),
|
||||
iter_south(||track.sequencer.midi_ins.iter(),
|
||||
|port, _|fg_bg(Rgb(255, 255, 255), track.color.dark.term,
|
||||
w_full(origin_w(format!("·i{index:02} {}", port.port_name()))))))))
|
||||
.draw(to);
|
||||
}
|
||||
Ok(XYWH(0, 0, 0, 0))
|
||||
});
|
||||
view_track_row_section(theme, button_2("i", "nput", false), button_2("I", "+", false),
|
||||
bg(theme.darker.term, origin_w(content)))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + Send + Sync {
|
||||
/// Default scene height.
|
||||
const H_SCENE: usize = 2;
|
||||
/// Default editor height.
|
||||
const H_EDITOR: usize = 15;
|
||||
fn h_scenes (&self) -> u16;
|
||||
fn w_side (&self) -> u16;
|
||||
fn w_mid (&self) -> u16;
|
||||
fn scenes_with_sizes (&self) -> impl ScenesSizes<'_> {
|
||||
let mut y = 0;
|
||||
self.scenes().iter().enumerate().skip(self.scene_scroll()).map_while(move|(s, scene)|{
|
||||
let height = if self.selection().scene() == Some(s) && self.editor().is_some() {
|
||||
8
|
||||
} else {
|
||||
Self::H_SCENE
|
||||
};
|
||||
if y + height <= self.clips_size().h() as usize {
|
||||
let data = (s, scene, y, y + height);
|
||||
y += height;
|
||||
Some(data)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn view_scenes_names (&self) -> impl Draw<Tui> {
|
||||
w_exact(20, thunk(|to: &mut Tui|{
|
||||
for (index, scene, ..) in self.scenes_with_sizes() {
|
||||
self.view_scene_name(index, scene).draw(to);
|
||||
}
|
||||
Ok(XYWH(1, 1, 1, 1))
|
||||
}))
|
||||
}
|
||||
|
||||
fn view_scene_name <'a> (&'a self, index: usize, scene: &'a Scene) -> impl Draw<Tui> + 'a {
|
||||
let h = if self.selection().scene() == Some(index) && let Some(_editor) = self.editor() {
|
||||
7
|
||||
} else {
|
||||
Self::H_SCENE as u16
|
||||
};
|
||||
let a = w_full(origin_w(east(format!("·s{index:02} "),
|
||||
fg(g(255), bold(true, &scene.name)))));
|
||||
let b = when(self.selection().scene() == Some(index) && self.is_editing(),
|
||||
wh_full(origin_nw(south(
|
||||
self.editor().as_ref().map(|e|e.clip_status()),
|
||||
self.editor().as_ref().map(|e|e.edit_status())))));
|
||||
let c = if self.selection().scene() == Some(index) {
|
||||
scene.color.light.term
|
||||
} else {
|
||||
scene.color.base.term
|
||||
};
|
||||
wh_exact(Some(20), Some(h), bg(c, origin_nw(south(a, b))))
|
||||
}
|
||||
|
||||
}
|
||||
pub trait HasSceneScroll: HasScenes { fn scene_scroll (&self) -> usize; }
|
||||
pub trait HasTrackScroll: HasTracks { fn track_scroll (&self) -> usize; }
|
||||
pub trait HasScene: AsRefOpt<Scene> + AsMutOpt<Scene> {
|
||||
fn scene_mut (&mut self) -> Option<&mut Scene> { self.as_mut_opt() }
|
||||
fn scene (&self) -> Option<&Scene> { self.as_ref_opt() }
|
||||
}
|
||||
pub trait HasScenes: AsRef<Vec<Scene>> + AsMut<Vec<Scene>> {
|
||||
fn scenes (&self) -> &Vec<Scene> { self.as_ref() }
|
||||
fn scenes_mut (&mut self) -> &mut Vec<Scene> { self.as_mut() }
|
||||
/// Generate the default name for a new scene
|
||||
fn scene_default_name (&self) -> Arc<str> { format!("s{:3>}", self.scenes().len() + 1).into() }
|
||||
fn scene_longest_name (&self) -> usize { self.scenes().iter().map(|s|s.name.len()).fold(0, usize::max) }
|
||||
/// Add multiple scenes
|
||||
fn scenes_add (&mut self, n: usize) -> Usually<()> where Self: HasTracks {
|
||||
let scene_color_1 = ItemColor::random();
|
||||
let scene_color_2 = ItemColor::random();
|
||||
for i in 0..n {
|
||||
let _ = self.scene_add(None, Some(
|
||||
scene_color_1.mix(scene_color_2, i as f32 / n as f32).into()
|
||||
))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// Add a scene
|
||||
fn scene_add (&mut self, name: Option<&str>, color: Option<ItemTheme>)
|
||||
-> Usually<(usize, &mut Scene)> where Self: HasTracks
|
||||
{
|
||||
let scene = Scene {
|
||||
name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()),
|
||||
clips: vec![None;self.tracks().len()],
|
||||
color: color.unwrap_or_else(ItemTheme::random),
|
||||
};
|
||||
self.scenes_mut().push(scene);
|
||||
let index = self.scenes().len() - 1;
|
||||
Ok((index, &mut self.scenes_mut()[index]))
|
||||
}
|
||||
}
|
||||
pub trait HasTracks: AsRef<Vec<Track>> + AsMut<Vec<Track>> {
|
||||
fn tracks (&self) -> &Vec<Track> { self.as_ref() }
|
||||
fn tracks_mut (&mut self) -> &mut Vec<Track> { self.as_mut() }
|
||||
/// Run audio callbacks for every track and every device
|
||||
fn process_tracks (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
||||
for track in self.tracks_mut().iter_mut() {
|
||||
if Control::Quit == Audio::process(&mut track.sequencer, client, scope) {
|
||||
return Control::Quit
|
||||
}
|
||||
for device in track.devices.iter_mut() {
|
||||
if Control::Quit == DeviceAudio(device).process(client, scope) {
|
||||
return Control::Quit
|
||||
}
|
||||
}
|
||||
}
|
||||
Control::Continue
|
||||
}
|
||||
fn track_longest_name (&self) -> usize { self.tracks().iter().map(|s|s.name.len()).fold(0, usize::max) }
|
||||
/// Stop all playing clips
|
||||
fn tracks_stop_all (&mut self) { for track in self.tracks_mut().iter_mut() { track.sequencer.enqueue_next(None); } }
|
||||
/// Stop all playing clips
|
||||
fn tracks_launch (&mut self, clips: Option<Vec<Option<Arc<RwLock<MidiClip>>>>>) {
|
||||
if let Some(clips) = clips {
|
||||
for (clip, track) in clips.iter().zip(self.tracks_mut()) { track.sequencer.enqueue_next(clip.as_ref()); }
|
||||
} else {
|
||||
for track in self.tracks_mut().iter_mut() { track.sequencer.enqueue_next(None); }
|
||||
}
|
||||
}
|
||||
/// Spacing between tracks.
|
||||
const TRACK_SPACING: usize = 0;
|
||||
}
|
||||
pub trait HasTrack: AsRefOpt<Track> + AsMutOpt<Track> {
|
||||
fn track (&self) -> Option<&Track> { self.as_ref_opt() }
|
||||
fn track_mut (&mut self) -> Option<&mut Track> { self.as_mut_opt() }
|
||||
#[cfg(feature = "port")] fn view_midi_ins_status <'a> (&'a self, theme: ItemTheme) -> impl Draw<Tui> + 'a {
|
||||
self.track().map(move|track|view_ports_status(theme, "MIDI ins: ", &track.sequencer.midi_ins))
|
||||
}
|
||||
#[cfg(feature = "port")] fn view_midi_outs_status (&self, theme: ItemTheme) -> impl Draw<Tui> + '_ {
|
||||
self.track().map(move|track|view_ports_status(theme, "MIDI outs: ", &track.sequencer.midi_outs))
|
||||
}
|
||||
#[cfg(feature = "port")] fn view_audio_ins_status (&self, theme: ItemTheme) -> impl Draw<Tui> {
|
||||
self.track().map(move|track|view_ports_status(theme, "Audio ins: ", &track.audio_ins()))
|
||||
}
|
||||
#[cfg(feature = "port")] fn view_audio_outs_status (&self, theme: ItemTheme) -> impl Draw<Tui> {
|
||||
self.track().map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs()))
|
||||
}
|
||||
}
|
||||
impl_as_ref!(Vec<Track>: |self: App| self.project.as_ref());
|
||||
impl_as_mut!(Vec<Track>: |self: App| self.project.as_mut());
|
||||
#[cfg(feature = "select")] impl_as_ref_opt!(Track: |self: App| self.project.as_ref_opt());
|
||||
#[cfg(feature = "select")] impl_as_mut_opt!(Track: |self: App| self.project.as_mut_opt());
|
||||
impl HasTrackScroll for App { fn track_scroll (&self) -> usize { self.project.track_scroll() } }
|
||||
impl HasTrackScroll for Arrangement { fn track_scroll (&self) -> usize { self.track_scroll } }
|
||||
|
||||
impl HasWidth for Track {
|
||||
const MIN_WIDTH: usize = 9;
|
||||
fn width_inc (&mut self) { self.width += 1; }
|
||||
fn width_dec (&mut self) { if self.width > Track::MIN_WIDTH { self.width -= 1; } }
|
||||
}
|
||||
|
||||
impl Track {
|
||||
/// Create a new track with only the default [Sequencer].
|
||||
pub fn new (
|
||||
name: &impl AsRef<str>,
|
||||
color: Option<ItemTheme>,
|
||||
jack: &Jack<'static>,
|
||||
clock: Option<&Clock>,
|
||||
clip: Option<&Arc<RwLock<MidiClip>>>,
|
||||
midi_from: &[Connect],
|
||||
midi_to: &[Connect],
|
||||
) -> Usually<Self> {
|
||||
Ok(Self {
|
||||
name: name.as_ref().into(),
|
||||
color: color.unwrap_or_default(),
|
||||
sequencer: Sequencer::new(format!("{}/sequencer", name.as_ref()), jack, clock, clip, midi_from, midi_to)?,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
pub fn audio_ins (&self) -> &[AudioInput] {
|
||||
self.devices.first().map(|x|x.audio_ins()).unwrap_or_default()
|
||||
}
|
||||
pub fn audio_outs (&self) -> &[AudioOutput] {
|
||||
self.devices.last().map(|x|x.audio_outs()).unwrap_or_default()
|
||||
}
|
||||
fn _todo_opt_bool_stub_ (&self) -> Option<bool> { todo!() }
|
||||
fn _todo_usize_stub_ (&self) -> usize { todo!() }
|
||||
fn _todo_arc_str_stub_ (&self) -> Arc<str> { todo!() }
|
||||
fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() }
|
||||
pub fn per <'a, T: Draw<Tui> + 'a, U: TracksSizes<'a>> (
|
||||
tracks: impl Fn() -> U + Send + Sync + 'a,
|
||||
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
|
||||
) -> impl Draw<Tui> + 'a {
|
||||
iter_east(tracks,
|
||||
move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{
|
||||
w_exact((x2 - x1) as u16, fg_bg(
|
||||
track.color.lightest.term,
|
||||
track.color.base.term,
|
||||
callback(index, track)))})
|
||||
}
|
||||
/// Create a new track connecting the [Sequencer] to a [Sampler].
|
||||
#[cfg(feature = "sampler")] pub fn new_with_sampler (
|
||||
name: &impl AsRef<str>,
|
||||
color: Option<ItemTheme>,
|
||||
jack: &Jack<'static>,
|
||||
clock: Option<&Clock>,
|
||||
clip: Option<&Arc<RwLock<MidiClip>>>,
|
||||
midi_from: &[Connect],
|
||||
midi_to: &[Connect],
|
||||
audio_from: &[&[Connect];2],
|
||||
audio_to: &[&[Connect];2],
|
||||
) -> Usually<Self> {
|
||||
let mut track = Self::new(name, color, jack, clock, clip, midi_from, midi_to)?;
|
||||
let client_name = jack.with_client(|c|c.name().to_string());
|
||||
let port_name = track.sequencer.midi_outs[0].port_name();
|
||||
let connect = [Connect::exact(format!("{client_name}:{}", port_name))];
|
||||
track.devices.push(Device::Sampler(Sampler::new(
|
||||
jack, &format!("{}/sampler", name.as_ref()), &connect, audio_from, audio_to
|
||||
)?));
|
||||
Ok(track)
|
||||
}
|
||||
#[cfg(feature = "sampler")] pub fn sampler (&self, mut nth: usize) -> Option<&Sampler> {
|
||||
for device in self.devices.iter() {
|
||||
match device {
|
||||
Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; },
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
#[cfg(feature = "sampler")] pub fn sampler_mut (&mut self, mut nth: usize) -> Option<&mut Sampler> {
|
||||
for device in self.devices.iter_mut() {
|
||||
match device {
|
||||
Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; },
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn per_track <'a, T: Draw<Tui> + 'a, U: TracksSizes<'a>> (
|
||||
tracks: impl Fn() -> U + Send + Sync + 'a,
|
||||
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
|
||||
) -> impl Draw<Tui> + 'a {
|
||||
per_track_top(tracks, move|index, track|h_full(origin_y(callback(index, track))))
|
||||
}
|
||||
|
||||
pub fn per_track_top <'a, T: Draw<Tui> + 'a, U: TracksSizes<'a>> (
|
||||
tracks: impl Fn() -> U + Send + Sync + 'a,
|
||||
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
|
||||
) -> impl Draw<Tui> + 'a {
|
||||
origin_x(bg(Reset, iter_east(tracks,
|
||||
move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{
|
||||
w_exact((x2 - x1) as u16, fg_bg(
|
||||
track.color.lightest.term,
|
||||
track.color.base.term,
|
||||
callback(index, track))) })))
|
||||
}
|
||||
#[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: App| self.project.as_ref_opt());
|
||||
#[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: App| self.project.as_mut_opt());
|
||||
#[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: Arrangement| self.selected_scene());
|
||||
#[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: Arrangement| self.selected_scene_mut());
|
||||
impl HasSceneScroll for App { fn scene_scroll (&self) -> usize { self.project.scene_scroll() } }
|
||||
impl HasSceneScroll for Arrangement { fn scene_scroll (&self) -> usize { self.scene_scroll } }
|
||||
impl ScenesView for App {
|
||||
fn w_mid (&self) -> u16 { (self.size.w() as u16).saturating_sub(self.w_side()) }
|
||||
fn w_side (&self) -> u16 { 20 }
|
||||
fn h_scenes (&self) -> u16 { (self.size.h() as u16).saturating_sub(20) }
|
||||
}
|
||||
impl Scene {
|
||||
/// Returns the pulse length of the longest clip in the scene
|
||||
pub fn pulses (&self) -> usize {
|
||||
self.clips.iter().fold(0, |a, p|{
|
||||
a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0))
|
||||
})
|
||||
}
|
||||
/// Returns true if all clips in the scene are
|
||||
/// currently playing on the given collection of tracks.
|
||||
pub fn is_playing (&self, tracks: &[Track]) -> bool {
|
||||
self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate()
|
||||
.all(|(track_index, clip)|match clip {
|
||||
Some(c) => tracks
|
||||
.get(track_index)
|
||||
.map(|track|{
|
||||
if let Some((_, Some(clip))) = track.sequencer().play_clip() {
|
||||
*clip.read().unwrap() == *c.read().unwrap()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.unwrap_or(false),
|
||||
None => true
|
||||
})
|
||||
}
|
||||
pub fn clip (&self, index: usize) -> Option<&Arc<RwLock<MidiClip>>> {
|
||||
match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None }
|
||||
}
|
||||
fn _todo_opt_bool_stub_ (&self) -> Option<bool> { todo!() }
|
||||
fn _todo_usize_stub_ (&self) -> usize { todo!() }
|
||||
fn _todo_arc_str_stub_ (&self) -> Arc<str> { todo!() }
|
||||
fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() }
|
||||
}
|
||||
|
||||
impl Arrangement {
|
||||
|
||||
/// Create a new arrangement.
|
||||
pub fn new (
|
||||
jack: &Jack<'static>,
|
||||
name: Option<Arc<str>>,
|
||||
clock: Clock,
|
||||
tracks: Vec<Track>,
|
||||
scenes: Vec<Scene>,
|
||||
midi_ins: Vec<MidiInput>,
|
||||
midi_outs: Vec<MidiOutput>,
|
||||
) -> Self {
|
||||
Self {
|
||||
clock, tracks, scenes, midi_ins, midi_outs,
|
||||
jack: jack.clone(),
|
||||
name: name.unwrap_or_default(),
|
||||
color: ItemTheme::random(),
|
||||
selection: Selection::TrackClip { track: 0, scene: 0 },
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Width of display
|
||||
pub fn w (&self) -> u16 {
|
||||
self.size.w() as u16
|
||||
}
|
||||
|
||||
/// Width allocated for sidebar.
|
||||
pub fn w_sidebar (&self, is_editing: bool) -> u16 {
|
||||
self.w() / if is_editing { 16 } else { 8 } as u16
|
||||
}
|
||||
|
||||
/// Width available to display tracks.
|
||||
pub fn w_tracks_area (&self, is_editing: bool) -> u16 {
|
||||
self.w().saturating_sub(self.w_sidebar(is_editing))
|
||||
}
|
||||
|
||||
/// Height of display
|
||||
pub fn h (&self) -> u16 {
|
||||
self.size.h() as u16
|
||||
}
|
||||
|
||||
/// Height taken by visible device slots.
|
||||
pub fn h_devices (&self) -> u16 {
|
||||
2
|
||||
//1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Add multiple tracks
|
||||
#[cfg(feature = "track")] pub fn tracks_add (
|
||||
&mut self,
|
||||
count: usize, width: Option<usize>,
|
||||
mins: &[Connect], mouts: &[Connect],
|
||||
) -> Usually<()> {
|
||||
let track_color_1 = ItemColor::random();
|
||||
let track_color_2 = ItemColor::random();
|
||||
for i in 0..count {
|
||||
let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into();
|
||||
let track = self.track_add(None, Some(color), mins, mouts)?.1;
|
||||
if let Some(width) = width {
|
||||
track.width = width;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add a track
|
||||
#[cfg(feature = "track")] pub fn track_add (
|
||||
&mut self,
|
||||
name: Option<&str>, color: Option<ItemTheme>,
|
||||
mins: &[Connect], mouts: &[Connect],
|
||||
) -> Usually<(usize, &mut Track)> {
|
||||
let name: Arc<str> = name.map_or_else(
|
||||
||format!("trk{:02}", self.track_last).into(),
|
||||
|x|x.to_string().into()
|
||||
);
|
||||
self.track_last += 1;
|
||||
let track = Track {
|
||||
width: (name.len() + 2).max(12),
|
||||
color: color.unwrap_or_else(ItemTheme::random),
|
||||
sequencer: Sequencer::new(
|
||||
&format!("{name}"),
|
||||
self.jack(),
|
||||
Some(self.clock()),
|
||||
None,
|
||||
mins,
|
||||
mouts
|
||||
)?,
|
||||
name,
|
||||
..Default::default()
|
||||
};
|
||||
self.tracks_mut().push(track);
|
||||
let len = self.tracks().len();
|
||||
let index = len - 1;
|
||||
for scene in self.scenes_mut().iter_mut() {
|
||||
while scene.clips.len() < len {
|
||||
scene.clips.push(None);
|
||||
}
|
||||
}
|
||||
Ok((index, &mut self.tracks_mut()[index]))
|
||||
}
|
||||
|
||||
#[cfg(feature = "track")] pub fn view_inputs (&self, _theme: ItemTheme) -> impl Draw<Tui> + '_ {
|
||||
south(
|
||||
h_exact(1, self.view_inputs_header()),
|
||||
thunk(|to: &mut Tui|{
|
||||
for (index, port) in self.midi_ins().iter().enumerate() {
|
||||
x_push(index as u16 * 10, h_exact(1, self.view_inputs_row(port))).draw(to);
|
||||
}
|
||||
todo!()
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "track")] fn view_inputs_header (&self) -> impl Draw<Tui> + '_ {
|
||||
east(w_exact(20, origin_w(button_3("i", "nput ", format!("{}", self.midi_ins.len()), false))),
|
||||
west(w_exact(4, button_2("I", "+", false)), thunk(move|to: &mut Tui|{
|
||||
for (_index, track, x1, _x2) in self.tracks_with_sizes() {
|
||||
#[cfg(feature = "track")]
|
||||
x_push(x1 as u16, bg(track.color.dark.term, origin_w(w_exact(track.width as u16, east!(
|
||||
either(track.sequencer.monitoring, fg(Green, "mon "), "mon "),
|
||||
either(track.sequencer.recording, fg(Red, "rec "), "rec "),
|
||||
either(track.sequencer.overdub, fg(Yellow, "dub "), "dub "),
|
||||
))))).draw(to);
|
||||
}
|
||||
todo!()
|
||||
})))
|
||||
}
|
||||
|
||||
#[cfg(feature = "track")] fn view_inputs_row (&self, port: &MidiInput) -> impl Draw<Tui> {
|
||||
east(w_exact(20, origin_w(east(" ● ", bold(true, fg(Rgb(255,255,255), port.port_name()))))),
|
||||
west(w_exact(4, ()), thunk(move|to: &mut Tui|{
|
||||
for (_index, track, _x1, _x2) in self.tracks_with_sizes() {
|
||||
#[cfg(feature = "track")]
|
||||
bg(track.color.darker.term, origin_w(w_exact(track.width as u16, east!(
|
||||
either(track.sequencer.monitoring, fg(Green, " ● "), " · "),
|
||||
either(track.sequencer.recording, fg(Red, " ● "), " · "),
|
||||
either(track.sequencer.overdub, fg(Yellow, " ● "), " · "),
|
||||
)))).draw(to);
|
||||
}
|
||||
todo!()
|
||||
})))
|
||||
}
|
||||
|
||||
#[cfg(feature = "track")] pub fn view_outputs (&self, theme: ItemTheme) -> impl Draw<Tui> {
|
||||
let mut h = 1;
|
||||
|
||||
for output in self.midi_outs().iter() {
|
||||
h += 1 + output.connections.len();
|
||||
}
|
||||
|
||||
let h = h as u16;
|
||||
|
||||
let list = south(
|
||||
h_exact(1, w_full(origin_w(button_3(
|
||||
"o", "utput", format!("{}", self.midi_outs.len()), false
|
||||
)))),
|
||||
h_exact(h - 1, wh_full(origin_nw(thunk(|to: &mut Tui|{
|
||||
for (_index, port) in self.midi_outs().iter().enumerate() {
|
||||
h_exact(1,w_full(east(
|
||||
origin_w(east(" ● ", fg(Rgb(255,255,255), bold(true, port.port_name())))),
|
||||
w_full(origin_e(format!("{}/{} ",
|
||||
port.port().get_connections().len(),
|
||||
port.connections.len())))))).draw(to);
|
||||
for (index, conn) in port.connections.iter().enumerate() {
|
||||
h_exact(1, w_full(origin_w(format!(" c{index:02}{}", conn.info()))))
|
||||
.draw(to);
|
||||
}
|
||||
}
|
||||
todo!();
|
||||
}))))
|
||||
);
|
||||
|
||||
h_exact(h, view_track_row_section(theme, list, button_2("O", "+", false),
|
||||
bg(theme.darker.term, origin_w(w_full(
|
||||
thunk(|to: &mut Tui|{
|
||||
for (index, track, _x1, _x2) in self.tracks_with_sizes() {
|
||||
w_exact(track_width(index, track),
|
||||
thunk(|to: &mut Tui|{
|
||||
h_exact(1, origin_w(east(
|
||||
either(true, fg(Green, "play "), "play "),
|
||||
either(false, fg(Yellow, "solo "), "solo "),
|
||||
))).draw(to);
|
||||
for (_index, port) in self.midi_outs().iter().enumerate() {
|
||||
h_exact(1, origin_w(east(
|
||||
either(true, fg(Green, " ● "), " · "),
|
||||
either(false, fg(Yellow, " ● "), " · "),
|
||||
))).draw(to);
|
||||
for (_index, _conn) in port.connections.iter().enumerate() {
|
||||
h_exact(1, w_full("")).draw(to);
|
||||
}
|
||||
}
|
||||
todo!()
|
||||
})
|
||||
).draw(to);
|
||||
}
|
||||
todo!()
|
||||
})
|
||||
)))
|
||||
))
|
||||
}
|
||||
#[cfg(feature = "track")] pub fn view_track_devices (&self, theme: ItemTheme) -> impl Draw<Tui> {
|
||||
let mut h = 2u16;
|
||||
for track in self.tracks().iter() {
|
||||
h = h.max(track.devices.len() as u16 * 2);
|
||||
}
|
||||
let tracks = ||self.tracks_with_sizes();
|
||||
let track = move|(_, track, _x1, _x2), index| wh_exact(
|
||||
Some(track_width(index, track)),
|
||||
Some(h + 1),
|
||||
bg(track.color.dark.term, origin_nw(iter_south(move||0..h,
|
||||
|_, _index|wh_exact(Some(track.width as u16), Some(2),
|
||||
fg_bg(
|
||||
ItemTheme::G[32].lightest.term,
|
||||
ItemTheme::G[32].dark.term,
|
||||
origin_nw(format!(" · {}", "--"))
|
||||
)
|
||||
)))));
|
||||
view_track_row_section(theme,
|
||||
button_3("d", "evice", format!("{}", self.track().map(|t|t.devices.len()).unwrap_or(0)), false),
|
||||
button_2("D", "+", false),
|
||||
iter(tracks, track)
|
||||
)
|
||||
}
|
||||
|
||||
/// Put a clip in a slot
|
||||
#[cfg(feature = "clip")] 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
|
||||
#[cfg(feature = "clip")] 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.write().unwrap();
|
||||
let old = clip.color.clone();
|
||||
clip.color = color.clone();
|
||||
panic!("{color:?} {old:?}");
|
||||
//old
|
||||
})
|
||||
}
|
||||
|
||||
/// Toggle looping for the active clip
|
||||
#[cfg(feature = "clip")] pub fn toggle_loop (&mut self) {
|
||||
if let Some(clip) = self.selected_clip() {
|
||||
clip.write().unwrap().toggle_loop()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the first sampler of the active track
|
||||
#[cfg(feature = "sampler")] pub fn sampler (&self) -> Option<&Sampler> {
|
||||
self.selected_track()?.sampler(0)
|
||||
}
|
||||
|
||||
/// Get the first sampler of the active track
|
||||
#[cfg(feature = "sampler")] pub fn sampler_mut (&mut self) -> Option<&mut Sampler> {
|
||||
self.selected_track_mut()?.sampler_mut(0)
|
||||
}
|
||||
|
||||
}
|
||||
impl ScenesView for Arrangement {
|
||||
fn h_scenes (&self) -> u16 {
|
||||
(self.size.h() as u16).saturating_sub(20)
|
||||
}
|
||||
fn w_side (&self) -> u16 {
|
||||
(self.size.w() as u16 * 2 / 10).max(20)
|
||||
}
|
||||
fn w_mid (&self) -> u16 {
|
||||
(self.size.w() as u16).saturating_sub(2 * self.w_side()).max(40)
|
||||
}
|
||||
}
|
||||
impl HasClipsSize for Arrangement {
|
||||
fn clips_size (&self) -> &Size { &self.size_inner }
|
||||
}
|
||||
|
||||
pub type SceneWith<'a, T> =
|
||||
(usize, &'a Scene, usize, usize, T);
|
||||
|
||||
def_command!(SceneCommand: |scene: Scene| {
|
||||
SetSize { size: usize } => { todo!() },
|
||||
SetZoom { size: usize } => { todo!() },
|
||||
SetName { name: Arc<str> } =>
|
||||
swap_value(&mut scene.name, name, |name|Self::SetName{name}),
|
||||
SetColor { color: ItemTheme } =>
|
||||
swap_value(&mut scene.color, color, |color|Self::SetColor{color}),
|
||||
});
|
||||
|
||||
def_command!(TrackCommand: |track: Track| {
|
||||
Stop => { track.sequencer.enqueue_next(None); Ok(None) },
|
||||
SetMute { mute: Option<bool> } => todo!(),
|
||||
SetSolo { solo: Option<bool> } => todo!(),
|
||||
SetSize { size: usize } => todo!(),
|
||||
SetZoom { zoom: usize } => todo!(),
|
||||
SetName { name: Arc<str> } =>
|
||||
swap_value(&mut track.name, name, |name|Self::SetName { name }),
|
||||
SetColor { color: ItemTheme } =>
|
||||
swap_value(&mut track.color, color, |color|Self::SetColor { color }),
|
||||
SetRec { rec: Option<bool> } =>
|
||||
toggle_bool(&mut track.sequencer.recording, rec, |rec|Self::SetRec { rec }),
|
||||
SetMon { mon: Option<bool> } =>
|
||||
toggle_bool(&mut track.sequencer.monitoring, mon, |mon|Self::SetMon { mon }),
|
||||
});
|
||||
|
||||
def_command!(ClipCommand: |clip: MidiClip| {
|
||||
SetColor { color: Option<ItemTheme> } => {
|
||||
//(SetColor [t: usize, s: usize, c: ItemTheme]
|
||||
//clip.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o)))));
|
||||
//("color" [a: usize, b: usize] Some(Self::SetColor(a.unwrap(), b.unwrap(), ItemTheme::random())))
|
||||
todo!()
|
||||
},
|
||||
SetLoop { looping: Option<bool> } => {
|
||||
//(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}"))
|
||||
//("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap())))
|
||||
todo!()
|
||||
}
|
||||
});
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
use crate::{*, bind::*, mode::*, view::*};
|
||||
|
||||
/// Configuration: mode, view, and bind definitions.
|
||||
///
|
||||
/// ```
|
||||
/// let config = tek::Config::default();
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
/// // Some dizzle.
|
||||
/// // What indentation to use here lol?
|
||||
/// let source = stringify!((mode :menu (name Menu)
|
||||
/// (info Mode selector.) (keys :axis/y :confirm)
|
||||
/// (view (bg (g 0) (bsp/s :ports/out
|
||||
/// (bsp/n :ports/in
|
||||
/// (bg (g 30) (bsp/s (fixed/y 7 :logo)
|
||||
/// (fill :dialog/menu)))))))));
|
||||
/// // Add this definition to the config and try to load it.
|
||||
/// // A "mode" is basically a state machine
|
||||
/// // with associated input and output definitions.
|
||||
/// tek::Config::default().add(&source).unwrap().get_mode(":menu").unwrap();
|
||||
/// ```
|
||||
#[derive(Default, Debug)] pub struct Config {
|
||||
/// XDG base directories of running user.
|
||||
pub dirs: BaseDirectories,
|
||||
/// Active collection of interaction modes.
|
||||
pub modes: Modes,
|
||||
/// Active collection of event bindings.
|
||||
pub binds: Binds,
|
||||
/// Active collection of view definitions.
|
||||
pub views: Views,
|
||||
}
|
||||
impl Config {
|
||||
const CONFIG_DIR: &'static str = "tek";
|
||||
const CONFIG_SUB: &'static str = "v0";
|
||||
const CONFIG: &'static str = "tek.edn";
|
||||
const DEFAULTS: &'static str = include_str!("./tek.edn");
|
||||
/// Create a new app configuration from a set of XDG base directories,
|
||||
pub fn new (dirs: Option<BaseDirectories>) -> Self {
|
||||
let default = ||BaseDirectories::with_profile(Self::CONFIG_DIR, Self::CONFIG_SUB);
|
||||
let dirs = dirs.unwrap_or_else(default);
|
||||
Self { dirs, ..Default::default() }
|
||||
}
|
||||
/// Write initial contents of configuration.
|
||||
pub fn init (&mut self) -> Usually<()> {
|
||||
self.init_one(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|{
|
||||
cfgs.add(&dsl)?;
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
/// Write initial contents of a configuration file.
|
||||
pub fn init_one (
|
||||
&mut self, path: &str, defaults: &str, mut each: impl FnMut(&mut Self, &str)->Usually<()>
|
||||
) -> Usually<()> {
|
||||
if self.dirs.find_config_file(path).is_none() {
|
||||
//println!("Creating {path:?}");
|
||||
std::fs::write(self.dirs.place_config_file(path)?, defaults)?;
|
||||
}
|
||||
Ok(if let Some(path) = self.dirs.find_config_file(path) {
|
||||
//println!("Loading {path:?}");
|
||||
let src = std::fs::read_to_string(&path)?;
|
||||
src.as_str().each(move|item|each(self, item))?;
|
||||
} else {
|
||||
return Err(format!("{path}: not found").into())
|
||||
})
|
||||
}
|
||||
/// Add statements to configuration from [Dsl] source.
|
||||
pub fn add (&mut self, dsl: impl Language) -> Usually<&mut Self> {
|
||||
dsl.each(|item|self.add_one(item))?;
|
||||
Ok(self)
|
||||
}
|
||||
fn add_one (&self, item: impl Language) -> Usually<()> {
|
||||
if let Some(expr) = item.expr()? {
|
||||
let head = expr.head()?;
|
||||
let tail = expr.tail()?;
|
||||
let name = tail.head()?;
|
||||
let body = tail.tail()?;
|
||||
//println!("Config::load: {} {} {}", head.unwrap_or_default(), name.unwrap_or_default(), body.unwrap_or_default());
|
||||
match head {
|
||||
Some("mode") if let Some(name) = name => load_mode(&self.modes, &name, &body)?,
|
||||
Some("keys") if let Some(name) = name => load_bind(&self.binds, &name, &body)?,
|
||||
Some("view") if let Some(name) = name => load_view(&self.views, &name, &body)?,
|
||||
_ => return Err(format!("Config::load: expected view/keys/mode, got: {item:?}").into())
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
return Err(format!("Config::load: expected expr, got: {item:?}").into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
607
src/device.rs
607
src/device.rs
|
|
@ -1,30 +1,7 @@
|
|||
use crate::*;
|
||||
use ConnectName::*;
|
||||
use ConnectScope::*;
|
||||
use ConnectStatus::*;
|
||||
|
||||
def_command!(DeviceCommand: |device: Device| {});
|
||||
|
||||
def_command!(MidiInputCommand: |port: MidiInput| {
|
||||
Close => todo!(),
|
||||
Connect { midi_out: Arc<str> } => todo!(),
|
||||
});
|
||||
|
||||
def_command!(MidiOutputCommand: |port: MidiOutput| {
|
||||
Close => todo!(),
|
||||
Connect { midi_in: Arc<str> } => todo!(),
|
||||
});
|
||||
|
||||
def_command!(AudioInputCommand: |port: AudioInput| {
|
||||
Close => todo!(),
|
||||
Connect { audio_out: Arc<str> } => todo!(),
|
||||
});
|
||||
|
||||
def_command!(AudioOutputCommand: |port: AudioOutput| {
|
||||
Close => todo!(),
|
||||
Connect { audio_in: Arc<str> } => todo!(),
|
||||
});
|
||||
|
||||
impl Device {
|
||||
pub fn name (&self) -> &str {
|
||||
match self {
|
||||
|
|
@ -84,528 +61,6 @@ impl Device {
|
|||
/// Some sort of wrapper?
|
||||
pub struct DeviceAudio<'a>(pub &'a mut Device);
|
||||
|
||||
/// Audio input port.
|
||||
#[derive(Debug)] pub struct AudioInput {
|
||||
/// Handle to JACK client, for receiving reconnect events.
|
||||
pub jack: Jack<'static>,
|
||||
/// Port name
|
||||
pub name: Arc<str>,
|
||||
/// Port handle.
|
||||
pub port: Port<AudioIn>,
|
||||
/// List of ports to connect to.
|
||||
pub connections: Vec<Connect>,
|
||||
}
|
||||
|
||||
/// Audio output port.
|
||||
#[derive(Debug)] pub struct AudioOutput {
|
||||
/// Handle to JACK client, for receiving reconnect events.
|
||||
pub jack: Jack<'static>,
|
||||
/// Port name
|
||||
pub name: Arc<str>,
|
||||
/// Port handle.
|
||||
pub port: Port<AudioOut>,
|
||||
/// List of ports to connect to.
|
||||
pub connections: Vec<Connect>,
|
||||
}
|
||||
|
||||
/// MIDI input port.
|
||||
#[derive(Debug)] pub struct MidiInput {
|
||||
/// Handle to JACK client, for receiving reconnect events.
|
||||
pub jack: Jack<'static>,
|
||||
/// Port name
|
||||
pub name: Arc<str>,
|
||||
/// Port handle.
|
||||
pub port: Port<MidiIn>,
|
||||
/// List of currently held notes.
|
||||
pub held: Arc<RwLock<[bool;128]>>,
|
||||
/// List of ports to connect to.
|
||||
pub connections: Vec<Connect>,
|
||||
}
|
||||
|
||||
/// MIDI output port.
|
||||
#[derive(Debug)] pub struct MidiOutput {
|
||||
/// Handle to JACK client, for receiving reconnect events.
|
||||
pub jack: Jack<'static>,
|
||||
/// Port name
|
||||
pub name: Arc<str>,
|
||||
/// Port handle.
|
||||
pub port: Port<MidiOut>,
|
||||
/// List of ports to connect to.
|
||||
pub connections: Vec<Connect>,
|
||||
/// List of currently held notes.
|
||||
pub held: Arc<RwLock<[bool;128]>>,
|
||||
/// Buffer
|
||||
pub note_buffer: Vec<u8>,
|
||||
/// Buffer
|
||||
pub output_buffer: Vec<Vec<Vec<u8>>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)] pub enum ConnectName {
|
||||
/** Exact match */
|
||||
Exact(Arc<str>),
|
||||
/** Match regular expression */
|
||||
RegExp(Arc<str>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectScope {
|
||||
One,
|
||||
All
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectStatus {
|
||||
Missing,
|
||||
Disconnected,
|
||||
Connected,
|
||||
Mismatch,
|
||||
}
|
||||
|
||||
/// Port connection manager.
|
||||
///
|
||||
/// ```
|
||||
/// let connect = tek::Connect::default();
|
||||
/// ```
|
||||
#[derive(Clone, Debug, Default)] pub struct Connect {
|
||||
pub name: Option<ConnectName>,
|
||||
pub scope: Option<ConnectScope>,
|
||||
pub status: Arc<RwLock<Vec<(Port<Unowned>, Arc<str>, ConnectStatus)>>>,
|
||||
pub info: Arc<str>,
|
||||
}
|
||||
|
||||
impl Connect {
|
||||
pub fn collect (exact: &[impl AsRef<str>], re: &[impl AsRef<str>], re_all: &[impl AsRef<str>])
|
||||
-> Vec<Self>
|
||||
{
|
||||
let mut connections = vec![];
|
||||
for port in exact.iter() { connections.push(Self::exact(port)) }
|
||||
for port in re.iter() { connections.push(Self::regexp(port)) }
|
||||
for port in re_all.iter() { connections.push(Self::regexp_all(port)) }
|
||||
connections
|
||||
}
|
||||
/// Connect to this exact port
|
||||
pub fn exact (name: impl AsRef<str>) -> Self {
|
||||
let info = format!("=:{}", name.as_ref()).into();
|
||||
let name = Some(Exact(name.as_ref().into()));
|
||||
Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info }
|
||||
}
|
||||
pub fn regexp (name: impl AsRef<str>) -> Self {
|
||||
let info = format!("~:{}", name.as_ref()).into();
|
||||
let name = Some(RegExp(name.as_ref().into()));
|
||||
Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info }
|
||||
}
|
||||
pub fn regexp_all (name: impl AsRef<str>) -> Self {
|
||||
let info = format!("+:{}", name.as_ref()).into();
|
||||
let name = Some(RegExp(name.as_ref().into()));
|
||||
Self { name, scope: Some(All), status: Arc::new(RwLock::new(vec![])), info }
|
||||
}
|
||||
pub fn info (&self) -> Arc<str> {
|
||||
format!(" ({}) {} {}", {
|
||||
let status = self.status.read().unwrap();
|
||||
let mut ok = 0;
|
||||
for (_, _, state) in status.iter() {
|
||||
if *state == Connected {
|
||||
ok += 1
|
||||
}
|
||||
}
|
||||
format!("{ok}/{}", status.len())
|
||||
}, match self.scope {
|
||||
None => "x",
|
||||
Some(One) => " ",
|
||||
Some(All) => "*",
|
||||
}, match &self.name {
|
||||
None => format!("x"),
|
||||
Some(Exact(name)) => format!("= {name}"),
|
||||
Some(RegExp(name)) => format!("~ {name}"),
|
||||
}).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl HasJack<'static> for MidiInput { fn jack (&self) -> &Jack<'static> { &self.jack } }
|
||||
impl HasJack<'static> for MidiOutput { fn jack (&self) -> &Jack<'static> { &self.jack } }
|
||||
impl HasJack<'static> for AudioInput { fn jack (&self) -> &Jack<'static> { &self.jack } }
|
||||
impl HasJack<'static> for AudioOutput { fn jack (&self) -> &Jack<'static> { &self.jack } }
|
||||
impl<J: HasJack<'static>> RegisterPorts for J {
|
||||
fn midi_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiInput> {
|
||||
MidiInput::new(self.jack(), name, connect)
|
||||
}
|
||||
fn midi_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiOutput> {
|
||||
MidiOutput::new(self.jack(), name, connect)
|
||||
}
|
||||
fn audio_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioInput> {
|
||||
AudioInput::new(self.jack(), name, connect)
|
||||
}
|
||||
fn audio_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioOutput> {
|
||||
AudioOutput::new(self.jack(), name, connect)
|
||||
}
|
||||
}
|
||||
|
||||
/// May create new MIDI input ports.
|
||||
pub trait AddMidiIn {
|
||||
fn midi_in_add (&mut self) -> Usually<()>;
|
||||
}
|
||||
|
||||
/// May create new MIDI output ports.
|
||||
pub trait AddMidiOut {
|
||||
fn midi_out_add (&mut self) -> Usually<()>;
|
||||
}
|
||||
|
||||
pub trait RegisterPorts: HasJack<'static> {
|
||||
/// Register a MIDI input port.
|
||||
fn midi_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiInput>;
|
||||
/// Register a MIDI output port.
|
||||
fn midi_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiOutput>;
|
||||
/// Register an audio input port.
|
||||
fn audio_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioInput>;
|
||||
/// Register an audio output port.
|
||||
fn audio_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioOutput>;
|
||||
}
|
||||
|
||||
pub trait JackPort: HasJack<'static> {
|
||||
|
||||
type Port: PortSpec + Default;
|
||||
|
||||
type Pair: PortSpec + Default;
|
||||
|
||||
fn new (jack: &Jack<'static>, name: &impl AsRef<str>, connect: &[Connect])
|
||||
-> Usually<Self> where Self: Sized;
|
||||
|
||||
fn register (jack: &Jack<'static>, name: &impl AsRef<str>) -> Usually<Port<Self::Port>> {
|
||||
jack.with_client(|c|c.register_port::<Self::Port>(name.as_ref(), Default::default()))
|
||||
.map_err(|e|e.into())
|
||||
}
|
||||
|
||||
fn port_name (&self) -> &Arc<str>;
|
||||
|
||||
fn connections (&self) -> &[Connect];
|
||||
|
||||
fn port (&self) -> &Port<Self::Port>;
|
||||
|
||||
fn port_mut (&mut self) -> &mut Port<Self::Port>;
|
||||
|
||||
fn into_port (self) -> Port<Self::Port> where Self: Sized;
|
||||
|
||||
fn close (self) -> Usually<()> where Self: Sized {
|
||||
let jack = self.jack().clone();
|
||||
Ok(jack.with_client(|c|c.unregister_port(self.into_port()))?)
|
||||
}
|
||||
|
||||
fn ports (&self, re_name: Option<&str>, re_type: Option<&str>, flags: PortFlags) -> Vec<String> {
|
||||
self.with_client(|c|c.ports(re_name, re_type, flags))
|
||||
}
|
||||
|
||||
fn port_by_id (&self, id: u32) -> Option<Port<Unowned>> {
|
||||
self.with_client(|c|c.port_by_id(id))
|
||||
}
|
||||
|
||||
fn port_by_name (&self, name: impl AsRef<str>) -> Option<Port<Unowned>> {
|
||||
self.with_client(|c|c.port_by_name(name.as_ref()))
|
||||
}
|
||||
|
||||
fn connect_to_matching <'k> (&'k self) -> Usually<()> {
|
||||
for connect in self.connections().iter() {
|
||||
match &connect.name {
|
||||
Some(Exact(name)) => {
|
||||
*connect.status.write().unwrap() = self.connect_exact(name)?;
|
||||
},
|
||||
Some(RegExp(re)) => {
|
||||
*connect.status.write().unwrap() = self.connect_regexp(re, connect.scope)?;
|
||||
},
|
||||
_ => {},
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn connect_exact <'k> (&'k self, name: &str) ->
|
||||
Usually<Vec<(Port<Unowned>, Arc<str>, ConnectStatus)>>
|
||||
{
|
||||
self.with_client(move|c|{
|
||||
let mut status = vec![];
|
||||
for port in c.ports(None, None, PortFlags::empty()).iter() {
|
||||
if port.as_str() == &*name {
|
||||
if let Some(port) = c.port_by_name(port.as_str()) {
|
||||
let port_status = self.connect_to_unowned(&port)?;
|
||||
let name = port.name()?.into();
|
||||
status.push((port, name, port_status));
|
||||
if port_status == Connected {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(status)
|
||||
})
|
||||
}
|
||||
|
||||
fn connect_regexp <'k> (
|
||||
&'k self, re: &str, scope: Option<ConnectScope>
|
||||
) -> Usually<Vec<(Port<Unowned>, Arc<str>, ConnectStatus)>> {
|
||||
self.with_client(move|c|{
|
||||
let mut status = vec![];
|
||||
let ports = c.ports(Some(&re), None, PortFlags::empty());
|
||||
for port in ports.iter() {
|
||||
if let Some(port) = c.port_by_name(port.as_str()) {
|
||||
let port_status = self.connect_to_unowned(&port)?;
|
||||
let name = port.name()?.into();
|
||||
status.push((port, name, port_status));
|
||||
if port_status == Connected && scope == Some(One) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(status)
|
||||
})
|
||||
}
|
||||
|
||||
/** Connect to a matching port by name. */
|
||||
fn connect_to_name (&self, name: impl AsRef<str>) -> Usually<ConnectStatus> {
|
||||
self.with_client(|c|if let Some(ref port) = c.port_by_name(name.as_ref()) {
|
||||
self.connect_to_unowned(port)
|
||||
} else {
|
||||
Ok(Missing)
|
||||
})
|
||||
}
|
||||
|
||||
/** Connect to a matching port by reference. */
|
||||
fn connect_to_unowned (&self, port: &Port<Unowned>) -> Usually<ConnectStatus> {
|
||||
self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) {
|
||||
Connected
|
||||
} else if let Ok(_) = c.connect_ports(port, self.port()) {
|
||||
Connected
|
||||
} else {
|
||||
Mismatch
|
||||
}))
|
||||
}
|
||||
|
||||
/** Connect to an owned matching port by reference. */
|
||||
fn connect_to_owned (&self, port: &Port<Self::Pair>) -> Usually<ConnectStatus> {
|
||||
self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) {
|
||||
Connected
|
||||
} else if let Ok(_) = c.connect_ports(port, self.port()) {
|
||||
Connected
|
||||
} else {
|
||||
Mismatch
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl JackPort for MidiInput {
|
||||
type Port = MidiIn;
|
||||
type Pair = MidiOut;
|
||||
fn port_name (&self) -> &Arc<str> {
|
||||
&self.name
|
||||
}
|
||||
fn port (&self) -> &Port<Self::Port> {
|
||||
&self.port
|
||||
}
|
||||
fn port_mut (&mut self) -> &mut Port<Self::Port> {
|
||||
&mut self.port
|
||||
}
|
||||
fn into_port (self) -> Port<Self::Port> {
|
||||
self.port
|
||||
}
|
||||
fn connections (&self) -> &[Connect] {
|
||||
self.connections.as_slice()
|
||||
}
|
||||
fn new (jack: &Jack<'static>, name: &impl AsRef<str>, connect: &[Connect])
|
||||
-> Usually<Self> where Self: Sized
|
||||
{
|
||||
let port = Self {
|
||||
port: Self::register(jack, name)?,
|
||||
jack: jack.clone(),
|
||||
name: name.as_ref().into(),
|
||||
connections: connect.to_vec(),
|
||||
held: Arc::new(RwLock::new([false;128]))
|
||||
};
|
||||
port.connect_to_matching()?;
|
||||
Ok(port)
|
||||
}
|
||||
}
|
||||
|
||||
impl JackPort for MidiOutput {
|
||||
type Port = MidiOut;
|
||||
type Pair = MidiIn;
|
||||
fn port_name (&self) -> &Arc<str> {
|
||||
&self.name
|
||||
}
|
||||
fn port (&self) -> &Port<Self::Port> {
|
||||
&self.port
|
||||
}
|
||||
fn port_mut (&mut self) -> &mut Port<Self::Port> {
|
||||
&mut self.port
|
||||
}
|
||||
fn into_port (self) -> Port<Self::Port> {
|
||||
self.port
|
||||
}
|
||||
fn connections (&self) -> &[Connect] {
|
||||
self.connections.as_slice()
|
||||
}
|
||||
fn new (jack: &Jack<'static>, name: &impl AsRef<str>, connect: &[Connect])
|
||||
-> Usually<Self> where Self: Sized
|
||||
{
|
||||
let port = Self::register(jack, name)?;
|
||||
let jack = jack.clone();
|
||||
let name = name.as_ref().into();
|
||||
let connections = connect.to_vec();
|
||||
let port = Self {
|
||||
jack,
|
||||
port,
|
||||
name,
|
||||
connections,
|
||||
held: Arc::new([false;128].into()),
|
||||
note_buffer: vec![0;8],
|
||||
output_buffer: vec![vec![];65536],
|
||||
};
|
||||
port.connect_to_matching()?;
|
||||
Ok(port)
|
||||
}
|
||||
}
|
||||
|
||||
impl View<Tui> for MidiInput {
|
||||
fn view (&self) -> impl Draw<Tui> {
|
||||
"TODO: MIDI IN"
|
||||
}
|
||||
}
|
||||
|
||||
impl View<Tui> for MidiOutput {
|
||||
fn view (&self) -> impl Draw<Tui> {
|
||||
"TODO: MIDI OUT"
|
||||
}
|
||||
}
|
||||
|
||||
impl MidiOutput {
|
||||
/// Clear the section of the output buffer that we will be using,
|
||||
/// emitting "all notes off" at start of buffer if requested.
|
||||
pub fn buffer_clear (&mut self, scope: &ProcessScope, reset: bool) {
|
||||
let n_frames = (scope.n_frames() as usize).min(self.output_buffer.len());
|
||||
for frame in &mut self.output_buffer[0..n_frames] {
|
||||
frame.clear();
|
||||
}
|
||||
if reset {
|
||||
all_notes_off(&mut self.output_buffer);
|
||||
}
|
||||
}
|
||||
/// Write a note to the output buffer
|
||||
pub fn buffer_write <'a> (
|
||||
&'a mut self,
|
||||
sample: usize,
|
||||
event: LiveEvent,
|
||||
) {
|
||||
self.note_buffer.fill(0);
|
||||
event.write(&mut self.note_buffer).expect("failed to serialize MIDI event");
|
||||
self.output_buffer[sample].push(self.note_buffer.clone());
|
||||
// Update the list of currently held notes.
|
||||
if let LiveEvent::Midi { ref message, .. } = event {
|
||||
update_keys(&mut*self.held.write().unwrap(), message);
|
||||
}
|
||||
}
|
||||
/// Write a chunk of MIDI data from the output buffer to the output port.
|
||||
pub fn buffer_emit (&mut self, scope: &ProcessScope) {
|
||||
let samples = scope.n_frames() as usize;
|
||||
let mut writer = self.port.writer(scope);
|
||||
for (time, events) in self.output_buffer.iter().enumerate().take(samples) {
|
||||
for bytes in events.iter() {
|
||||
writer.write(&RawMidi { time: time as u32, bytes }).unwrap_or_else(|_|{
|
||||
panic!("Failed to write MIDI data: {bytes:?}");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl MidiInput {
|
||||
pub fn parsed <'a> (&'a self, scope: &'a ProcessScope) -> impl Iterator<Item=(usize, LiveEvent<'a>, &'a [u8])> {
|
||||
parse_midi_input(self.port().iter(scope))
|
||||
}
|
||||
}
|
||||
impl<T: AsRef<Vec<MidiInput>> + AsMut<Vec<MidiInput>>> HasMidiIns for T {
|
||||
fn midi_ins (&self) -> &Vec<MidiInput> { self.as_ref() }
|
||||
fn midi_ins_mut (&mut self) -> &mut Vec<MidiInput> { self.as_mut() }
|
||||
}
|
||||
impl<T: AsRef<Vec<MidiOutput>> + AsMut<Vec<MidiOutput>>> HasMidiOuts for T {
|
||||
fn midi_outs (&self) -> &Vec<MidiOutput> { self.as_ref() }
|
||||
fn midi_outs_mut (&mut self) -> &mut Vec<MidiOutput> { self.as_mut() }
|
||||
}
|
||||
impl<T: HasMidiIns + HasJack<'static>> AddMidiIn for T {
|
||||
fn midi_in_add (&mut self) -> Usually<()> {
|
||||
let index = self.midi_ins().len();
|
||||
let port = MidiInput::new(self.jack(), &format!("M/{index}"), &[])?;
|
||||
self.midi_ins_mut().push(port);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
/// Trail for thing that may gain new MIDI ports.
|
||||
impl<T: HasMidiOuts + HasJack<'static>> AddMidiOut for T {
|
||||
fn midi_out_add (&mut self) -> Usually<()> {
|
||||
let index = self.midi_outs().len();
|
||||
let port = MidiOutput::new(self.jack(), &format!("{index}/M"), &[])?;
|
||||
self.midi_outs_mut().push(port);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl JackPort for AudioInput {
|
||||
type Port = AudioIn;
|
||||
type Pair = AudioOut;
|
||||
fn port_name (&self) -> &Arc<str> {
|
||||
&self.name
|
||||
}
|
||||
fn port (&self) -> &Port<Self::Port> {
|
||||
&self.port
|
||||
}
|
||||
fn port_mut (&mut self) -> &mut Port<Self::Port> {
|
||||
&mut self.port
|
||||
}
|
||||
fn into_port (self) -> Port<Self::Port> {
|
||||
self.port
|
||||
}
|
||||
fn connections (&self) -> &[Connect] {
|
||||
self.connections.as_slice()
|
||||
}
|
||||
fn new (jack: &Jack<'static>, name: &impl AsRef<str>, connect: &[Connect])
|
||||
-> Usually<Self> where Self: Sized
|
||||
{
|
||||
let port = Self {
|
||||
port: Self::register(jack, name)?,
|
||||
jack: jack.clone(),
|
||||
name: name.as_ref().into(),
|
||||
connections: connect.to_vec()
|
||||
};
|
||||
port.connect_to_matching()?;
|
||||
Ok(port)
|
||||
}
|
||||
}
|
||||
|
||||
impl JackPort for AudioOutput {
|
||||
type Port = AudioOut;
|
||||
type Pair = AudioIn;
|
||||
fn port_name (&self) -> &Arc<str> {
|
||||
&self.name
|
||||
}
|
||||
fn port (&self) -> &Port<Self::Port> {
|
||||
&self.port
|
||||
}
|
||||
fn port_mut (&mut self) -> &mut Port<Self::Port> {
|
||||
&mut self.port
|
||||
}
|
||||
fn into_port (self) -> Port<Self::Port> {
|
||||
self.port
|
||||
}
|
||||
fn connections (&self) -> &[Connect] {
|
||||
self.connections.as_slice()
|
||||
}
|
||||
fn new (jack: &Jack<'static>, name: &impl AsRef<str>, connect: &[Connect])
|
||||
-> Usually<Self> where Self: Sized
|
||||
{
|
||||
let port = Self {
|
||||
port: Self::register(jack, name)?,
|
||||
jack: jack.clone(),
|
||||
name: name.as_ref().into(),
|
||||
connections: connect.to_vec()
|
||||
};
|
||||
port.connect_to_matching()?;
|
||||
Ok(port)
|
||||
}
|
||||
}
|
||||
|
||||
impl_audio!(|self: DeviceAudio<'a>, client, scope|{
|
||||
use Device::*;
|
||||
match self.0 {
|
||||
|
|
@ -635,48 +90,26 @@ impl<T: AsRef<Vec<Device>> + AsMut<Vec<Device>>> HasDevices for T {
|
|||
self.as_mut()
|
||||
}
|
||||
}
|
||||
/// Trait for thing that may receive MIDI.
|
||||
pub trait HasMidiIns {
|
||||
fn midi_ins (&self) -> &Vec<MidiInput>;
|
||||
fn midi_ins_mut (&mut self) -> &mut Vec<MidiInput>;
|
||||
/// Collect MIDI input from app ports (TODO preallocate large buffers)
|
||||
fn midi_input_collect <'a> (&'a self, scope: &'a ProcessScope) -> CollectedMidiInput<'a> {
|
||||
self.midi_ins().iter()
|
||||
.map(|port|port.port().iter(scope)
|
||||
.map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes)))
|
||||
.collect::<Vec<_>>())
|
||||
.collect::<Vec<_>>()
|
||||
|
||||
pub trait HasDevices: AsRef<Vec<Device>> + AsMut<Vec<Device>> {
|
||||
fn devices (&self) -> &Vec<Device> {
|
||||
self.as_ref()
|
||||
}
|
||||
fn midi_ins_with_sizes <'a> (&'a self) ->
|
||||
impl Iterator<Item=(usize, &'a Arc<str>, &'a [Connect], usize, usize)> + Send + Sync + 'a
|
||||
{
|
||||
let mut y = 0;
|
||||
self.midi_ins().iter().enumerate().map(move|(i, input)|{
|
||||
let height = 1 + input.connections().len();
|
||||
let data = (i, input.port_name(), input.connections(), y, y + height);
|
||||
y += height;
|
||||
data
|
||||
})
|
||||
}
|
||||
}
|
||||
/// Trait for thing that may output MIDI.
|
||||
pub trait HasMidiOuts {
|
||||
fn midi_outs (&self) -> &Vec<MidiOutput>;
|
||||
fn midi_outs_mut (&mut self) -> &mut Vec<MidiOutput>;
|
||||
fn midi_outs_with_sizes <'a> (&'a self) ->
|
||||
impl Iterator<Item=(usize, &'a Arc<str>, &'a [Connect], usize, usize)> + Send + Sync + 'a
|
||||
{
|
||||
let mut y = 0;
|
||||
self.midi_outs().iter().enumerate().map(move|(i, output)|{
|
||||
let height = 1 + output.connections().len();
|
||||
let data = (i, output.port_name(), output.connections(), y, y + height);
|
||||
y += height;
|
||||
data
|
||||
})
|
||||
}
|
||||
fn midi_outs_emit (&mut self, scope: &ProcessScope) {
|
||||
for port in self.midi_outs_mut().iter_mut() {
|
||||
port.buffer_emit(scope)
|
||||
}
|
||||
fn devices_mut (&mut self) -> &mut Vec<Device> {
|
||||
self.as_mut()
|
||||
}
|
||||
}
|
||||
|
||||
pub mod arrange; pub use self::arrange::*;
|
||||
pub mod browse; pub use self::browse::*;
|
||||
pub mod clock; pub use self::clock::*;
|
||||
pub mod dialog; pub use self::dialog::*;
|
||||
pub mod editor; pub use self::editor::*;
|
||||
pub mod menu; pub use self::menu::*;
|
||||
pub mod mix; pub use self::mix::*;
|
||||
pub mod port; pub use self::port::*;
|
||||
pub mod sample; pub use self::sample::*;
|
||||
pub mod sequence; pub use self::sequence::*;
|
||||
|
||||
#[cfg(feature = "plugin")] pub mod plugin;
|
||||
#[cfg(feature = "plugin")] pub use self::plugin::*;
|
||||
|
|
|
|||
123
src/device/arrange.rs
Normal file
123
src/device/arrange.rs
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
use crate::*;
|
||||
|
||||
/// Arranger.
|
||||
///
|
||||
/// ```
|
||||
/// let arranger = tek::Arrangement::default();
|
||||
/// ```
|
||||
#[derive(Default, Debug)] pub struct Arrangement {
|
||||
/// Project name.
|
||||
pub name: Arc<str>,
|
||||
/// Base color.
|
||||
pub color: ItemTheme,
|
||||
/// JACK client handle.
|
||||
pub jack: Jack<'static>,
|
||||
/// FIXME a render of the project arrangement, redrawn on update.
|
||||
/// TODO rename to "render_cache" or smth
|
||||
pub arranger: Arc<RwLock<Buffer>>,
|
||||
/// Display size
|
||||
pub size: Size,
|
||||
/// Display size of clips area
|
||||
pub size_inner: Size,
|
||||
/// Source of time
|
||||
#[cfg(feature = "clock")] pub clock: Clock,
|
||||
/// Allows one MIDI clip to be edited
|
||||
#[cfg(feature = "editor")] pub editor: Option<MidiEditor>,
|
||||
/// List of global midi inputs
|
||||
#[cfg(feature = "port")] pub midi_ins: Vec<MidiInput>,
|
||||
/// List of global midi outputs
|
||||
#[cfg(feature = "port")] pub midi_outs: Vec<MidiOutput>,
|
||||
/// List of global audio inputs
|
||||
#[cfg(feature = "port")] pub audio_ins: Vec<AudioInput>,
|
||||
/// List of global audio outputs
|
||||
#[cfg(feature = "port")] pub audio_outs: Vec<AudioOutput>,
|
||||
/// Selected UI element
|
||||
#[cfg(feature = "select")] pub selection: Selection,
|
||||
/// Last track number (to avoid duplicate port names)
|
||||
#[cfg(feature = "track")] pub track_last: usize,
|
||||
/// List of tracks
|
||||
#[cfg(feature = "track")] pub tracks: Vec<Track>,
|
||||
/// Scroll offset of tracks
|
||||
#[cfg(feature = "track")] pub track_scroll: usize,
|
||||
/// List of scenes
|
||||
#[cfg(feature = "scene")] pub scenes: Vec<Scene>,
|
||||
/// Scroll offset of scenes
|
||||
#[cfg(feature = "scene")] pub scene_scroll: usize,
|
||||
}
|
||||
|
||||
impl HasJack<'static> for Arrangement { fn jack (&self) -> &Jack<'static> { &self.jack } }
|
||||
impl_has!(Jack<'static>: |self: Arrangement| self.jack);
|
||||
impl_has!(Size: |self: Arrangement| self.size);
|
||||
impl_has!(Vec<MidiInput>: |self: Arrangement| self.midi_ins);
|
||||
impl_has!(Vec<MidiOutput>: |self: Arrangement| self.midi_outs);
|
||||
impl_has!(Clock: |self: Arrangement| self.clock);
|
||||
impl_has!(Selection: |self: Arrangement| self.selection);
|
||||
impl_as_ref_opt!(MidiEditor: |self: Arrangement| self.editor.as_ref());
|
||||
impl_as_mut_opt!(MidiEditor: |self: Arrangement| self.editor.as_mut());
|
||||
|
||||
impl Arrangement {
|
||||
|
||||
/// Create a new arrangement.
|
||||
pub fn new (
|
||||
jack: &Jack<'static>,
|
||||
name: Option<Arc<str>>,
|
||||
clock: Clock,
|
||||
tracks: Vec<Track>,
|
||||
scenes: Vec<Scene>,
|
||||
midi_ins: Vec<MidiInput>,
|
||||
midi_outs: Vec<MidiOutput>,
|
||||
) -> Self {
|
||||
Self {
|
||||
clock, tracks, scenes, midi_ins, midi_outs,
|
||||
jack: jack.clone(),
|
||||
name: name.unwrap_or_default(),
|
||||
color: ItemTheme::random(),
|
||||
selection: Selection::TrackClip { track: 0, scene: 0 },
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Width of display
|
||||
pub fn w (&self) -> u16 {
|
||||
self.size.w() as u16
|
||||
}
|
||||
|
||||
/// Width allocated for sidebar.
|
||||
pub fn w_sidebar (&self, is_editing: bool) -> u16 {
|
||||
self.w() / if is_editing { 16 } else { 8 } as u16
|
||||
}
|
||||
|
||||
/// Width available to display tracks.
|
||||
pub fn w_tracks_area (&self, is_editing: bool) -> u16 {
|
||||
self.w().saturating_sub(self.w_sidebar(is_editing))
|
||||
}
|
||||
|
||||
/// Height of display
|
||||
pub fn h (&self) -> u16 {
|
||||
self.size.h() as u16
|
||||
}
|
||||
|
||||
/// Height taken by visible device slots.
|
||||
pub fn h_devices (&self) -> u16 {
|
||||
2
|
||||
//1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Get the first sampler of the active track
|
||||
#[cfg(feature = "sampler")]
|
||||
pub fn sampler (&self) -> Option<&Sampler> {
|
||||
self.selected_track()?.sampler(0)
|
||||
}
|
||||
|
||||
/// Get the first sampler of the active track
|
||||
#[cfg(feature = "sampler")]
|
||||
pub fn sampler_mut (&mut self) -> Option<&mut Sampler> {
|
||||
self.selected_track_mut()?.sampler_mut(0)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[cfg(feature = "clip")] mod clip; #[cfg(feature = "clip")] pub use self::clip::*;
|
||||
#[cfg(feature = "scene")] mod scene; #[cfg(feature = "scene")] pub use self::scene::*;
|
||||
#[cfg(feature = "track")] mod track; #[cfg(feature = "track")] pub use self::track::*;
|
||||
#[cfg(feature = "select")] mod select; #[cfg(feature = "select")] pub use self::select::*;
|
||||
90
src/device/arrange/clip.rs
Normal file
90
src/device/arrange/clip.rs
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
use crate::*;
|
||||
|
||||
/// TODO: Preserve the generic passthru syntax;
|
||||
/// remove this macro (only used twice) and potentially the trait.
|
||||
#[macro_export] macro_rules! impl_has_clips {
|
||||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||||
impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? {
|
||||
fn clips <'a> (&'a $self) -> std::sync::RwLockReadGuard<'a, ClipPool> {
|
||||
$cb.read().unwrap()
|
||||
}
|
||||
fn clips_mut <'a> (&'a $self) -> std::sync::RwLockWriteGuard<'a, ClipPool> {
|
||||
$cb.write().unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! has_clip {
|
||||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||||
impl $(<$($L),*$($T $(: $U)?),*>)? HasMidiClip for $Struct $(<$($L),*$($T),*>)? {
|
||||
fn clip (&$self) -> Option<Arc<RwLock<MidiClip>>> { $cb }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ClipsView: TracksView + ScenesView {
|
||||
/// Draw clips per scene
|
||||
fn view_scenes_clips <'a> (&'a self) -> impl Draw<Tui> + 'a {
|
||||
crate::view::view_scenes_clips(
|
||||
self.tracks_with_sizes(), self.clips_size()
|
||||
)
|
||||
}
|
||||
/// Draw clips for tracks
|
||||
fn view_track_clips <'a> (&'a self, track_index: usize, track: &'a Track) -> impl Draw<Tui> + 'a {
|
||||
crate::view::view_track_clips(
|
||||
self.scenes_with_sizes(), self.selection(), self.editor(), track_index, track
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl HasClipsSize for Arrangement {
|
||||
fn clips_size (&self) -> &Size { &self.size_inner }
|
||||
}
|
||||
|
||||
def_command!(ClipCommand: |clip: MidiClip| {
|
||||
SetColor { color: Option<ItemTheme> } => {
|
||||
//(SetColor [t: usize, s: usize, c: ItemTheme]
|
||||
//clip.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o)))));
|
||||
//("color" [a: usize, b: usize] Some(Self::SetColor(a.unwrap(), b.unwrap(), ItemTheme::random())))
|
||||
todo!()
|
||||
},
|
||||
SetLoop { looping: Option<bool> } => {
|
||||
//(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}"))
|
||||
//("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap())))
|
||||
todo!()
|
||||
}
|
||||
});
|
||||
|
||||
impl Arrangement {
|
||||
/// 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.write().unwrap();
|
||||
let old = clip.color.clone();
|
||||
clip.color = color.clone();
|
||||
panic!("{color:?} {old:?}");
|
||||
//old
|
||||
})
|
||||
}
|
||||
|
||||
/// Toggle looping for the active clip
|
||||
pub fn toggle_loop (&mut self) {
|
||||
if let Some(clip) = self.selected_clip() {
|
||||
clip.write().unwrap().toggle_loop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HasClipsSize { fn clips_size (&self) -> &Size; }
|
||||
167
src/device/arrange/scene.rs
Normal file
167
src/device/arrange/scene.rs
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
use crate::*;
|
||||
|
||||
/// A scene consists of a set of clips to play together.
|
||||
///
|
||||
/// ```
|
||||
/// let scene: tek::Scene = Default::default();
|
||||
/// let _ = scene.pulses();
|
||||
/// let _ = scene.is_playing(&[]);
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct Scene {
|
||||
/// Name of scene
|
||||
pub name: Arc<str>,
|
||||
/// Identifying color of scene
|
||||
pub color: ItemTheme,
|
||||
/// Clips in scene, one per track
|
||||
pub clips: Vec<Option<Arc<RwLock<MidiClip>>>>,
|
||||
}
|
||||
|
||||
impl Scene {
|
||||
/// Returns the pulse length of the longest clip in the scene
|
||||
pub fn pulses (&self) -> usize {
|
||||
self.clips.iter().fold(0, |a, p|{
|
||||
a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0))
|
||||
})
|
||||
}
|
||||
/// Returns true if all clips in the scene are
|
||||
/// currently playing on the given collection of tracks.
|
||||
pub fn is_playing (&self, tracks: &[Track]) -> bool {
|
||||
self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate()
|
||||
.all(|(track_index, clip)|match clip {
|
||||
Some(c) => tracks
|
||||
.get(track_index)
|
||||
.map(|track|{
|
||||
if let Some((_, Some(clip))) = track.sequencer().play_clip() {
|
||||
*clip.read().unwrap() == *c.read().unwrap()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.unwrap_or(false),
|
||||
None => true
|
||||
})
|
||||
}
|
||||
pub fn clip (&self, index: usize) -> Option<&Arc<RwLock<MidiClip>>> {
|
||||
match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None }
|
||||
}
|
||||
fn _todo_opt_bool_stub_ (&self) -> Option<bool> { todo!() }
|
||||
fn _todo_usize_stub_ (&self) -> usize { todo!() }
|
||||
fn _todo_arc_str_stub_ (&self) -> Arc<str> { todo!() }
|
||||
fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() }
|
||||
}
|
||||
|
||||
pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + Send + Sync {
|
||||
/// Default scene height.
|
||||
const H_SCENE: usize = 2;
|
||||
/// Default editor height.
|
||||
const H_EDITOR: usize = 15;
|
||||
fn h_scenes (&self) -> u16;
|
||||
fn w_side (&self) -> u16;
|
||||
fn w_mid (&self) -> u16;
|
||||
|
||||
fn view_scenes_names (&self) -> impl Draw<Tui> {
|
||||
crate::view::view_scenes_names(
|
||||
self.scenes_with_sizes()
|
||||
)
|
||||
}
|
||||
fn view_scene_name <'a> (&'a self, index: usize, scene: &'a Scene) -> impl Draw<Tui> + 'a {
|
||||
crate::view::view_scene_name(
|
||||
self.selected(), self.editor(), index, scene
|
||||
)
|
||||
}
|
||||
fn scenes_with_sizes (&self) -> impl ScenesSizes<'_> {
|
||||
let mut y = 0;
|
||||
self.scenes().iter().enumerate().skip(self.scene_scroll()).map_while(move|(s, scene)|{
|
||||
let height = if self.selection().scene() == Some(s) && self.editor().is_some() {
|
||||
8
|
||||
} else {
|
||||
Self::H_SCENE
|
||||
};
|
||||
if y + height <= self.clips_size().h() as usize {
|
||||
let data = (s, scene, y, y + height);
|
||||
y += height;
|
||||
Some(data)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HasSceneScroll: HasScenes {
|
||||
fn scene_scroll (&self) -> usize;
|
||||
}
|
||||
|
||||
pub trait HasScene: AsRefOpt<Scene> + AsMutOpt<Scene> {
|
||||
fn scene_mut (&mut self) -> Option<&mut Scene> { self.as_mut_opt() }
|
||||
fn scene (&self) -> Option<&Scene> { self.as_ref_opt() }
|
||||
}
|
||||
|
||||
pub trait HasScenes: AsRef<Vec<Scene>> + AsMut<Vec<Scene>> {
|
||||
fn scenes (&self) -> &Vec<Scene> { self.as_ref() }
|
||||
fn scenes_mut (&mut self) -> &mut Vec<Scene> { self.as_mut() }
|
||||
/// Generate the default name for a new scene
|
||||
fn scene_default_name (&self) -> Arc<str> { format!("s{:3>}", self.scenes().len() + 1).into() }
|
||||
fn scene_longest_name (&self) -> usize { self.scenes().iter().map(|s|s.name.len()).fold(0, usize::max) }
|
||||
/// Add multiple scenes
|
||||
fn scenes_add (&mut self, n: usize) -> Usually<()> where Self: HasTracks {
|
||||
let scene_color_1 = ItemColor::random();
|
||||
let scene_color_2 = ItemColor::random();
|
||||
for i in 0..n {
|
||||
let _ = self.scene_add(None, Some(
|
||||
scene_color_1.mix(scene_color_2, i as f32 / n as f32).into()
|
||||
))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// Add a scene
|
||||
fn scene_add (&mut self, name: Option<&str>, color: Option<ItemTheme>)
|
||||
-> Usually<(usize, &mut Scene)> where Self: HasTracks
|
||||
{
|
||||
let scene = Scene {
|
||||
name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()),
|
||||
clips: vec![None;self.tracks().len()],
|
||||
color: color.unwrap_or_else(ItemTheme::random),
|
||||
};
|
||||
self.scenes_mut().push(scene);
|
||||
let index = self.scenes().len() - 1;
|
||||
Ok((index, &mut self.scenes_mut()[index]))
|
||||
}
|
||||
}
|
||||
|
||||
impl HasSceneScroll for Arrangement {
|
||||
fn scene_scroll (&self) -> usize { self.scene_scroll }
|
||||
}
|
||||
|
||||
impl ScenesView for Arrangement {
|
||||
fn h_scenes (&self) -> u16 {
|
||||
(self.size.h() as u16).saturating_sub(20)
|
||||
}
|
||||
fn w_side (&self) -> u16 {
|
||||
(self.size.w() as u16 * 2 / 10).max(20)
|
||||
}
|
||||
fn w_mid (&self) -> u16 {
|
||||
(self.size.w() as u16).saturating_sub(2 * self.w_side()).max(40)
|
||||
}
|
||||
}
|
||||
|
||||
pub type SceneWith<'a, T> =
|
||||
(usize, &'a Scene, usize, usize, T);
|
||||
|
||||
def_command!(SceneCommand: |scene: Scene| {
|
||||
SetSize { size: usize } => { todo!() },
|
||||
SetZoom { size: usize } => { todo!() },
|
||||
SetName { name: Arc<str> } =>
|
||||
swap_value(&mut scene.name, name, |name|Self::SetName{name}),
|
||||
SetColor { color: ItemTheme } =>
|
||||
swap_value(&mut scene.color, color, |color|Self::SetColor{color}),
|
||||
});
|
||||
|
||||
#[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: App| self.project.as_ref_opt());
|
||||
#[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: App| self.project.as_mut_opt());
|
||||
#[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: Arrangement| self.selected_scene());
|
||||
#[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: Arrangement| self.selected_scene_mut());
|
||||
impl<T: TracksView+ScenesView+Send+Sync> ClipsView for T {}
|
||||
impl_has!(Vec<Scene>: |self: Arrangement| self.scenes);
|
||||
impl<T: AsRef<Vec<Scene>>+AsMut<Vec<Scene>>> HasScenes for T {}
|
||||
impl<T: AsRefOpt<Scene>+AsMutOpt<Scene>+Send+Sync> HasScene for T {}
|
||||
339
src/device/arrange/track.rs
Normal file
339
src/device/arrange/track.rs
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
use crate::*;
|
||||
|
||||
/// A track consists of a sequencer and zero or more devices chained after it.
|
||||
///
|
||||
/// ```
|
||||
/// let track: tek::Track = Default::default();
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub struct Track {
|
||||
/// Name of track
|
||||
pub name: Arc<str>,
|
||||
/// Identifying color of track
|
||||
pub color: ItemTheme,
|
||||
/// Preferred width of track column
|
||||
pub width: usize,
|
||||
/// MIDI sequencer state
|
||||
pub sequencer: Sequencer,
|
||||
/// Device chain
|
||||
pub devices: Vec<Device>,
|
||||
}
|
||||
|
||||
impl Track {
|
||||
/// Create a new track with only the default [Sequencer].
|
||||
pub fn new (
|
||||
name: &impl AsRef<str>,
|
||||
color: Option<ItemTheme>,
|
||||
jack: &Jack<'static>,
|
||||
clock: Option<&Clock>,
|
||||
clip: Option<&Arc<RwLock<MidiClip>>>,
|
||||
midi_from: &[Connect],
|
||||
midi_to: &[Connect],
|
||||
) -> Usually<Self> {
|
||||
Ok(Self {
|
||||
name: name.as_ref().into(),
|
||||
color: color.unwrap_or_default(),
|
||||
sequencer: Sequencer::new(
|
||||
format!("{}/sequencer", name.as_ref()), jack, clock, clip, midi_from, midi_to
|
||||
)?,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
pub fn audio_ins (&self) -> &[AudioInput] {
|
||||
self.devices.first().map(|x|x.audio_ins()).unwrap_or_default()
|
||||
}
|
||||
pub fn audio_outs (&self) -> &[AudioOutput] {
|
||||
self.devices.last().map(|x|x.audio_outs()).unwrap_or_default()
|
||||
}
|
||||
fn _todo_opt_bool_stub_ (&self) -> Option<bool> { todo!() }
|
||||
fn _todo_usize_stub_ (&self) -> usize { todo!() }
|
||||
fn _todo_arc_str_stub_ (&self) -> Arc<str> { todo!() }
|
||||
fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() }
|
||||
|
||||
pub fn per <'a, T: Draw<Tui> + 'a, U: TracksSizes<'a>> (
|
||||
tracks: impl Fn() -> U + Send + Sync + 'a,
|
||||
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
|
||||
) -> impl Draw<Tui> + 'a {
|
||||
per(tracks, callback)
|
||||
}
|
||||
|
||||
/// Create a new track connecting the [Sequencer] to a [Sampler].
|
||||
#[cfg(feature = "sampler")] pub fn new_with_sampler (
|
||||
name: &impl AsRef<str>,
|
||||
color: Option<ItemTheme>,
|
||||
jack: &Jack<'static>,
|
||||
clock: Option<&Clock>,
|
||||
clip: Option<&Arc<RwLock<MidiClip>>>,
|
||||
midi_from: &[Connect],
|
||||
midi_to: &[Connect],
|
||||
audio_from: &[&[Connect];2],
|
||||
audio_to: &[&[Connect];2],
|
||||
) -> Usually<Self> {
|
||||
let mut track = Self::new(name, color, jack, clock, clip, midi_from, midi_to)?;
|
||||
let client_name = jack.with_client(|c|c.name().to_string());
|
||||
let port_name = track.sequencer.midi_outs[0].port_name();
|
||||
let connect = [Connect::exact(format!("{client_name}:{}", port_name))];
|
||||
track.devices.push(Device::Sampler(Sampler::new(
|
||||
jack, &format!("{}/sampler", name.as_ref()), &connect, audio_from, audio_to
|
||||
)?));
|
||||
Ok(track)
|
||||
}
|
||||
|
||||
#[cfg(feature = "sampler")] pub fn sampler (&self, mut nth: usize) -> Option<&Sampler> {
|
||||
for device in self.devices.iter() {
|
||||
match device {
|
||||
Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; },
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(feature = "sampler")] pub fn sampler_mut (&mut self, mut nth: usize) -> Option<&mut Sampler> {
|
||||
for device in self.devices.iter_mut() {
|
||||
match device {
|
||||
Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; },
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl HasWidth for Track {
|
||||
const MIN_WIDTH: usize = 9;
|
||||
fn width_inc (&mut self) { self.width += 1; }
|
||||
fn width_dec (&mut self) { if self.width > Track::MIN_WIDTH { self.width -= 1; } }
|
||||
}
|
||||
|
||||
pub trait HasTracks: AsRef<Vec<Track>> + AsMut<Vec<Track>> {
|
||||
fn tracks (&self) -> &Vec<Track> { self.as_ref() }
|
||||
fn tracks_mut (&mut self) -> &mut Vec<Track> { self.as_mut() }
|
||||
/// Run audio callbacks for every track and every device
|
||||
fn process_tracks (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
||||
for track in self.tracks_mut().iter_mut() {
|
||||
if Control::Quit == Audio::process(&mut track.sequencer, client, scope) {
|
||||
return Control::Quit
|
||||
}
|
||||
for device in track.devices.iter_mut() {
|
||||
if Control::Quit == DeviceAudio(device).process(client, scope) {
|
||||
return Control::Quit
|
||||
}
|
||||
}
|
||||
}
|
||||
Control::Continue
|
||||
}
|
||||
fn track_longest_name (&self) -> usize {
|
||||
self.tracks().iter().map(|s|s.name.len()).fold(0, usize::max)
|
||||
}
|
||||
/// Stop all playing clips
|
||||
fn tracks_stop_all (&mut self) {
|
||||
for track in self.tracks_mut().iter_mut() {
|
||||
track.sequencer.enqueue_next(None);
|
||||
}
|
||||
}
|
||||
/// Stop all playing clips
|
||||
fn tracks_launch (&mut self, clips: Option<Vec<Option<Arc<RwLock<MidiClip>>>>>) {
|
||||
if let Some(clips) = clips {
|
||||
for (clip, track) in clips.iter().zip(self.tracks_mut()) {
|
||||
track.sequencer.enqueue_next(clip.as_ref());
|
||||
}
|
||||
} else {
|
||||
for track in self.tracks_mut().iter_mut() {
|
||||
track.sequencer.enqueue_next(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Spacing between tracks.
|
||||
const TRACK_SPACING: usize = 0;
|
||||
}
|
||||
|
||||
pub trait HasTrack: AsRefOpt<Track> + AsMutOpt<Track> {
|
||||
fn track (&self) -> Option<&Track> { self.as_ref_opt() }
|
||||
fn track_mut (&mut self) -> Option<&mut Track> { self.as_mut_opt() }
|
||||
|
||||
#[cfg(feature = "port")]
|
||||
fn view_midi_ins_status <'a> (&'a self, theme: ItemTheme) -> impl Draw<Tui> + 'a {
|
||||
crate::view::view_midi_ins_status(theme, self.track())
|
||||
}
|
||||
|
||||
#[cfg(feature = "port")]
|
||||
fn view_midi_outs_status (&self, theme: ItemTheme) -> impl Draw<Tui> + '_ {
|
||||
crate::view::view_midi_outs_status(theme, self.track())
|
||||
}
|
||||
|
||||
#[cfg(feature = "port")]
|
||||
fn view_audio_ins_status (&self, theme: ItemTheme) -> impl Draw<Tui> {
|
||||
crate::view::view_audio_ins_status(theme, self.track())
|
||||
}
|
||||
|
||||
#[cfg(feature = "port")]
|
||||
fn view_audio_outs_status (&self, theme: ItemTheme) -> impl Draw<Tui> {
|
||||
crate::view::view_audio_outs_status(theme, self.track())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HasTrackScroll: HasTracks {
|
||||
fn track_scroll (&self) -> usize;
|
||||
}
|
||||
|
||||
pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll {
|
||||
/// Draw name of each track
|
||||
fn view_track_names (&self, theme: ItemTheme) -> impl Draw<Tui> {
|
||||
crate::view::view_track_names(
|
||||
self.tracks_with_sizes(), self.tracks().len(), self.scenes().len(), self.selection()
|
||||
)
|
||||
}
|
||||
/// Draw outputs per track
|
||||
fn view_track_outputs <'a> (&'a self, theme: ItemTheme, _h: u16) -> impl Draw<Tui> {
|
||||
crate::view::view_track_outputs(
|
||||
theme, self.tracks_with_sizes(), self.midi_outs().iter()
|
||||
)
|
||||
}
|
||||
/// Draw inputs per track
|
||||
fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Draw<Tui> {
|
||||
let mut h = 0u16;
|
||||
for track in self.tracks().iter() {
|
||||
h = h.max(track.sequencer.midi_ins.len() as u16);
|
||||
}
|
||||
crate::view::view_track_inputs(theme, self.tracks_with_sizes(), h)
|
||||
}
|
||||
/// Iterate over tracks with their corresponding sizes.
|
||||
fn tracks_with_sizes (&self) -> impl TracksSizes<'_> {
|
||||
let _editor_width = self.editor().map(|e|e.size.w());
|
||||
let _active_track = self.selection().track();
|
||||
let mut x = 0;
|
||||
let w = self.clips_size().w() as usize;
|
||||
self.tracks().iter().enumerate().map_while(move |(index, track)|{
|
||||
let width = track.width.max(8);
|
||||
if x + width < w {
|
||||
let data = (index, track, x, x + width);
|
||||
x += width + Self::TRACK_SPACING;
|
||||
Some(data)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl HasTrackScroll for Arrangement {
|
||||
fn track_scroll (&self) -> usize { self.track_scroll }
|
||||
}
|
||||
|
||||
def_command!(TrackCommand: |track: Track| {
|
||||
Stop => { track.sequencer.enqueue_next(None); Ok(None) },
|
||||
SetMute { mute: Option<bool> } => todo!(),
|
||||
SetSolo { solo: Option<bool> } => todo!(),
|
||||
SetSize { size: usize } => todo!(),
|
||||
SetZoom { zoom: usize } => todo!(),
|
||||
SetName { name: Arc<str> } =>
|
||||
swap_value(&mut track.name, name, |name|Self::SetName { name }),
|
||||
SetColor { color: ItemTheme } =>
|
||||
swap_value(&mut track.color, color, |color|Self::SetColor { color }),
|
||||
SetRec { rec: Option<bool> } =>
|
||||
toggle_bool(&mut track.sequencer.recording, rec, |rec|Self::SetRec { rec }),
|
||||
SetMon { mon: Option<bool> } =>
|
||||
toggle_bool(&mut track.sequencer.monitoring, mon, |mon|Self::SetMon { mon }),
|
||||
});
|
||||
|
||||
impl<T: AsRef<Vec<Track>>+AsMut<Vec<Track>>> HasTracks for T {}
|
||||
impl<T: AsRefOpt<Track>+AsMutOpt<Track>+Send+Sync> HasTrack for T {}
|
||||
impl<T: ScenesView+HasMidiIns+HasMidiOuts+HasTrackScroll> TracksView for T {}
|
||||
impl_as_ref!(Vec<Track>: |self: App| self.project.as_ref());
|
||||
impl_as_mut!(Vec<Track>: |self: App| self.project.as_mut());
|
||||
#[cfg(feature = "select")] impl_as_ref_opt!(Track: |self: App| self.project.as_ref_opt());
|
||||
#[cfg(feature = "select")] impl_as_mut_opt!(Track: |self: App| self.project.as_mut_opt());
|
||||
impl_has!(Vec<Track>: |self: Arrangement| self.tracks);
|
||||
impl_as_ref_opt!(Track: |self: Arrangement| self.selected_track());
|
||||
impl_as_mut_opt!(Track: |self: Arrangement| self.selected_track_mut());
|
||||
|
||||
impl Arrangement {
|
||||
#[cfg(feature = "track")]
|
||||
pub fn view_inputs (&self, _theme: ItemTheme) -> impl Draw<Tui> + '_ {
|
||||
crate::view::view_inputs()
|
||||
}
|
||||
|
||||
#[cfg(feature = "track")]
|
||||
pub fn view_inputs_header (&self) -> impl Draw<Tui> + '_ {
|
||||
crate::view::view_inputs_header(self.tracks_with_sizes())
|
||||
}
|
||||
|
||||
#[cfg(feature = "track")]
|
||||
pub fn view_inputs_row (&self, port: &MidiInput) -> impl Draw<Tui> {
|
||||
crate::view::view_inputs_row(port)
|
||||
}
|
||||
|
||||
#[cfg(feature = "track")]
|
||||
pub fn view_outputs (&self, theme: ItemTheme) -> impl Draw<Tui> {
|
||||
let mut h = 1;
|
||||
for output in self.midi_outs().iter() {
|
||||
h += 1 + output.connections.len();
|
||||
}
|
||||
let h = h as u16;
|
||||
crate::view::view_outputs(self.tracks_with_sizes())
|
||||
}
|
||||
|
||||
#[cfg(feature = "track")]
|
||||
pub fn view_track_devices (&self, theme: ItemTheme) -> impl Draw<Tui> {
|
||||
let mut h = 2u16;
|
||||
for track in self.tracks().iter() {
|
||||
h = h.max(track.devices.len() as u16 * 2);
|
||||
}
|
||||
crate::view::view_track_devices(||self.tracks_with_sizes(), self.track(), h)
|
||||
}
|
||||
|
||||
/// Add multiple tracks
|
||||
#[cfg(feature = "track")] pub fn tracks_add (
|
||||
&mut self,
|
||||
count: usize, width: Option<usize>,
|
||||
mins: &[Connect], mouts: &[Connect],
|
||||
) -> Usually<()> {
|
||||
let track_color_1 = ItemColor::random();
|
||||
let track_color_2 = ItemColor::random();
|
||||
for i in 0..count {
|
||||
let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into();
|
||||
let track = self.track_add(None, Some(color), mins, mouts)?.1;
|
||||
if let Some(width) = width {
|
||||
track.width = width;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add a track
|
||||
#[cfg(feature = "track")] pub fn track_add (
|
||||
&mut self,
|
||||
name: Option<&str>, color: Option<ItemTheme>,
|
||||
mins: &[Connect], mouts: &[Connect],
|
||||
) -> Usually<(usize, &mut Track)> {
|
||||
let name: Arc<str> = name.map_or_else(
|
||||
||format!("trk{:02}", self.track_last).into(),
|
||||
|x|x.to_string().into()
|
||||
);
|
||||
self.track_last += 1;
|
||||
let track = Track {
|
||||
width: (name.len() + 2).max(12),
|
||||
color: color.unwrap_or_else(ItemTheme::random),
|
||||
sequencer: Sequencer::new(
|
||||
&format!("{name}"),
|
||||
self.jack(),
|
||||
Some(self.clock()),
|
||||
None,
|
||||
mins,
|
||||
mouts
|
||||
)?,
|
||||
name,
|
||||
..Default::default()
|
||||
};
|
||||
self.tracks_mut().push(track);
|
||||
let len = self.tracks().len();
|
||||
let index = len - 1;
|
||||
for scene in self.scenes_mut().iter_mut() {
|
||||
while scene.clips.len() < len {
|
||||
scene.clips.push(None);
|
||||
}
|
||||
}
|
||||
Ok((index, &mut self.tracks_mut()[index]))
|
||||
}
|
||||
}
|
||||
0
src/device/audio.rs
Normal file
0
src/device/audio.rs
Normal file
|
|
@ -24,11 +24,12 @@ def_command!(FileBrowserCommand: |sampler: Sampler|{
|
|||
pub size: Size,
|
||||
}
|
||||
|
||||
pub(crate) struct EntriesIterator<'a> {
|
||||
pub(crate) struct EntriesIterator<'a, S: Screen> {
|
||||
pub browser: &'a Browse,
|
||||
pub offset: usize,
|
||||
pub length: usize,
|
||||
pub index: usize,
|
||||
_screen: std::marker::PhantomData<S>
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)] pub enum BrowseTarget {
|
||||
|
|
@ -318,12 +319,13 @@ impl Browse {
|
|||
index: 0,
|
||||
length: self.dirs.len() + self.files.len(),
|
||||
browser: self,
|
||||
_screen: Default::default(),
|
||||
};
|
||||
iter_south_fixed(1, iterate, item)
|
||||
}
|
||||
}
|
||||
impl<'a> Iterator for EntriesIterator<'a> {
|
||||
type Item = ();
|
||||
impl<'a> Iterator for EntriesIterator<'a, Tui> {
|
||||
type Item = impl Draw<Tui>;
|
||||
fn next (&mut self) -> Option<Self::Item> {
|
||||
let dirs = self.browser.dirs.len();
|
||||
let files = self.browser.files.len();
|
||||
|
|
@ -360,24 +362,36 @@ def_command!(BrowseCommand: |browse: Browse| {
|
|||
|
||||
def_command!(PoolCommand: |pool: Pool| {
|
||||
// Toggle visibility of pool
|
||||
Show { visible: bool } => { pool.visible = *visible; Ok(Some(Self::Show { visible: !visible })) },
|
||||
Show { visible: bool } => {
|
||||
pool.visible = *visible;
|
||||
Ok(Some(Self::Show { visible: !visible }))
|
||||
},
|
||||
// Select a clip from the clip pool
|
||||
Select { index: usize } => { pool.set_clip_index(*index); Ok(None) },
|
||||
Select { index: usize } => {
|
||||
pool.set_clip_index(*index);
|
||||
Ok(None)
|
||||
},
|
||||
// Update the contents of the clip pool
|
||||
Clip { command: PoolClipCommand } => Ok(command.execute(pool)?.map(|command|Self::Clip{command})),
|
||||
Clip { command: PoolClipCommand } => Ok(
|
||||
command.act(pool)?.map(|command|Self::Clip{command})
|
||||
),
|
||||
// Rename a clip
|
||||
Rename { command: RenameCommand } => Ok(command.delegate(pool, |command|Self::Rename{command})?),
|
||||
Rename { command: RenameCommand } => Ok(
|
||||
command.act(pool)?.map(|command|Self::Rename{command})
|
||||
),
|
||||
// Change the length of a clip
|
||||
Length { command: CropCommand } => Ok(command.delegate(pool, |command|Self::Length{command})?),
|
||||
Length { command: CropCommand } => Ok(
|
||||
command.act(pool)?.map(|command|Self::Length{command})
|
||||
),
|
||||
// Import from file
|
||||
Import { command: BrowseCommand } => Ok(if let Some(browse) = pool.browse.as_mut() {
|
||||
command.delegate(browse, |command|Self::Import{command})?
|
||||
command.act(browse)?.map(|command|Self::Import{command})
|
||||
} else {
|
||||
None
|
||||
}),
|
||||
// Export to file
|
||||
Export { command: BrowseCommand } => Ok(if let Some(browse) = pool.browse.as_mut() {
|
||||
command.delegate(browse, |command|Self::Export{command})?
|
||||
command.act(browse)?.map(|command|Self::Export{command})
|
||||
} else {
|
||||
None
|
||||
}),
|
||||
|
|
@ -429,7 +443,7 @@ def_command!(PoolClipCommand: |pool: Pool| {
|
|||
for event in events.iter() {
|
||||
clip.notes[event.0 as usize].push(event.2);
|
||||
}
|
||||
Ok(Self::Add { index, clip }.execute(pool)?)
|
||||
Ok(Self::Add { index, clip }.act(pool)?)
|
||||
},
|
||||
SetName { index: usize, name: Arc<str> } => {
|
||||
let index = *index;
|
||||
|
|
@ -103,4 +103,3 @@ impl Dialog {
|
|||
/// FIXME: implement
|
||||
pub fn browser_target (&self) -> Option<&BrowseTarget> { todo!() }
|
||||
}
|
||||
|
||||
|
|
@ -188,7 +188,7 @@ pub trait MidiViewer: MidiRange + MidiPoint + Debug + Send + Sync {
|
|||
}
|
||||
let next_time_area = time_axis * next_time_zoom;
|
||||
if next_time_area >= time_len {
|
||||
self.time_zoom().set(next_time_zoom);
|
||||
self.time_zoom().store(next_time_zoom, Relaxed);
|
||||
} else {
|
||||
break
|
||||
}
|
||||
|
|
@ -199,7 +199,7 @@ pub trait MidiViewer: MidiRange + MidiPoint + Debug + Send + Sync {
|
|||
}
|
||||
let prev_time_area = time_axis * prev_time_zoom;
|
||||
if prev_time_area <= time_len {
|
||||
self.time_zoom().set(prev_time_zoom);
|
||||
self.time_zoom().store(prev_time_zoom, Relaxed);
|
||||
} else {
|
||||
break
|
||||
}
|
||||
|
|
@ -746,7 +746,7 @@ impl MidiViewer for PianoHorizontal {
|
|||
let buf_size = self.buffer_size(&clip);
|
||||
let mut buffer = BigBuffer::from(buf_size);
|
||||
let time_zoom = self.get_time_zoom();
|
||||
self.time_len().set(clip.length);
|
||||
self.time_len().store(clip.length, Relaxed);
|
||||
PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom,self.get_note_len(), self.get_note_pos(), self.get_time_pos());
|
||||
PianoHorizontal::draw_fg(&mut buffer, &clip, time_zoom);
|
||||
buffer
|
||||
0
src/device/midi.rs
Normal file
0
src/device/midi.rs
Normal file
|
|
@ -1,4 +1,5 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Debug, Default)] pub enum MeteringMode {
|
||||
#[default] Rms,
|
||||
Log10,
|
||||
163
src/device/port.rs
Normal file
163
src/device/port.rs
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
use crate::*;
|
||||
use ConnectName::*;
|
||||
use ConnectScope::*;
|
||||
use ConnectStatus::*;
|
||||
|
||||
pub mod audio; pub use self::audio::*;
|
||||
pub mod midi; pub use self::midi::*;
|
||||
pub mod connect; pub use self::connect::*;
|
||||
|
||||
pub trait JackPort: HasJack<'static> {
|
||||
|
||||
type Port: PortSpec + Default;
|
||||
|
||||
type Pair: PortSpec + Default;
|
||||
|
||||
fn new (jack: &Jack<'static>, name: &impl AsRef<str>, connect: &[Connect])
|
||||
-> Usually<Self> where Self: Sized;
|
||||
|
||||
fn register (jack: &Jack<'static>, name: &impl AsRef<str>) -> Usually<Port<Self::Port>> {
|
||||
jack.with_client(|c|c.register_port::<Self::Port>(name.as_ref(), Default::default()))
|
||||
.map_err(|e|e.into())
|
||||
}
|
||||
|
||||
fn port_name (&self) -> &Arc<str>;
|
||||
|
||||
fn connections (&self) -> &[Connect];
|
||||
|
||||
fn port (&self) -> &Port<Self::Port>;
|
||||
|
||||
fn port_mut (&mut self) -> &mut Port<Self::Port>;
|
||||
|
||||
fn into_port (self) -> Port<Self::Port> where Self: Sized;
|
||||
|
||||
fn close (self) -> Usually<()> where Self: Sized {
|
||||
let jack = self.jack().clone();
|
||||
Ok(jack.with_client(|c|c.unregister_port(self.into_port()))?)
|
||||
}
|
||||
|
||||
fn ports (&self, re_name: Option<&str>, re_type: Option<&str>, flags: PortFlags) -> Vec<String> {
|
||||
self.with_client(|c|c.ports(re_name, re_type, flags))
|
||||
}
|
||||
|
||||
fn port_by_id (&self, id: u32) -> Option<Port<Unowned>> {
|
||||
self.with_client(|c|c.port_by_id(id))
|
||||
}
|
||||
|
||||
fn port_by_name (&self, name: impl AsRef<str>) -> Option<Port<Unowned>> {
|
||||
self.with_client(|c|c.port_by_name(name.as_ref()))
|
||||
}
|
||||
|
||||
fn connect_to_matching <'k> (&'k self) -> Usually<()> {
|
||||
for connect in self.connections().iter() {
|
||||
match &connect.name {
|
||||
Some(Exact(name)) => {
|
||||
*connect.status.write().unwrap() = self.connect_exact(name)?;
|
||||
},
|
||||
Some(RegExp(re)) => {
|
||||
*connect.status.write().unwrap() = self.connect_regexp(re, connect.scope)?;
|
||||
},
|
||||
_ => {},
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn connect_exact <'k> (&'k self, name: &str) ->
|
||||
Usually<Vec<(Port<Unowned>, Arc<str>, ConnectStatus)>>
|
||||
{
|
||||
self.with_client(move|c|{
|
||||
let mut status = vec![];
|
||||
for port in c.ports(None, None, PortFlags::empty()).iter() {
|
||||
if port.as_str() == &*name {
|
||||
if let Some(port) = c.port_by_name(port.as_str()) {
|
||||
let port_status = self.connect_to_unowned(&port)?;
|
||||
let name = port.name()?.into();
|
||||
status.push((port, name, port_status));
|
||||
if port_status == Connected {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(status)
|
||||
})
|
||||
}
|
||||
|
||||
fn connect_regexp <'k> (
|
||||
&'k self, re: &str, scope: Option<ConnectScope>
|
||||
) -> Usually<Vec<(Port<Unowned>, Arc<str>, ConnectStatus)>> {
|
||||
self.with_client(move|c|{
|
||||
let mut status = vec![];
|
||||
let ports = c.ports(Some(&re), None, PortFlags::empty());
|
||||
for port in ports.iter() {
|
||||
if let Some(port) = c.port_by_name(port.as_str()) {
|
||||
let port_status = self.connect_to_unowned(&port)?;
|
||||
let name = port.name()?.into();
|
||||
status.push((port, name, port_status));
|
||||
if port_status == Connected && scope == Some(One) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(status)
|
||||
})
|
||||
}
|
||||
|
||||
/** Connect to a matching port by name. */
|
||||
fn connect_to_name (&self, name: impl AsRef<str>) -> Usually<ConnectStatus> {
|
||||
self.with_client(|c|if let Some(ref port) = c.port_by_name(name.as_ref()) {
|
||||
self.connect_to_unowned(port)
|
||||
} else {
|
||||
Ok(Missing)
|
||||
})
|
||||
}
|
||||
|
||||
/** Connect to a matching port by reference. */
|
||||
fn connect_to_unowned (&self, port: &Port<Unowned>) -> Usually<ConnectStatus> {
|
||||
self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) {
|
||||
Connected
|
||||
} else if let Ok(_) = c.connect_ports(port, self.port()) {
|
||||
Connected
|
||||
} else {
|
||||
Mismatch
|
||||
}))
|
||||
}
|
||||
|
||||
/** Connect to an owned matching port by reference. */
|
||||
fn connect_to_owned (&self, port: &Port<Self::Pair>) -> Usually<ConnectStatus> {
|
||||
self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) {
|
||||
Connected
|
||||
} else if let Ok(_) = c.connect_ports(port, self.port()) {
|
||||
Connected
|
||||
} else {
|
||||
Mismatch
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait RegisterPorts: HasJack<'static> {
|
||||
/// Register a MIDI input port.
|
||||
fn midi_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiInput>;
|
||||
/// Register a MIDI output port.
|
||||
fn midi_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiOutput>;
|
||||
/// Register an audio input port.
|
||||
fn audio_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioInput>;
|
||||
/// Register an audio output port.
|
||||
fn audio_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioOutput>;
|
||||
}
|
||||
|
||||
impl<J: HasJack<'static>> RegisterPorts for J {
|
||||
fn midi_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiInput> {
|
||||
MidiInput::new(self.jack(), name, connect)
|
||||
}
|
||||
fn midi_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiOutput> {
|
||||
MidiOutput::new(self.jack(), name, connect)
|
||||
}
|
||||
fn audio_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioInput> {
|
||||
AudioInput::new(self.jack(), name, connect)
|
||||
}
|
||||
fn audio_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioOutput> {
|
||||
AudioOutput::new(self.jack(), name, connect)
|
||||
}
|
||||
}
|
||||
103
src/device/port/audio.rs
Normal file
103
src/device/port/audio.rs
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
use crate::*;
|
||||
|
||||
def_command!(AudioInputCommand: |port: AudioInput| {
|
||||
Close => todo!(),
|
||||
Connect { audio_out: Arc<str> } => todo!(),
|
||||
});
|
||||
|
||||
def_command!(AudioOutputCommand: |port: AudioOutput| {
|
||||
Close => todo!(),
|
||||
Connect { audio_in: Arc<str> } => todo!(),
|
||||
});
|
||||
|
||||
impl HasJack<'static> for AudioInput { fn jack (&self) -> &Jack<'static> { &self.jack } }
|
||||
impl HasJack<'static> for AudioOutput { fn jack (&self) -> &Jack<'static> { &self.jack } }
|
||||
|
||||
/// Audio input port.
|
||||
#[derive(Debug)] pub struct AudioInput {
|
||||
/// Handle to JACK client, for receiving reconnect events.
|
||||
pub jack: Jack<'static>,
|
||||
/// Port name
|
||||
pub name: Arc<str>,
|
||||
/// Port handle.
|
||||
pub port: Port<AudioIn>,
|
||||
/// List of ports to connect to.
|
||||
pub connections: Vec<Connect>,
|
||||
}
|
||||
|
||||
/// Audio output port.
|
||||
#[derive(Debug)] pub struct AudioOutput {
|
||||
/// Handle to JACK client, for receiving reconnect events.
|
||||
pub jack: Jack<'static>,
|
||||
/// Port name
|
||||
pub name: Arc<str>,
|
||||
/// Port handle.
|
||||
pub port: Port<AudioOut>,
|
||||
/// List of ports to connect to.
|
||||
pub connections: Vec<Connect>,
|
||||
}
|
||||
|
||||
|
||||
impl JackPort for AudioInput {
|
||||
type Port = AudioIn;
|
||||
type Pair = AudioOut;
|
||||
fn port_name (&self) -> &Arc<str> {
|
||||
&self.name
|
||||
}
|
||||
fn port (&self) -> &Port<Self::Port> {
|
||||
&self.port
|
||||
}
|
||||
fn port_mut (&mut self) -> &mut Port<Self::Port> {
|
||||
&mut self.port
|
||||
}
|
||||
fn into_port (self) -> Port<Self::Port> {
|
||||
self.port
|
||||
}
|
||||
fn connections (&self) -> &[Connect] {
|
||||
self.connections.as_slice()
|
||||
}
|
||||
fn new (jack: &Jack<'static>, name: &impl AsRef<str>, connect: &[Connect])
|
||||
-> Usually<Self> where Self: Sized
|
||||
{
|
||||
let port = Self {
|
||||
port: Self::register(jack, name)?,
|
||||
jack: jack.clone(),
|
||||
name: name.as_ref().into(),
|
||||
connections: connect.to_vec()
|
||||
};
|
||||
port.connect_to_matching()?;
|
||||
Ok(port)
|
||||
}
|
||||
}
|
||||
|
||||
impl JackPort for AudioOutput {
|
||||
type Port = AudioOut;
|
||||
type Pair = AudioIn;
|
||||
fn port_name (&self) -> &Arc<str> {
|
||||
&self.name
|
||||
}
|
||||
fn port (&self) -> &Port<Self::Port> {
|
||||
&self.port
|
||||
}
|
||||
fn port_mut (&mut self) -> &mut Port<Self::Port> {
|
||||
&mut self.port
|
||||
}
|
||||
fn into_port (self) -> Port<Self::Port> {
|
||||
self.port
|
||||
}
|
||||
fn connections (&self) -> &[Connect] {
|
||||
self.connections.as_slice()
|
||||
}
|
||||
fn new (jack: &Jack<'static>, name: &impl AsRef<str>, connect: &[Connect])
|
||||
-> Usually<Self> where Self: Sized
|
||||
{
|
||||
let port = Self {
|
||||
port: Self::register(jack, name)?,
|
||||
jack: jack.clone(),
|
||||
name: name.as_ref().into(),
|
||||
connections: connect.to_vec()
|
||||
};
|
||||
port.connect_to_matching()?;
|
||||
Ok(port)
|
||||
}
|
||||
}
|
||||
83
src/device/port/connect.rs
Normal file
83
src/device/port/connect.rs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
use crate::*;
|
||||
use ConnectName::*;
|
||||
use ConnectScope::*;
|
||||
use ConnectStatus::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)] pub enum ConnectName {
|
||||
/** Exact match */
|
||||
Exact(Arc<str>),
|
||||
/** Match regular expression */
|
||||
RegExp(Arc<str>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectScope {
|
||||
One,
|
||||
All
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectStatus {
|
||||
Missing,
|
||||
Disconnected,
|
||||
Connected,
|
||||
Mismatch,
|
||||
}
|
||||
|
||||
/// Port connection manager.
|
||||
///
|
||||
/// ```
|
||||
/// let connect = tek::Connect::default();
|
||||
/// ```
|
||||
#[derive(Clone, Debug, Default)] pub struct Connect {
|
||||
pub name: Option<ConnectName>,
|
||||
pub scope: Option<ConnectScope>,
|
||||
pub status: Arc<RwLock<Vec<(Port<Unowned>, Arc<str>, ConnectStatus)>>>,
|
||||
pub info: Arc<str>,
|
||||
}
|
||||
|
||||
impl Connect {
|
||||
pub fn collect (exact: &[impl AsRef<str>], re: &[impl AsRef<str>], re_all: &[impl AsRef<str>])
|
||||
-> Vec<Self>
|
||||
{
|
||||
let mut connections = vec![];
|
||||
for port in exact.iter() { connections.push(Self::exact(port)) }
|
||||
for port in re.iter() { connections.push(Self::regexp(port)) }
|
||||
for port in re_all.iter() { connections.push(Self::regexp_all(port)) }
|
||||
connections
|
||||
}
|
||||
/// Connect to this exact port
|
||||
pub fn exact (name: impl AsRef<str>) -> Self {
|
||||
let info = format!("=:{}", name.as_ref()).into();
|
||||
let name = Some(Exact(name.as_ref().into()));
|
||||
Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info }
|
||||
}
|
||||
pub fn regexp (name: impl AsRef<str>) -> Self {
|
||||
let info = format!("~:{}", name.as_ref()).into();
|
||||
let name = Some(RegExp(name.as_ref().into()));
|
||||
Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info }
|
||||
}
|
||||
pub fn regexp_all (name: impl AsRef<str>) -> Self {
|
||||
let info = format!("+:{}", name.as_ref()).into();
|
||||
let name = Some(RegExp(name.as_ref().into()));
|
||||
Self { name, scope: Some(All), status: Arc::new(RwLock::new(vec![])), info }
|
||||
}
|
||||
pub fn info (&self) -> Arc<str> {
|
||||
format!(" ({}) {} {}", {
|
||||
let status = self.status.read().unwrap();
|
||||
let mut ok = 0;
|
||||
for (_, _, state) in status.iter() {
|
||||
if *state == Connected {
|
||||
ok += 1
|
||||
}
|
||||
}
|
||||
format!("{ok}/{}", status.len())
|
||||
}, match self.scope {
|
||||
None => "x",
|
||||
Some(One) => " ",
|
||||
Some(All) => "*",
|
||||
}, match &self.name {
|
||||
None => format!("x"),
|
||||
Some(Exact(name)) => format!("= {name}"),
|
||||
Some(RegExp(name)) => format!("~ {name}"),
|
||||
}).into()
|
||||
}
|
||||
}
|
||||
256
src/device/port/midi.rs
Normal file
256
src/device/port/midi.rs
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
use crate::*;
|
||||
|
||||
impl HasJack<'static> for MidiInput { fn jack (&self) -> &Jack<'static> { &self.jack } }
|
||||
|
||||
impl HasJack<'static> for MidiOutput { fn jack (&self) -> &Jack<'static> { &self.jack } }
|
||||
|
||||
/// MIDI input port.
|
||||
#[derive(Debug)] pub struct MidiInput {
|
||||
/// Handle to JACK client, for receiving reconnect events.
|
||||
pub jack: Jack<'static>,
|
||||
/// Port name
|
||||
pub name: Arc<str>,
|
||||
/// Port handle.
|
||||
pub port: Port<MidiIn>,
|
||||
/// List of currently held notes.
|
||||
pub held: Arc<RwLock<[bool;128]>>,
|
||||
/// List of ports to connect to.
|
||||
pub connections: Vec<Connect>,
|
||||
}
|
||||
|
||||
/// MIDI output port.
|
||||
#[derive(Debug)] pub struct MidiOutput {
|
||||
/// Handle to JACK client, for receiving reconnect events.
|
||||
pub jack: Jack<'static>,
|
||||
/// Port name
|
||||
pub name: Arc<str>,
|
||||
/// Port handle.
|
||||
pub port: Port<MidiOut>,
|
||||
/// List of ports to connect to.
|
||||
pub connections: Vec<Connect>,
|
||||
/// List of currently held notes.
|
||||
pub held: Arc<RwLock<[bool;128]>>,
|
||||
/// Buffer
|
||||
pub note_buffer: Vec<u8>,
|
||||
/// Buffer
|
||||
pub output_buffer: Vec<Vec<Vec<u8>>>,
|
||||
}
|
||||
|
||||
def_command!(MidiInputCommand: |port: MidiInput| {
|
||||
Close => todo!(),
|
||||
Connect { midi_out: Arc<str> } => todo!(),
|
||||
});
|
||||
|
||||
def_command!(MidiOutputCommand: |port: MidiOutput| {
|
||||
Close => todo!(),
|
||||
Connect { midi_in: Arc<str> } => todo!(),
|
||||
});
|
||||
/// Trait for thing that may receive MIDI.
|
||||
pub trait HasMidiIns {
|
||||
fn midi_ins (&self) -> &Vec<MidiInput>;
|
||||
fn midi_ins_mut (&mut self) -> &mut Vec<MidiInput>;
|
||||
/// Collect MIDI input from app ports (TODO preallocate large buffers)
|
||||
fn midi_input_collect <'a> (&'a self, scope: &'a ProcessScope) -> CollectedMidiInput<'a> {
|
||||
self.midi_ins().iter()
|
||||
.map(|port|port.port().iter(scope)
|
||||
.map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes)))
|
||||
.collect::<Vec<_>>())
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
fn midi_ins_with_sizes <'a> (&'a self) ->
|
||||
impl Iterator<Item=(usize, &'a Arc<str>, &'a [Connect], usize, usize)> + Send + Sync + 'a
|
||||
{
|
||||
let mut y = 0;
|
||||
self.midi_ins().iter().enumerate().map(move|(i, input)|{
|
||||
let height = 1 + input.connections().len();
|
||||
let data = (i, input.port_name(), input.connections(), y, y + height);
|
||||
y += height;
|
||||
data
|
||||
})
|
||||
}
|
||||
}
|
||||
/// Trait for thing that may output MIDI.
|
||||
pub trait HasMidiOuts {
|
||||
fn midi_outs (&self) -> &Vec<MidiOutput>;
|
||||
fn midi_outs_mut (&mut self) -> &mut Vec<MidiOutput>;
|
||||
fn midi_outs_with_sizes <'a> (&'a self) ->
|
||||
impl Iterator<Item=(usize, &'a Arc<str>, &'a [Connect], usize, usize)> + Send + Sync + 'a
|
||||
{
|
||||
let mut y = 0;
|
||||
self.midi_outs().iter().enumerate().map(move|(i, output)|{
|
||||
let height = 1 + output.connections().len();
|
||||
let data = (i, output.port_name(), output.connections(), y, y + height);
|
||||
y += height;
|
||||
data
|
||||
})
|
||||
}
|
||||
fn midi_outs_emit (&mut self, scope: &ProcessScope) {
|
||||
for port in self.midi_outs_mut().iter_mut() {
|
||||
port.buffer_emit(scope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl JackPort for MidiInput {
|
||||
type Port = MidiIn;
|
||||
type Pair = MidiOut;
|
||||
fn port_name (&self) -> &Arc<str> {
|
||||
&self.name
|
||||
}
|
||||
fn port (&self) -> &Port<Self::Port> {
|
||||
&self.port
|
||||
}
|
||||
fn port_mut (&mut self) -> &mut Port<Self::Port> {
|
||||
&mut self.port
|
||||
}
|
||||
fn into_port (self) -> Port<Self::Port> {
|
||||
self.port
|
||||
}
|
||||
fn connections (&self) -> &[Connect] {
|
||||
self.connections.as_slice()
|
||||
}
|
||||
fn new (jack: &Jack<'static>, name: &impl AsRef<str>, connect: &[Connect])
|
||||
-> Usually<Self> where Self: Sized
|
||||
{
|
||||
let port = Self {
|
||||
port: Self::register(jack, name)?,
|
||||
jack: jack.clone(),
|
||||
name: name.as_ref().into(),
|
||||
connections: connect.to_vec(),
|
||||
held: Arc::new(RwLock::new([false;128]))
|
||||
};
|
||||
port.connect_to_matching()?;
|
||||
Ok(port)
|
||||
}
|
||||
}
|
||||
|
||||
impl JackPort for MidiOutput {
|
||||
type Port = MidiOut;
|
||||
type Pair = MidiIn;
|
||||
fn port_name (&self) -> &Arc<str> {
|
||||
&self.name
|
||||
}
|
||||
fn port (&self) -> &Port<Self::Port> {
|
||||
&self.port
|
||||
}
|
||||
fn port_mut (&mut self) -> &mut Port<Self::Port> {
|
||||
&mut self.port
|
||||
}
|
||||
fn into_port (self) -> Port<Self::Port> {
|
||||
self.port
|
||||
}
|
||||
fn connections (&self) -> &[Connect] {
|
||||
self.connections.as_slice()
|
||||
}
|
||||
fn new (jack: &Jack<'static>, name: &impl AsRef<str>, connect: &[Connect])
|
||||
-> Usually<Self> where Self: Sized
|
||||
{
|
||||
let port = Self::register(jack, name)?;
|
||||
let jack = jack.clone();
|
||||
let name = name.as_ref().into();
|
||||
let connections = connect.to_vec();
|
||||
let port = Self {
|
||||
jack,
|
||||
port,
|
||||
name,
|
||||
connections,
|
||||
held: Arc::new([false;128].into()),
|
||||
note_buffer: vec![0;8],
|
||||
output_buffer: vec![vec![];65536],
|
||||
};
|
||||
port.connect_to_matching()?;
|
||||
Ok(port)
|
||||
}
|
||||
}
|
||||
|
||||
impl View<Tui> for MidiInput {
|
||||
fn view (&self) -> impl Draw<Tui> {
|
||||
"TODO: MIDI IN"
|
||||
}
|
||||
}
|
||||
|
||||
impl View<Tui> for MidiOutput {
|
||||
fn view (&self) -> impl Draw<Tui> {
|
||||
"TODO: MIDI OUT"
|
||||
}
|
||||
}
|
||||
|
||||
impl MidiOutput {
|
||||
/// Clear the section of the output buffer that we will be using,
|
||||
/// emitting "all notes off" at start of buffer if requested.
|
||||
pub fn buffer_clear (&mut self, scope: &ProcessScope, reset: bool) {
|
||||
let n_frames = (scope.n_frames() as usize).min(self.output_buffer.len());
|
||||
for frame in &mut self.output_buffer[0..n_frames] {
|
||||
frame.clear();
|
||||
}
|
||||
if reset {
|
||||
all_notes_off(&mut self.output_buffer);
|
||||
}
|
||||
}
|
||||
/// Write a note to the output buffer
|
||||
pub fn buffer_write <'a> (
|
||||
&'a mut self,
|
||||
sample: usize,
|
||||
event: LiveEvent,
|
||||
) {
|
||||
self.note_buffer.fill(0);
|
||||
event.write(&mut self.note_buffer).expect("failed to serialize MIDI event");
|
||||
self.output_buffer[sample].push(self.note_buffer.clone());
|
||||
// Update the list of currently held notes.
|
||||
if let LiveEvent::Midi { ref message, .. } = event {
|
||||
update_keys(&mut*self.held.write().unwrap(), message);
|
||||
}
|
||||
}
|
||||
/// Write a chunk of MIDI data from the output buffer to the output port.
|
||||
pub fn buffer_emit (&mut self, scope: &ProcessScope) {
|
||||
let samples = scope.n_frames() as usize;
|
||||
let mut writer = self.port.writer(scope);
|
||||
for (time, events) in self.output_buffer.iter().enumerate().take(samples) {
|
||||
for bytes in events.iter() {
|
||||
writer.write(&RawMidi { time: time as u32, bytes }).unwrap_or_else(|_|{
|
||||
panic!("Failed to write MIDI data: {bytes:?}");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl MidiInput {
|
||||
pub fn parsed <'a> (&'a self, scope: &'a ProcessScope) -> impl Iterator<Item=(usize, LiveEvent<'a>, &'a [u8])> {
|
||||
parse_midi_input(self.port().iter(scope))
|
||||
}
|
||||
}
|
||||
impl<T: AsRef<Vec<MidiInput>> + AsMut<Vec<MidiInput>>> HasMidiIns for T {
|
||||
fn midi_ins (&self) -> &Vec<MidiInput> { self.as_ref() }
|
||||
fn midi_ins_mut (&mut self) -> &mut Vec<MidiInput> { self.as_mut() }
|
||||
}
|
||||
impl<T: AsRef<Vec<MidiOutput>> + AsMut<Vec<MidiOutput>>> HasMidiOuts for T {
|
||||
fn midi_outs (&self) -> &Vec<MidiOutput> { self.as_ref() }
|
||||
fn midi_outs_mut (&mut self) -> &mut Vec<MidiOutput> { self.as_mut() }
|
||||
}
|
||||
impl<T: HasMidiIns + HasJack<'static>> AddMidiIn for T {
|
||||
fn midi_in_add (&mut self) -> Usually<()> {
|
||||
let index = self.midi_ins().len();
|
||||
let port = MidiInput::new(self.jack(), &format!("M/{index}"), &[])?;
|
||||
self.midi_ins_mut().push(port);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
/// Trail for thing that may gain new MIDI ports.
|
||||
impl<T: HasMidiOuts + HasJack<'static>> AddMidiOut for T {
|
||||
fn midi_out_add (&mut self) -> Usually<()> {
|
||||
let index = self.midi_outs().len();
|
||||
let port = MidiOutput::new(self.jack(), &format!("{index}/M"), &[])?;
|
||||
self.midi_outs_mut().push(port);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// May create new MIDI input ports.
|
||||
pub trait AddMidiIn {
|
||||
fn midi_in_add (&mut self) -> Usually<()>;
|
||||
}
|
||||
|
||||
/// May create new MIDI output ports.
|
||||
pub trait AddMidiOut {
|
||||
fn midi_out_add (&mut self) -> Usually<()>;
|
||||
}
|
||||
|
|
@ -4,10 +4,10 @@ def_command!(SamplerCommand: |sampler: Sampler| {
|
|||
RecordToggle { slot: usize } => {
|
||||
let slot = *slot;
|
||||
let recording = sampler.recording.as_ref().map(|x|x.0);
|
||||
let _ = Self::RecordFinish.execute(sampler)?;
|
||||
let _ = Self::RecordFinish.act(sampler)?;
|
||||
// autoslice: continue recording at next slot
|
||||
if recording != Some(slot) {
|
||||
Self::RecordBegin { slot }.execute(sampler)
|
||||
Self::RecordBegin { slot }.act(sampler)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
99
src/mode.rs
99
src/mode.rs
|
|
@ -1,99 +0,0 @@
|
|||
use crate::{*, config::*};
|
||||
|
||||
impl Config {
|
||||
pub fn get_mode (&self, mode: impl AsRef<str>) -> Option<Arc<Mode<Arc<str>>>> {
|
||||
self.modes.clone().read().unwrap().get(mode.as_ref()).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn load_mode (modes: &Modes, name: &impl AsRef<str>, body: &impl Language) -> Usually<()> {
|
||||
let mut mode = Mode::default();
|
||||
body.each(|item|mode.add(item))?;
|
||||
modes.write().unwrap().insert(name.as_ref().into(), Arc::new(mode));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Collection of interaction modes.
|
||||
pub type Modes = Arc<RwLock<BTreeMap<Arc<str>, Arc<Mode<Arc<str>>>>>>;
|
||||
|
||||
impl Mode<Arc<str>> {
|
||||
/// Add a definition to the mode.
|
||||
///
|
||||
/// Supported definitions:
|
||||
///
|
||||
/// - (name ...) -> name
|
||||
/// - (info ...) -> description
|
||||
/// - (keys ...) -> key bindings
|
||||
/// - (mode ...) -> submode
|
||||
/// - ... -> view
|
||||
///
|
||||
/// ```
|
||||
/// let mut mode: tek::Mode<std::sync::Arc<str>> = Default::default();
|
||||
/// mode.add("(name hello)").unwrap();
|
||||
/// ```
|
||||
pub fn add (&mut self, dsl: impl Language) -> Usually<()> {
|
||||
Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() {
|
||||
//println!("Mode::add: {head} {:?}", expr.tail());
|
||||
let tail = expr.tail()?.map(|x|x.trim()).unwrap_or("");
|
||||
match head {
|
||||
"name" => self.add_name(tail)?,
|
||||
"info" => self.add_info(tail)?,
|
||||
"keys" => self.add_keys(tail)?,
|
||||
"mode" => self.add_mode(tail)?,
|
||||
_ => self.add_view(tail)?,
|
||||
};
|
||||
} else if let Ok(Some(word)) = dsl.word() {
|
||||
self.add_view(word);
|
||||
} else {
|
||||
return Err(format!("Mode::add: unexpected: {dsl:?}").into());
|
||||
})
|
||||
|
||||
//DslParse(dsl, ||Err(format!("Mode::add: unexpected: {dsl:?}").into()))
|
||||
//.word(|word|self.add_view(word))
|
||||
//.expr(|expr|expr.head(|head|{
|
||||
////println!("Mode::add: {head} {:?}", expr.tail());
|
||||
//let tail = expr.tail()?.map(|x|x.trim()).unwrap_or("");
|
||||
//match head {
|
||||
//"name" => self.add_name(tail),
|
||||
//"info" => self.add_info(tail),
|
||||
//"keys" => self.add_keys(tail)?,
|
||||
//"mode" => self.add_mode(tail)?,
|
||||
//_ => self.add_view(tail),
|
||||
//};
|
||||
//}))
|
||||
}
|
||||
|
||||
fn add_name (&mut self, dsl: impl Language) -> Perhaps<()> {
|
||||
Ok(dsl.src()?.map(|src|self.name.push(src.into())))
|
||||
}
|
||||
fn add_info (&mut self, dsl: impl Language) -> Perhaps<()> {
|
||||
Ok(dsl.src()?.map(|src|self.info.push(src.into())))
|
||||
}
|
||||
fn add_view (&mut self, dsl: impl Language) -> Perhaps<()> {
|
||||
Ok(dsl.src()?.map(|src|self.view.push(src.into())))
|
||||
}
|
||||
fn add_keys (&mut self, dsl: impl Language) -> Perhaps<()> {
|
||||
Ok(Some(dsl.each(|expr|{ self.keys.push(expr.trim().into()); Ok(()) })?))
|
||||
}
|
||||
fn add_mode (&mut self, dsl: impl Language) -> Perhaps<()> {
|
||||
Ok(Some(if let Some(id) = dsl.head()? {
|
||||
load_mode(&self.modes, &id, &dsl.tail())?;
|
||||
} else {
|
||||
return Err(format!("Mode::add: self: incomplete: {dsl:?}").into());
|
||||
}))
|
||||
}
|
||||
}
|
||||
/// Group of view and keys definitions.
|
||||
///
|
||||
/// ```
|
||||
/// let mode = tek::Mode::<std::sync::Arc<str>>::default();
|
||||
/// ```
|
||||
#[derive(Default, Debug)] pub struct Mode<D: Language + Ord> {
|
||||
pub path: PathBuf,
|
||||
pub name: Vec<D>,
|
||||
pub info: Vec<D>,
|
||||
pub view: Vec<D>,
|
||||
pub keys: Vec<D>,
|
||||
pub modes: Modes,
|
||||
}
|
||||
|
||||
918
src/tek.rs
918
src/tek.rs
|
|
@ -1,9 +1,37 @@
|
|||
#![allow(clippy::unit_arg)]
|
||||
#![feature(
|
||||
adt_const_params, associated_type_defaults, closure_lifetime_binder,
|
||||
adt_const_params, associated_type_defaults, anonymous_lifetime_in_impl_trait, closure_lifetime_binder,
|
||||
impl_trait_in_assoc_type, trait_alias, type_alias_impl_trait, type_changing_struct_update
|
||||
)]
|
||||
|
||||
/// CLI banner.
|
||||
pub(crate) const HEADER: &'static str = r#"
|
||||
|
||||
~ █▀█▀█ █▀▀█ █ █ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~
|
||||
█ █▀ █▀▀▄ ~ v0.4.0, 2026 winter (or is it) ~
|
||||
~ ▀ █▀▀█ ▀ ▀ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ "#;
|
||||
|
||||
#[cfg(feature = "cli")] use clap::{self, Parser, Subcommand};
|
||||
|
||||
pub extern crate atomic_float;
|
||||
|
||||
pub extern crate xdg;
|
||||
pub(crate) use ::xdg::BaseDirectories;
|
||||
|
||||
pub extern crate midly;
|
||||
pub(crate) use ::midly::{Smf, TrackEventKind, MidiMessage, Error as MidiError, num::*, live::*};
|
||||
|
||||
pub extern crate tengri;
|
||||
pub(crate) use tengri::{
|
||||
*, lang::*, exit::*, eval::*, keys::*, sing::*, time::*, draw::*, term::*, color::*,
|
||||
crossterm::event::{Event, KeyEvent},
|
||||
ratatui::{
|
||||
self,
|
||||
prelude::{Rect, Style, Stylize, Buffer, Color::{self, *}},
|
||||
widgets::{Widget, canvas::{Canvas, Line}},
|
||||
},
|
||||
};
|
||||
|
||||
/// Implement an arithmetic operation for a unit of time
|
||||
#[macro_export] macro_rules! impl_op {
|
||||
($T:ident, $Op:ident, $method:ident, |$a:ident,$b:ident|{$impl:expr}) => {
|
||||
|
|
@ -25,76 +53,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
/// TODO: Preserve the generic passthru syntax;
|
||||
/// remove this macro (only used twice) and potentially the trait.
|
||||
#[macro_export] macro_rules! impl_has_clips {
|
||||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||||
impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? {
|
||||
fn clips <'a> (&'a $self) -> std::sync::RwLockReadGuard<'a, ClipPool> {
|
||||
$cb.read().unwrap()
|
||||
}
|
||||
fn clips_mut <'a> (&'a $self) -> std::sync::RwLockWriteGuard<'a, ClipPool> {
|
||||
$cb.write().unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod arrange;
|
||||
pub mod bind;
|
||||
pub mod browse;
|
||||
pub mod cli;
|
||||
pub mod clock;
|
||||
pub mod config;
|
||||
pub mod device;
|
||||
pub mod dialog;
|
||||
pub mod editor;
|
||||
pub mod menu;
|
||||
pub mod mix;
|
||||
pub mod mode;
|
||||
pub mod sample;
|
||||
pub mod sequence;
|
||||
pub mod select;
|
||||
pub mod view;
|
||||
|
||||
#[cfg(feature = "plugin")] pub mod plugin;
|
||||
|
||||
use clap::{self, Parser, Subcommand};
|
||||
use self::{
|
||||
config::*, mode::*, view::*, bind::*,
|
||||
dialog::*, browse::*, menu::*,
|
||||
clock::*, sequence::*, editor::*,
|
||||
arrange::*, select::*, device::*, sample::*,
|
||||
};
|
||||
|
||||
extern crate xdg;
|
||||
pub(crate) use ::xdg::BaseDirectories;
|
||||
pub extern crate atomic_float;
|
||||
//pub(crate) use atomic_float::AtomicF64;
|
||||
//pub extern crate jack;
|
||||
//pub(crate) use ::jack::{*, contrib::{*, ClosureProcessHandler}};
|
||||
pub extern crate midly;
|
||||
pub(crate) use ::midly::{Smf, TrackEventKind, MidiMessage, Error as MidiError, num::*, live::*};
|
||||
pub extern crate tengri;
|
||||
pub(crate) use tengri::{
|
||||
*,
|
||||
lang::*,
|
||||
exit::*,
|
||||
eval::*,
|
||||
keys::*,
|
||||
sing::*,
|
||||
time::*,
|
||||
draw::*,
|
||||
term::*,
|
||||
color::*,
|
||||
space::*,
|
||||
crossterm::event::{Event, KeyEvent},
|
||||
ratatui::{
|
||||
self,
|
||||
prelude::{Rect, Style, Stylize, Buffer, Color::{self, *}},
|
||||
widgets::{Widget, canvas::{Canvas, Line}},
|
||||
},
|
||||
};
|
||||
#[cfg(feature = "sampler")] pub(crate) use symphonia::{
|
||||
default::get_codecs,
|
||||
core::{//errors::Error as SymphoniaError,
|
||||
|
|
@ -102,6 +60,7 @@ pub(crate) use tengri::{
|
|||
codecs::{Decoder, CODEC_TYPE_NULL},
|
||||
},
|
||||
};
|
||||
|
||||
#[cfg(feature = "lv2_gui")] use ::winit::{
|
||||
application::ApplicationHandler,
|
||||
event::WindowEvent,
|
||||
|
|
@ -109,6 +68,7 @@ pub(crate) use tengri::{
|
|||
window::{Window, WindowId},
|
||||
platform::x11::EventLoopBuilderExtX11
|
||||
};
|
||||
|
||||
#[allow(unused)] pub(crate) use ::{
|
||||
std::{
|
||||
cmp::Ord,
|
||||
|
|
@ -126,163 +86,23 @@ pub(crate) use tengri::{
|
|||
};
|
||||
|
||||
/// Command-line entrypoint.
|
||||
#[cfg(feature = "cli")] pub fn main () -> Usually<()> {
|
||||
#[cfg(feature = "cli")]
|
||||
pub fn main () -> Usually<()> {
|
||||
Config::watch(|config|{
|
||||
Exit::enter(|exit|{
|
||||
Jack::connect("tek", |jack|{
|
||||
let state = Arc::new(RwLock::new(App {
|
||||
color: ItemTheme::random(),
|
||||
config: Config::init(),
|
||||
dialog: Dialog::welcome(),
|
||||
jack: jack.clone(),
|
||||
mode: ":menu",
|
||||
project: Arrangement::new(&jack, &Clock::new(&jack, 51)),
|
||||
..Default::default()
|
||||
}));
|
||||
// TODO: Sync these timings with main clock, so that things
|
||||
// "accidentally" fall on the beat in overload conditions.
|
||||
let keyboard = tui_keyboard(&exit, &state, Duration::from_millis(100))?;
|
||||
let project = Arrangement::new(&jack, &Clock::new(&jack, 51));
|
||||
let state = App::new_shared(jack, config, project, ":menu");
|
||||
let keyboard = tui_input(&exit, &state, Duration::from_millis(100))?;
|
||||
let terminal = tui_output(&exit, &state, Duration::from_millis(10))?;
|
||||
(keyboard, terminal)
|
||||
// TODO: Sync I/O timings with main clock, so that things
|
||||
// "accidentally" fall on the beat in overload conditions.
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new application from a backend, project, config, and mode
|
||||
///
|
||||
/// ```
|
||||
/// let jack = tek::tengri::Jack::new(&"test_tek").expect("failed to connect to jack");
|
||||
/// let proj = tek::Arrangement::default();
|
||||
/// let mut conf = tek::Config::default();
|
||||
/// conf.add("(mode hello)");
|
||||
/// let tek = tek::tek(&jack, proj, conf, "hello");
|
||||
/// ```
|
||||
pub fn tek (
|
||||
jack: &Jack<'static>, project: Arrangement, config: Config, mode: impl AsRef<str>
|
||||
) -> App {
|
||||
let mode: &str = mode.as_ref();
|
||||
App {
|
||||
color: ItemTheme::random(),
|
||||
dialog: Dialog::welcome(),
|
||||
jack: jack.clone(),
|
||||
mode: config.get_mode(mode).expect(&format!("failed to find mode '{mode}'")),
|
||||
config,
|
||||
project,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn tek_confirm (state: &mut App) -> Perhaps<AppCommand> {
|
||||
Ok(match &state.dialog {
|
||||
Dialog::Menu(index, items) => {
|
||||
let callback = items.0[*index].1.clone();
|
||||
callback(state)?;
|
||||
None
|
||||
},
|
||||
_ => todo!(),
|
||||
})
|
||||
}
|
||||
|
||||
fn tek_inc (state: &mut App, axis: &ControlAxis) -> Perhaps<AppCommand> {
|
||||
Ok(match (&state.dialog, axis) {
|
||||
(Dialog::None, _) => todo!(),
|
||||
(Dialog::Menu(_, _), ControlAxis::Y) => AppCommand::SetDialog { dialog: state.dialog.menu_next() }
|
||||
.act(state)?,
|
||||
_ => todo!()
|
||||
})
|
||||
}
|
||||
|
||||
fn tek_dec (state: &mut App, axis: &ControlAxis) -> Perhaps<AppCommand> {
|
||||
Ok(match (&state.dialog, axis) {
|
||||
(Dialog::None, _) => None,
|
||||
(Dialog::Menu(_, _), ControlAxis::Y) => AppCommand::SetDialog { dialog: state.dialog.menu_prev() }
|
||||
.act(state)?,
|
||||
_ => todo!()
|
||||
})
|
||||
}
|
||||
|
||||
//impl_handle!(TuiIn: |self: App, input|{
|
||||
//let commands = tek_collect_commands(self, input)?;
|
||||
//let history = tek_execute_commands(self, commands)?;
|
||||
//self.history.extend(history.into_iter());
|
||||
//Ok(None)
|
||||
//});
|
||||
//fn tek_collect_commands (app: &App, input: &TuiIn) -> Usually<Vec<AppCommand>> {
|
||||
//let mut commands = vec![];
|
||||
//for id in app.mode.keys.iter() {
|
||||
//if let Some(event_map) = app.config.binds.clone().read().unwrap().get(id.as_ref())
|
||||
//&& let Some(bindings) = event_map.query(input.event()) {
|
||||
//for binding in bindings {
|
||||
//for command in binding.commands.iter() {
|
||||
//if let Some(command) = app.namespace(command)? as Option<AppCommand> {
|
||||
//commands.push(command)
|
||||
//}
|
||||
//}
|
||||
//}
|
||||
//}
|
||||
//}
|
||||
//Ok(commands)
|
||||
//}
|
||||
//fn tek_execute_commands (
|
||||
//app: &mut App, commands: Vec<AppCommand>
|
||||
//) -> Usually<Vec<(AppCommand, Option<AppCommand>)>> {
|
||||
//let mut history = vec![];
|
||||
//for command in commands.into_iter() {
|
||||
//let result = command.act(app);
|
||||
//match result { Err(err) => { history.push((command, None)); return Err(err) }
|
||||
//Ok(undo) => { history.push((command, undo)); } };
|
||||
//}
|
||||
//Ok(history)
|
||||
//}
|
||||
|
||||
pub fn tek_jack_process (app: &mut App, client: &Client, scope: &ProcessScope) -> Control {
|
||||
let t0 = app.perf.get_t0();
|
||||
app.clock().update_from_scope(scope).unwrap();
|
||||
let midi_in = app.project.midi_input_collect(scope);
|
||||
if let Some(editor) = &app.editor() {
|
||||
let mut pitch: Option<u7> = None;
|
||||
for port in midi_in.iter() {
|
||||
for event in port.iter() {
|
||||
if let (_, Ok(LiveEvent::Midi {message: MidiMessage::NoteOn {key, ..}, ..}))
|
||||
= event
|
||||
{
|
||||
pitch = Some(key.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(pitch) = pitch {
|
||||
editor.set_note_pos(pitch.as_int() as usize);
|
||||
}
|
||||
}
|
||||
let result = app.project.process_tracks(client, scope);
|
||||
app.perf.update_from_jack_scope(t0, scope);
|
||||
result
|
||||
}
|
||||
|
||||
pub fn tek_jack_event (app: &mut App, event: JackEvent) {
|
||||
use JackEvent::*;
|
||||
match event {
|
||||
SampleRate(sr) => { app.clock().timebase.sr.set(sr as f64); },
|
||||
PortRegistration(_id, true) => {
|
||||
//let port = app.jack().port_by_id(id);
|
||||
//println!("\rport add: {id} {port:?}");
|
||||
//println!("\rport add: {id}");
|
||||
},
|
||||
PortRegistration(_id, false) => {
|
||||
/*println!("\rport del: {id}")*/
|
||||
},
|
||||
PortsConnected(_a, _b, true) => { /*println!("\rport conn: {a} {b}")*/ },
|
||||
PortsConnected(_a, _b, false) => { /*println!("\rport disc: {a} {b}")*/ },
|
||||
ClientRegistration(_id, true) => {},
|
||||
ClientRegistration(_id, false) => {},
|
||||
ThreadInit => {},
|
||||
XRun => {},
|
||||
GraphReorder => {},
|
||||
_ => { panic!("{event:?}"); }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn swap_value <T: Clone + PartialEq, U> (
|
||||
target: &mut T, value: &T, returned: impl Fn(T)->U
|
||||
) -> Perhaps<U> {
|
||||
|
|
@ -307,18 +127,9 @@ pub fn toggle_bool <U> (
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
//take!(DeviceCommand|state: Arrangement, iter|state.selected_device().as_ref()
|
||||
//.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten()));
|
||||
|
||||
#[macro_export] macro_rules! has_clip {
|
||||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||||
impl $(<$($L),*$($T $(: $U)?),*>)? HasMidiClip for $Struct $(<$($L),*$($T),*>)? {
|
||||
fn clip (&$self) -> Option<Arc<RwLock<MidiClip>>> { $cb }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn scan (dir: &PathBuf) -> Usually<(Vec<OsString>, Vec<OsString>)> {
|
||||
let (mut subdirs, mut files) = std::fs::read_dir(dir)?
|
||||
.fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{
|
||||
|
|
@ -340,17 +151,6 @@ pub(crate) fn track_width (_index: usize, track: &Track) -> u16 {
|
|||
track.width as u16
|
||||
}
|
||||
|
||||
def_command!(AppCommand: |app: App| {
|
||||
Nop => Ok(None),
|
||||
Confirm => tek_confirm(app),
|
||||
Cancel => todo!(), // TODO delegate:
|
||||
Inc { axis: ControlAxis } => tek_inc(app, axis),
|
||||
Dec { axis: ControlAxis } => tek_dec(app, axis),
|
||||
SetDialog { dialog: Dialog } => {
|
||||
swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog })
|
||||
},
|
||||
});
|
||||
|
||||
/// Define a type alias for iterators of sized items (columns).
|
||||
macro_rules! def_sizes_iter {
|
||||
($Type:ident => $($Item:ty),+) => {
|
||||
|
|
@ -358,341 +158,11 @@ macro_rules! def_sizes_iter {
|
|||
Iterator<Item=(usize, $(&'a $Item,)+ usize, usize)> + Send + Sync + 'a;
|
||||
}
|
||||
}
|
||||
def_sizes_iter!(InputsSizes => MidiInput);
|
||||
def_sizes_iter!(OutputsSizes => MidiOutput);
|
||||
def_sizes_iter!(PortsSizes => Arc<str>, [Connect]);
|
||||
def_sizes_iter!(ScenesSizes => Scene);
|
||||
def_sizes_iter!(TracksSizes => Track);
|
||||
|
||||
/// ```
|
||||
/// let _ = tek::view_logo();
|
||||
/// ```
|
||||
pub fn view_logo () -> impl Draw<Tui> {
|
||||
wh_exact(Some(32), Some(7), bold(true, fg(Rgb(240, 200, 180), south!{
|
||||
h_exact(1, ""),
|
||||
h_exact(1, ""),
|
||||
h_exact(1, "~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~"),
|
||||
h_exact(1, east("~~~~ ║ ~ ╟─╌ ~╟─< ~~ ", east(fg(Rgb(230,100,40), "v0.3.0"), " ~~"))),
|
||||
h_exact(1, "~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~"),
|
||||
})))
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// let x = std::sync::Arc::<std::sync::RwLock<String>>::default();
|
||||
/// let _ = tek::view_transport(true, x.clone(), x.clone(), x.clone());
|
||||
/// let _ = tek::view_transport(false, x.clone(), x.clone(), x.clone());
|
||||
/// ```
|
||||
pub fn view_transport (play: bool, bpm: &str, beat: &str, time: &str) -> impl Draw<Tui> {
|
||||
let theme = ItemTheme::G[96];
|
||||
bg(Black, east!(above(
|
||||
wh_full(origin_w(button_play_pause(play))),
|
||||
wh_full(origin_e(east!(
|
||||
field_h(theme, "BPM", bpm),
|
||||
field_h(theme, "Beat", beat),
|
||||
field_h(theme, "Time", time),
|
||||
)))
|
||||
)))
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// let x = std::sync::Arc::<std::sync::RwLock<String>>::default();
|
||||
/// let _ = tek::view_status(None, x.clone(), x.clone(), x.clone());
|
||||
/// let _ = tek::view_status(Some("".into()), x.clone(), x.clone(), x.clone());
|
||||
/// ```
|
||||
pub fn view_status (sel: Option<&str>, sr: &str, buf: &str, lat: &str) -> impl Draw<Tui> {
|
||||
let theme = ItemTheme::G[96];
|
||||
let sr = field_h(theme, "SR", sr);
|
||||
let buf = field_h(theme, "Buf", buf);
|
||||
let lat = field_h(theme, "Lat", lat);
|
||||
bg(Black, east!(above(
|
||||
wh_full(origin_w(sel.map(|sel|field_h(theme, "Selected", sel)))),
|
||||
wh_full(origin_e(east!(sr, buf, lat))),
|
||||
)))
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// let _ = tek::button_play_pause(true);
|
||||
/// ```
|
||||
pub fn button_play_pause (playing: bool) -> impl Draw<Tui> {
|
||||
let compact = true;//self.is_editing();
|
||||
bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) },
|
||||
either(compact,
|
||||
thunk(move|to: &mut Tui|w_exact(9, either(playing,
|
||||
fg(Rgb(0, 255, 0), " PLAYING "),
|
||||
fg(Rgb(255, 128, 0), " STOPPED "))
|
||||
).draw(to)),
|
||||
thunk(move|to: &mut Tui|w_exact(5, either(playing,
|
||||
fg(Rgb(0, 255, 0), south(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)),
|
||||
fg(Rgb(255, 128, 0), south(" ▗▄▖ ", " ▝▀▘ ",)))
|
||||
).draw(to)),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "track")] pub fn view_track_row_section (
|
||||
_theme: ItemTheme,
|
||||
button: impl Draw<Tui>,
|
||||
button_add: impl Draw<Tui>,
|
||||
content: impl Draw<Tui>,
|
||||
) -> impl Draw<Tui> {
|
||||
west(h_full(w_exact(4, origin_nw(button_add))),
|
||||
east(w_exact(20, h_full(origin_nw(button))), wh_full(origin_c(content))))
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// let bg = tengri::ratatui::style::Color::Red;
|
||||
/// let fg = tengri::ratatui::style::Color::Green;
|
||||
/// let _ = tek::view_wrap(bg, fg, "and then blue, too!");
|
||||
/// ```
|
||||
pub fn view_wrap (bg: Color, fg: Color, content: impl Draw<Tui>) -> impl Draw<Tui> {
|
||||
let left = fg_bg(bg, Reset, w_exact(1, y_repeat("▐")));
|
||||
let right = fg_bg(bg, Reset, w_exact(1, y_repeat("▌")));
|
||||
east(left, west(right, fg_bg(fg, bg, content)))
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// let _ = tek::view_meter("", 0.0);
|
||||
/// let _ = tek::view_meters(&[0.0, 0.0]);
|
||||
/// ```
|
||||
pub fn view_meter <'a> (label: &'a str, value: f32) -> impl Draw<Tui> + 'a {
|
||||
let f = field_h(ItemTheme::G[128], label, format!("{:>+9.3}", value));
|
||||
let w = if value >= 0.0 { 13 }
|
||||
else if value >= -1.0 { 12 }
|
||||
else if value >= -2.0 { 11 }
|
||||
else if value >= -3.0 { 10 }
|
||||
else if value >= -4.0 { 9 }
|
||||
else if value >= -6.0 { 8 }
|
||||
else if value >= -9.0 { 7 }
|
||||
else if value >= -12.0 { 6 }
|
||||
else if value >= -15.0 { 5 }
|
||||
else if value >= -20.0 { 4 }
|
||||
else if value >= -25.0 { 3 }
|
||||
else if value >= -30.0 { 2 }
|
||||
else if value >= -40.0 { 1 }
|
||||
else { 0 };
|
||||
let c = if value >= 0.0 { Red }
|
||||
else if value >= -3.0 { Yellow }
|
||||
else { Green };
|
||||
south!(f, wh_exact(Some(w), Some(1), bg(c, ())))
|
||||
}
|
||||
|
||||
pub fn view_meters (values: &[f32;2]) -> impl Draw<Tui> + use<'_> {
|
||||
let left = format!("L/{:>+9.3}", values[0]);
|
||||
let right = format!("R/{:>+9.3}", values[1]);
|
||||
south(left, right)
|
||||
}
|
||||
|
||||
pub fn draw_info (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> + use<'_> {
|
||||
when(sample.is_some(), thunk(move|to: &mut Tui|{
|
||||
let sample = sample.unwrap().read().unwrap();
|
||||
let theme = sample.color;
|
||||
east!(
|
||||
field_h(theme, "Name", format!("{:<10}", sample.name.clone())),
|
||||
field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())),
|
||||
field_h(theme, "Start", format!("{:<8}", sample.start)),
|
||||
field_h(theme, "End", format!("{:<8}", sample.end)),
|
||||
field_h(theme, "Trans", "0"),
|
||||
field_h(theme, "Gain", format!("{}", sample.gain)),
|
||||
).draw(to)
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn draw_info_v (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> + use<'_> {
|
||||
let a = thunk(move|to: &mut Tui|{
|
||||
let sample = sample.unwrap().read().unwrap();
|
||||
let theme = sample.color;
|
||||
w_exact(20, south!(
|
||||
w_full(origin_w(field_h(theme, "Name ", format!("{:<10}", sample.name.clone())))),
|
||||
w_full(origin_w(field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())))),
|
||||
w_full(origin_w(field_h(theme, "Start ", format!("{:<8}", sample.start)))),
|
||||
w_full(origin_w(field_h(theme, "End ", format!("{:<8}", sample.end)))),
|
||||
w_full(origin_w(field_h(theme, "Trans ", "0"))),
|
||||
w_full(origin_w(field_h(theme, "Gain ", format!("{}", sample.gain)))),
|
||||
)).draw(to)
|
||||
});
|
||||
|
||||
let b = thunk(|to: &mut Tui|fg(Red, south!(
|
||||
bold(true, "× No sample."),
|
||||
"[r] record",
|
||||
"[Shift-F9] import",
|
||||
)).draw(to));
|
||||
|
||||
either(sample.is_some(), a, b)
|
||||
}
|
||||
|
||||
pub fn draw_status (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> {
|
||||
bold(true, fg(g(224), sample
|
||||
.map(|sample|{
|
||||
let sample = sample.read().unwrap();
|
||||
format!("Sample {}-{}", sample.start, sample.end)
|
||||
})
|
||||
.unwrap_or_else(||"No sample".to_string())))
|
||||
}
|
||||
|
||||
pub fn view_track_header (theme: ItemTheme, content: impl Draw<Tui>) -> impl Draw<Tui> {
|
||||
w_exact(12, bg(theme.darker.term, w_full(origin_e(content))))
|
||||
}
|
||||
|
||||
pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, ports: &'a [T])
|
||||
-> impl Draw<Tui> + use<'a, T>
|
||||
{
|
||||
let ins = ports.len() as u16;
|
||||
let frame = Outer(true, Style::default().fg(g(96)));
|
||||
let iter = move||ports.iter();
|
||||
let names = iter_south(iter, move|port, index|h_full(origin_w(format!(" {index} {}", port.port_name()))));
|
||||
let field = field_v(theme, title, names);
|
||||
wh_exact(Some(20), Some(1 + ins), border(true, frame, wh_exact(Some(20), Some(1 + ins), field)))
|
||||
}
|
||||
|
||||
pub fn io_ports <'a, T: PortsSizes<'a>> (
|
||||
fg: Color, bg: Color, items: impl Fn()->T + Send + Sync + 'a
|
||||
) -> impl Draw<Tui> + 'a {
|
||||
type Item<'a> = (usize, &'a Arc<str>, &'a [Connect], usize, usize);
|
||||
iter(items, move|(_index, name, connections, y, y2): Item<'a>, _| {
|
||||
y_push(y as u16, h_exact((y2-y) as u16,
|
||||
south(h_full(bold(true, fg_bg(fg, bg, origin_w(east(" ", name))))),
|
||||
iter(||connections.iter(), move|connect: &'a Connect, index|y_push(
|
||||
index as u16, h_exact(1, origin_w(bold(false, fg_bg(fg, bg, &connect.info))))
|
||||
)))))
|
||||
})
|
||||
}
|
||||
|
||||
/// CLI banner.
|
||||
pub(crate) const HEADER: &'static str = r#"
|
||||
|
||||
~ █▀█▀█ █▀▀█ █ █ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~
|
||||
█ █▀ █▀▀▄ ~ v0.4.0, 2026 winter (or is it) ~
|
||||
~ ▀ █▀▀█ ▀ ▀ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ "#;
|
||||
|
||||
/// Total state
|
||||
///
|
||||
/// ```
|
||||
/// use tek::{HasTracks, HasScenes, TracksView, ScenesView};
|
||||
/// let mut app = tek::App::default();
|
||||
/// let _ = app.scene_add(None, None).unwrap();
|
||||
/// let _ = app.update_clock();
|
||||
/// app.project.editor = Some(Default::default());
|
||||
/// //let _: Vec<_> = app.project.inputs_with_sizes().collect();
|
||||
/// //let _: Vec<_> = app.project.outputs_with_sizes().collect();
|
||||
/// let _: Vec<_> = app.project.tracks_with_sizes().collect();
|
||||
/// //let _: Vec<_> = app.project.scenes_with_sizes(true, 10, 10).collect();
|
||||
/// //let _: Vec<_> = app.scenes_with_colors(true, 10).collect();
|
||||
/// //let _: Vec<_> = app.scenes_with_track_colors(true, 10, 10).collect();
|
||||
/// let _ = app.project.w();
|
||||
/// //let _ = app.project.w_sidebar();
|
||||
/// //let _ = app.project.w_tracks_area();
|
||||
/// let _ = app.project.h();
|
||||
/// //let _ = app.project.h_tracks_area();
|
||||
/// //let _ = app.project.h_inputs();
|
||||
/// //let _ = app.project.h_outputs();
|
||||
/// let _ = app.project.h_scenes();
|
||||
/// ```
|
||||
#[derive(Default, Debug)] pub struct App {
|
||||
/// Base color.
|
||||
pub color: ItemTheme,
|
||||
/// Must not be dropped for the duration of the process
|
||||
pub jack: Jack<'static>,
|
||||
/// Display size
|
||||
pub size: Size,
|
||||
/// Performance counter
|
||||
pub perf: PerfModel,
|
||||
/// Available view modes and input bindings
|
||||
pub config: Config,
|
||||
/// Currently selected mode
|
||||
pub mode: Arc<Mode<Arc<str>>>,
|
||||
/// Undo history
|
||||
pub history: Vec<(AppCommand, Option<AppCommand>)>,
|
||||
/// Dialog overlay
|
||||
pub dialog: Dialog,
|
||||
/// Contains all recently created clips.
|
||||
pub pool: Pool,
|
||||
/// Contains the currently edited musical arrangement
|
||||
pub project: Arrangement,
|
||||
/// Error, if any
|
||||
pub error: Arc<RwLock<Option<Arc<str>>>>
|
||||
}
|
||||
impl_has!(Clock: |self: App|self.project.clock);
|
||||
impl_has!(Vec<MidiInput>: |self: App|self.project.midi_ins);
|
||||
impl_has!(Vec<MidiOutput>: |self: App|self.project.midi_outs);
|
||||
impl_has!(Dialog: |self: App|self.dialog);
|
||||
impl_has!(Jack<'static>: |self: App|self.jack);
|
||||
impl_has!(Size: |self: App|self.size);
|
||||
impl_has!(Pool: |self: App|self.pool);
|
||||
impl_has!(Selection: |self: App|self.project.selection);
|
||||
impl_as_ref!(Vec<Scene>: |self: App|self.project.as_ref());
|
||||
impl_as_mut!(Vec<Scene>: |self: App|self.project.as_mut());
|
||||
impl_as_ref_opt!(MidiEditor: |self: App|self.project.as_ref_opt());
|
||||
impl_as_mut_opt!(MidiEditor: |self: App|self.project.as_mut_opt());
|
||||
impl_has_clips!( |self: App|self.pool.clips);
|
||||
impl_audio!(App: tek_jack_process, tek_jack_event);
|
||||
namespace!(App: Arc<str> { literal = |dsl|Ok(dsl.src()?.map(|x|x.into())); });
|
||||
namespace!(App: u8 { literal = |dsl|try_to_u8(dsl); });
|
||||
namespace!(App: u16 { literal = |dsl|try_to_u16(dsl); symbol = |app| {
|
||||
":w/sidebar" => app.project.w_sidebar(app.editor().is_some()),
|
||||
":h/sample-detail" => 6.max(app.size.h() as u16 * 3 / 9), }; });
|
||||
namespace!(App: isize { literal = |dsl|try_to_isize(dsl); });
|
||||
namespace!(App: usize { literal = |dsl|try_to_usize(dsl); symbol = |app| {
|
||||
":scene-count" => app.scenes().len(),
|
||||
":track-count" => app.tracks().len(),
|
||||
":device-kind" => app.dialog.device_kind().unwrap_or(0),
|
||||
":device-kind/next" => app.dialog.device_kind_next().unwrap_or(0),
|
||||
":device-kind/prev" => app.dialog.device_kind_prev().unwrap_or(0), }; });
|
||||
namespace!(App: bool { symbol = |app| { // Provide boolean values.
|
||||
":mode/editor" => app.project.editor.is_some(),
|
||||
":focused/dialog" => !matches!(app.dialog, Dialog::None),
|
||||
":focused/message" => matches!(app.dialog, Dialog::Message(..)),
|
||||
":focused/add_device" => matches!(app.dialog, Dialog::Device(..)),
|
||||
":focused/browser" => app.dialog.browser().is_some(),
|
||||
":focused/pool/import" => matches!(app.pool.mode, Some(PoolMode::Import(..))),
|
||||
":focused/pool/export" => matches!(app.pool.mode, Some(PoolMode::Export(..))),
|
||||
":focused/pool/rename" => matches!(app.pool.mode, Some(PoolMode::Rename(..))),
|
||||
":focused/pool/length" => matches!(app.pool.mode, Some(PoolMode::Length(..))),
|
||||
":focused/clip" => !app.editor_focused() && matches!(app.selection(), Selection::TrackClip{..}),
|
||||
":focused/track" => !app.editor_focused() && matches!(app.selection(), Selection::Track(..)),
|
||||
":focused/scene" => !app.editor_focused() && matches!(app.selection(), Selection::Scene(..)),
|
||||
":focused/mix" => !app.editor_focused() && matches!(app.selection(), Selection::Mix),
|
||||
}; });
|
||||
namespace!(App: ItemTheme {}); // TODO: provide colors here
|
||||
namespace!(App: Selection { symbol = |app| {
|
||||
":select/scene" => app.selection().select_scene(app.tracks().len()),
|
||||
":select/scene/next" => app.selection().select_scene_next(app.scenes().len()),
|
||||
":select/scene/prev" => app.selection().select_scene_prev(),
|
||||
":select/track" => app.selection().select_track(app.tracks().len()),
|
||||
":select/track/next" => app.selection().select_track_next(app.tracks().len()),
|
||||
":select/track/prev" => app.selection().select_track_prev(),
|
||||
}; });
|
||||
namespace!(App: Color {
|
||||
symbol = |app| {
|
||||
":color/bg" => Color::Rgb(28, 32, 36),
|
||||
};
|
||||
expression = |app| {
|
||||
"g" (n: u8) => Color::Rgb(n, n, n),
|
||||
"rgb" (r: u8, g: u8, b: u8) => Color::Rgb(r, g, b),
|
||||
};
|
||||
});
|
||||
namespace!(App: Option<u7> { symbol = |app| {
|
||||
":editor/pitch" => Some((app.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into())
|
||||
}; });
|
||||
namespace!(App: Option<usize> { symbol = |app| {
|
||||
":selected/scene" => app.selection().scene(),
|
||||
":selected/track" => app.selection().track(),
|
||||
}; });
|
||||
namespace!(App: Option<Arc<RwLock<MidiClip>>> {
|
||||
symbol = |app| {
|
||||
":selected/clip" => if let Selection::TrackClip { track, scene } = app.selection() {
|
||||
app.scenes()[*scene].clips[*track].clone()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
pub trait HasClipsSize { fn clips_size (&self) -> &Size; }
|
||||
|
||||
pub trait HasDevices: AsRef<Vec<Device>> + AsMut<Vec<Device>> {
|
||||
fn devices (&self) -> &Vec<Device> { self.as_ref() }
|
||||
fn devices_mut (&mut self) -> &mut Vec<Device> { self.as_mut() }
|
||||
}
|
||||
primitive!(u8: try_to_u8);
|
||||
primitive!(u16: try_to_u16);
|
||||
primitive!(usize: try_to_usize);
|
||||
primitive!(isize: try_to_isize);
|
||||
pub trait HasWidth {
|
||||
const MIN_WIDTH: usize;
|
||||
/// Increment track width.
|
||||
|
|
@ -701,301 +171,11 @@ pub trait HasWidth {
|
|||
fn width_dec (&mut self);
|
||||
}
|
||||
|
||||
impl<'a> Namespace<'a, AppCommand> for App {
|
||||
symbols!('a |app| -> AppCommand {
|
||||
"x/inc" => AppCommand::Inc { axis: ControlAxis::X },
|
||||
"x/dec" => AppCommand::Dec { axis: ControlAxis::X },
|
||||
"y/inc" => AppCommand::Inc { axis: ControlAxis::Y },
|
||||
"y/dec" => AppCommand::Dec { axis: ControlAxis::Y },
|
||||
"confirm" => AppCommand::Confirm,
|
||||
"cancel" => AppCommand::Cancel,
|
||||
});
|
||||
}
|
||||
impl Interpret<Tui, ()> for App {
|
||||
fn interpret_expr <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Usually<()> {
|
||||
app_interpret_expr(self, to, lang)
|
||||
}
|
||||
fn interpret_word <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Usually<()> {
|
||||
app_interpret_word(self, to, lang)
|
||||
}
|
||||
}
|
||||
fn app_interpret_expr (state: &App, to: &mut Tui, lang: &impl Expression) -> Usually<()> {
|
||||
if eval_view(state, to, lang)? || eval_view_tui(state, to, lang)? {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("App::interpret_expr: unexpected: {lang:?}").into())
|
||||
}
|
||||
}
|
||||
fn app_interpret_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usually<()> {
|
||||
let mut frags = dsl.src()?.unwrap().split("/");
|
||||
match frags.next() {
|
||||
pub mod device; pub use self::device::*;
|
||||
pub mod app; pub use self::app::*;
|
||||
|
||||
Some(":logo") => view_logo().draw(to),
|
||||
|
||||
Some(":status") => h_exact(1, "TODO: Status Bar").draw(to),
|
||||
|
||||
Some(":meters") => match frags.next() {
|
||||
Some("input") => bg(Rgb(30, 30, 30), h_full(origin_s("Input Meters"))).draw(to),
|
||||
Some("output") => bg(Rgb(30, 30, 30), h_full(origin_s("Output Meters"))).draw(to),
|
||||
_ => panic!()
|
||||
},
|
||||
|
||||
Some(":tracks") => match frags.next() {
|
||||
None => "TODO tracks".draw(to),
|
||||
Some("names") => state.project.view_track_names(state.color.clone()).draw(to),//bg(Rgb(40, 40, 40), w_full(origin_w("Track Names")))),
|
||||
Some("inputs") => bg(Rgb(40, 40, 40), w_full(origin_w("Track Inputs"))).draw(to),
|
||||
Some("devices") => bg(Rgb(40, 40, 40), w_full(origin_w("Track Devices"))).draw(to),
|
||||
Some("outputs") => bg(Rgb(40, 40, 40), w_full(origin_w("Track Outputs"))).draw(to),
|
||||
_ => panic!()
|
||||
},
|
||||
|
||||
Some(":scenes") => match frags.next() {
|
||||
None => "TODO scenes".draw(to),
|
||||
Some(":scenes/names") => "TODO Scene Names".draw(to),
|
||||
_ => panic!()
|
||||
},
|
||||
|
||||
Some(":editor") => "TODO Editor".draw(to),
|
||||
|
||||
Some(":dialog") => match frags.next() {
|
||||
Some("menu") => if let Dialog::Menu(selected, items) = &state.dialog {
|
||||
let items = items.clone();
|
||||
let selected = selected;
|
||||
Some(wh_full(thunk(move|to: &mut Tui|{
|
||||
for (index, MenuItem(item, _)) in items.0.iter().enumerate() {
|
||||
to.place(&y_push((2 * index) as u16,
|
||||
fg_bg(
|
||||
if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) },
|
||||
if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) },
|
||||
h_exact(2, origin_n(w_full(item)))
|
||||
)));
|
||||
}
|
||||
})))
|
||||
} else {
|
||||
None
|
||||
}.draw(to),
|
||||
_ => unimplemented!("App::interpret_word: {dsl:?} ({frags:?})"),
|
||||
},
|
||||
|
||||
Some(":templates") => {
|
||||
let modes = state.config.modes.clone();
|
||||
let height = (modes.read().unwrap().len() * 2) as u16;
|
||||
h_exact(height, w_min(Some(30), thunk(move |to: &mut Tui|{
|
||||
for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() {
|
||||
let bg = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) };
|
||||
let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or("<no name>");
|
||||
let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or("<no info>");
|
||||
let fg1 = Rgb(224, 192, 128);
|
||||
let fg2 = Rgb(224, 128, 32);
|
||||
let field_name = w_full(origin_w(fg(fg1, name)));
|
||||
let field_id = w_full(origin_e(fg(fg2, id)));
|
||||
let field_info = w_full(origin_w(info));
|
||||
y_push((2 * index) as u16,
|
||||
h_exact(2, w_full(bg(bg, south(
|
||||
above(field_name, field_id), field_info))))).draw(to);
|
||||
}
|
||||
})))
|
||||
}.draw(to),
|
||||
|
||||
Some(":sessions") => h_exact(Some(6), w_min(30, thunk(|to: &mut Tui|{
|
||||
let fg = Rgb(224, 192, 128);
|
||||
for (index, name) in ["session1", "session2", "session3"].iter().enumerate() {
|
||||
let bg = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) };
|
||||
y_push((2 * index) as u16,
|
||||
&h_exact(2, w_full(bg(bg, origin_w(fg(fg, name)))))).draw(to);
|
||||
}
|
||||
}))).draw(to),
|
||||
|
||||
Some(":browse/title") => w_full(origin_w(field_v(ItemColor::default(),
|
||||
match state.dialog.browser_target().unwrap() {
|
||||
BrowseTarget::SaveProject => "Save project:",
|
||||
BrowseTarget::LoadProject => "Load project:",
|
||||
BrowseTarget::ImportSample(_) => "Import sample:",
|
||||
BrowseTarget::ExportSample(_) => "Export sample:",
|
||||
BrowseTarget::ImportClip(_) => "Import clip:",
|
||||
BrowseTarget::ExportClip(_) => "Export clip:",
|
||||
}, w_shrink(3, h_exact(1, fg(g(96), x_repeat("🭻"))))))).draw(to),
|
||||
|
||||
Some(":device") => {
|
||||
let selected = state.dialog.device_kind().unwrap();
|
||||
south(bold(true, "Add device"), iter_south(
|
||||
move||device_kinds().iter(),
|
||||
move|_label: &&'static str, i|{
|
||||
let b = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) };
|
||||
let l = if i == selected { "[ " } else { " " };
|
||||
let r = if i == selected { " ]" } else { " " };
|
||||
w_full(bg(b, east(l, west(r, "FIXME device name")))) })).draw(to)
|
||||
},
|
||||
|
||||
Some(":debug") => h_exact(1, format!("[{:?}]", to.area())).draw(to),
|
||||
|
||||
Some(_) => {
|
||||
let views = state.config.views.read().unwrap();
|
||||
if let Some(dsl) = views.get(dsl.src()?.unwrap()) {
|
||||
let dsl = dsl.clone();
|
||||
std::mem::drop(views);
|
||||
state.interpret(to, &dsl)?
|
||||
} else {
|
||||
unimplemented!("{dsl:?}");
|
||||
}
|
||||
},
|
||||
|
||||
_ => unreachable!()
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
impl App {
|
||||
/// Update memoized render of clock values.
|
||||
/// ```
|
||||
/// tek::App::default().update_clock();
|
||||
/// ```
|
||||
pub fn update_clock (&self) {
|
||||
ClockView::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80)
|
||||
}
|
||||
|
||||
/// Set modal dialog.
|
||||
///
|
||||
/// ```
|
||||
/// let previous: tek::Dialog = tek::App::default().set_dialog(tek::Dialog::welcome());
|
||||
/// ```
|
||||
pub fn set_dialog (&mut self, mut dialog: Dialog) -> Dialog {
|
||||
std::mem::swap(&mut self.dialog, &mut dialog);
|
||||
dialog
|
||||
}
|
||||
|
||||
/// FIXME: generalize. Set picked device in device pick dialog.
|
||||
///
|
||||
/// ```
|
||||
/// tek::App::default().device_pick(0);
|
||||
/// ```
|
||||
pub fn device_pick (&mut self, index: usize) {
|
||||
self.dialog = Dialog::Device(index);
|
||||
}
|
||||
|
||||
/// FIXME: generalize. Add device to current track.
|
||||
pub fn add_device (&mut self, index: usize) -> Usually<()> {
|
||||
match index {
|
||||
0 => {
|
||||
let name = self.jack.with_client(|c|c.name().to_string());
|
||||
let midi = self.project.track().expect("no active track").sequencer.midi_outs[0].port_name();
|
||||
let track = self.track().expect("no active track");
|
||||
let port = format!("{}/Sampler", &track.name);
|
||||
let connect = Connect::exact(format!("{name}:{midi}"));
|
||||
let sampler = if let Ok(sampler) = Sampler::new(
|
||||
&self.jack, &port, &[connect], &[&[], &[]], &[&[], &[]]
|
||||
) {
|
||||
self.dialog = Dialog::None;
|
||||
Device::Sampler(sampler)
|
||||
} else {
|
||||
self.dialog = Dialog::Message("Failed to add device.".into());
|
||||
return Err("failed to add device".into())
|
||||
};
|
||||
let track = self.track_mut().expect("no active track");
|
||||
track.devices.push(sampler);
|
||||
Ok(())
|
||||
},
|
||||
1 => {
|
||||
todo!();
|
||||
//Ok(())
|
||||
},
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return reference to content browser if open.
|
||||
///
|
||||
/// ```
|
||||
/// assert_eq!(tek::App::default().browser(), None);
|
||||
/// ```
|
||||
pub fn browser (&self) -> Option<&Browse> {
|
||||
if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None }
|
||||
}
|
||||
|
||||
/// 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.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.read().unwrap().count_midi_messages() == 0 {
|
||||
std::mem::swap(&mut swapped, slot);
|
||||
}
|
||||
if let Some(clip) = swapped {
|
||||
self.pool.delete_clip(&clip.read().unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The [Draw] implementation for [App] handles the loaded view,
|
||||
/// which is defined in terms of [dizzle] DSL.
|
||||
///
|
||||
/// If there is an error, the error is displayed. FIXME: overlay it
|
||||
/// Then, every top-level form of the DSL description is rendered.
|
||||
impl Draw<Tui> for App {
|
||||
fn draw (self, to: &mut Tui) -> Usually<XYWH<u16>> {
|
||||
let xywh = to.area().into();
|
||||
if let Some(e) = self.error.read().unwrap().as_ref() {
|
||||
to.show(xywh, e.as_ref());
|
||||
}
|
||||
for (index, dsl) in self.mode.view.iter().enumerate() {
|
||||
if let Err(e) = self.interpret(to, dsl) {
|
||||
*self.error.write().unwrap() = Some(format!("view #{index}: {e}").into());
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(xywh)
|
||||
}
|
||||
}
|
||||
|
||||
impl HasClipsSize for App { fn clips_size (&self) -> &Size { &self.project.size_inner } }
|
||||
impl HasJack<'static> for App { fn jack (&self) -> &Jack<'static> { &self.jack } }
|
||||
impl_default!(AppCommand: Self::Nop);
|
||||
primitive!(u8: try_to_u8);
|
||||
primitive!(u16: try_to_u16);
|
||||
primitive!(usize: try_to_usize);
|
||||
primitive!(isize: try_to_isize);
|
||||
|
||||
fn field_h <T: Screen> (theme: ItemTheme, head: impl Draw<T>, body: impl Draw<T>) -> impl Draw<T> {
|
||||
}
|
||||
|
||||
fn field_v <T: Screen> (theme: ItemTheme, head: impl Draw<T>, body: impl Draw<T>) -> impl Draw<T> {
|
||||
}
|
||||
def_sizes_iter!(InputsSizes => MidiInput);
|
||||
def_sizes_iter!(OutputsSizes => MidiOutput);
|
||||
def_sizes_iter!(PortsSizes => Arc<str>, [Connect]);
|
||||
def_sizes_iter!(ScenesSizes => Scene);
|
||||
def_sizes_iter!(TracksSizes => Track);
|
||||
|
|
|
|||
10
src/view.rs
10
src/view.rs
|
|
@ -1,10 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
/// Collection of view definitions.
|
||||
pub type Views = Arc<RwLock<BTreeMap<Arc<str>, Arc<str>>>>;
|
||||
|
||||
pub(crate) fn load_view (views: &Views, name: &impl AsRef<str>, body: &impl Language) -> Usually<()> {
|
||||
views.write().unwrap().insert(name.as_ref().into(), body.src()?.unwrap_or_default().into());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue