collect tek_view, tek_ctrl

This commit is contained in:
i do not exist 2026-09-21 00:16:58 +03:00
parent c5d6ce24b2
commit 186b81f29c
47 changed files with 2665 additions and 2893 deletions

View file

@ -1,636 +0,0 @@
use crate::*;
/// Total application state.
///
/// ```
/// use tek::{HasTracks, HasScenes, TracksView, ScenesView};
/// let mut app = tek::App::default();
/// let _ = app.scenes_add_one(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)]
#[namespace(Arc<[Connect]> get_arc_array_connect)]
#[namespace(Arc<str> get_arc_str)]
#[namespace(Color get_color)]
#[namespace(ControlAxis get_axis)]
#[namespace(Dialog get_dialog)]
#[namespace(ItemColor)]
#[namespace(ItemTheme)]
#[namespace(MidiClip)]
#[namespace(Option<Arc<[Connect]>> get_opt_arc_array_connect)]
#[namespace(Option<Arc<str>> get_opt_arc_str)]
#[namespace(Option<ItemTheme> get_opt_itemtheme)]
#[namespace(Option<Arc<RwLock<MidiClip>>> get_opt_arc_rwlock_midiclip)]
#[namespace(Option<Vec<Option<Arc<RwLock<MidiClip>>>>> get_opt_vec_opt_arc_rwlock_midiclip)]
#[namespace(Option<bool> get_opt_bool)]
#[namespace(Option<u16> get_opt_u16)]
#[namespace(Option<u32> get_opt_u32)]
#[namespace(Option<usize> get_opt_usize)]
#[namespace(PathBuf get_path_buf)]
#[namespace(Vec<Arc<str>> get_vec_arc_str)]
#[namespace(bool get_bool)]
#[namespace(f64 get_f64)]
#[namespace(isize)]
#[namespace(u16 get_u16)]
#[namespace(u32)]
#[namespace(u8)]
#[namespace(usize get_usize)]
pub struct App {
/// Exit flag
pub exit: Exit,
/// Base color.
pub color: ItemTheme,
/// Must not be dropped for the duration of the process
pub jack: Jack<'static>,
/// Display size
pub size: Sizer,
/// Performance counter
pub perf: PerfModel,
/// Available view modes and input bindings
pub config: Arc<Config>,
/// Currently selected mode
pub modes: Arc<RwLock<Vec<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: Project,
/// Error, if any
pub error: Arc<RwLock<Option<Arc<str>>>>,
#[cfg(feature = "prof2")]
/// Tracing guard
pub guard: Option<tracing_flame::FlushGuard<std::io::BufWriter<std::fs::File>>>,
}
impl App {
/// Create a new application instance from a backend, project, config, and mode
///
/// ```
/// let mut proj = tek::Project::default();
/// proj.jack = tek::tengri::Jack::new(&"test_tek").expect("failed to connect to jack");
/// let mut conf = tek::Config::load(tek::Config::default(), "(mode hello)").unwrap();
/// let tek = tek::App::new(None, proj, conf.into(), "hello");
/// ```
pub fn new (
exit: Option<Exit>,
project: Project,
config: Arc<Config>,
mode: impl AsRef<str>
) -> Self {
App {
exit: exit.unwrap_or_default(),
jack: project.jack.clone(),
color: ItemTheme::random(),
dialog: Dialog::welcome(),
modes: Arc::new(RwLock::new(vec![mode.as_ref().into()])),
config,
project,
..Default::default()
}
}
}
fn get_arc_str <'a> (_: &App, src: impl Language) -> Perhaps<Arc<str>> {
Ok(src.src()?.map(|x|x.into()))
}
fn get_u16 <'a> (state: &App, src: impl Language) -> Perhaps<u16> {
Ok(Some(match src.word()? {
Some(":w/sidebar") => state.project.w_sidebar(state.editor().is_some()),
Some(":h/sample-detail") => 6.max(state.size.h() as u16 * 3 / 9),
_ => return try_to_u16(src)
}))
}
fn get_usize <'a> (state: &App, src: impl Language) -> Perhaps<usize> {
Ok(Some(match src.word()? {
Some(":scene-count") => state.scenes().len(),
Some(":track-count") => state.tracks().len(),
Some(":device-kind") => state.dialog.device_kind().unwrap_or(0),
Some(":device-kind/next") => state.dialog.device_kind_next().unwrap_or(0),
Some(":device-kind/prev") => state.dialog.device_kind_prev().unwrap_or(0),
_ => return try_to_usize(src)
}))
}
fn get_f64 <'a> (_state: &App, _src: impl Language) -> Perhaps<f64> {
todo!()
}
fn get_bool <'a> (state: &App, src: impl Language) -> Perhaps<bool> {
src.word()?.map(|word|Ok(match word {
"Y" => true,
"N" => false,
":mode/editor" => state.project.editor.is_some(),
":focused/dialog" => !matches!(state.dialog, Dialog::None),
":focused/message" => matches!(state.dialog, Dialog::Message(..)),
":focused/add_device" => matches!(state.dialog, Dialog::Device(..)),
":focused/browser" => state.dialog.browser().is_some(),
":focused/pool/import" => matches!(state.pool.mode, Some(PoolMode::Import(..))),
":focused/pool/export" => matches!(state.pool.mode, Some(PoolMode::Export(..))),
":focused/pool/rename" => matches!(state.pool.mode, Some(PoolMode::Rename(..))),
":focused/pool/length" => matches!(state.pool.mode, Some(PoolMode::Length(..))),
":focused/clip" => !state.editor_focused() && matches!(state.selection(), Selection::TrackClip{..}),
":focused/track" => !state.editor_focused() && matches!(state.selection(), Selection::Track(..)),
":focused/scene" => !state.editor_focused() && matches!(state.selection(), Selection::Scene(..)),
":focused/mix" => !state.editor_focused() && matches!(state.selection(), Selection::Mix),
_ => return err!("not bool: {word}")
})).transpose()
}
#[allow(unused)]
fn get_selection <'a> (state: &App, src: impl Language) -> Perhaps<Selection> {
src.word()?.map(|word|Ok(match word {
":select/scene" => state.selection().select_scene(state.tracks().len()),
":select/scene/next" => state.selection().select_scene_next(state.scenes().len()),
":select/scene/prev" => state.selection().select_scene_prev(),
":select/track" => state.selection().select_track(state.tracks().len()),
":select/track/next" => state.selection().select_track_next(state.tracks().len()),
":select/track/prev" => state.selection().select_track_prev(),
_ => return err!("not selection: {word}")
})).transpose()
}
fn get_color <'a> (_: &App, src: impl Language) -> Perhaps<Color> {
if let Some(expr) = src.expr()? {
match (expr.head()?, expr.tail()?) {
(Some("g"), Some(tail)) => {
let n = try_to_u8(tail.head().map_err(Into::into))?
.ok_or(LanguageError::domain("not gray"))?;
Ok(Some(Color::Rgb(n, n, n)))
},
(Some("rgb"), Some(tail)) => {
let r = try_to_u8(tail.head().map_err(Into::into))?
.ok_or(LanguageError::domain("not red"))?;
let g = try_to_u8(tail.tail().head().map_err(Into::into))?
.ok_or(LanguageError::domain("not green"))?;
let b = try_to_u8(tail.tail().tail().head().map_err(Into::into))?
.ok_or(LanguageError::domain("not blue"))?;
Ok(Some(Color::Rgb(r, g, b)))
},
(Some(_), _) => return err!("not a color expression: {expr}"),
(None, _) => return err!("not a color expression: {expr}"),
}
} else if let Ok(Some(sym)) = src.word() {
Ok(match sym {
":color/bg" => Some(Color::Rgb(28, 32, 36)),
":color/fg" => Some(Color::Rgb(98, 92, 96)),
_ => return err!("not a color: {sym}")
})
} else {
return err!("not a color: {:?}", src.src()?)
}
}
#[allow(unused)]
fn get_opt_u7 <'a> (state: &App, src: impl Language) -> Perhaps<Option<u7>> {
eval_word(src, |word|Ok(match word {
":editor/pitch" => Some((
state.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8
).into()),
_ => return err!("unknown midi note: {word}")
}))
}
fn get_opt_usize <'a> (state: &App, src: impl Language) -> Perhaps<Option<usize>> {
eval_word(src, |word|Ok(match word {
":selected/scene" => state.selection().scene(),
":selected/track" => state.selection().track(),
_ => return err!("unknown opt<usize>: {word}")
}))
}
fn get_axis <'a> (_: &App, src: impl Language) -> Perhaps<ControlAxis> {
eval_word(src, |word|Ok(match word {
"X" => ControlAxis::X, "XFine" | "X2" => ControlAxis::XFine,
"Y" => ControlAxis::Y, "YFine" | "Y2" => ControlAxis::YFine,
"Z" => ControlAxis::Z, "ZFine" | "Z2" => ControlAxis::ZFine,
_ => return err!("unknown axis {word}")
}))
}
fn get_opt_u16 <'a> (_: &App, _src: impl Language) -> Perhaps<Option<u16>> {
Ok(None)
}
fn get_opt_u32 <'a> (_: &App, _src: impl Language) -> Perhaps<Option<u32>> {
Ok(None)
}
fn get_opt_itemtheme <'a> (_: &App, _: impl Language) -> Perhaps<Option<ItemTheme>> {
todo!()
}
fn get_opt_arc_array_connect <'a> (_: &App, _: impl Language) -> Perhaps<Option<Arc<[Connect]>>> {
todo!()
}
fn get_arc_array_connect <'a> (_: &App, _: impl Language) -> Perhaps<Arc<[Connect]>> {
todo!()
}
fn get_opt_arc_str <'a> (_: &App, _: impl Language) -> Perhaps<Option<Arc<str>>> {
todo!()
}
fn get_opt_bool <'a> (_: &App, _: impl Language) -> Perhaps<Option<bool>> {
todo!()
}
fn get_path_buf <'a> (_: &App, _: impl Language) -> Perhaps<PathBuf> {
todo!()
}
fn get_vec_arc_str <'a> (_: &App, _: impl Language) -> Perhaps<Vec<Arc<str>>> {
todo!()
}
impl_has!(Clock: |self: App|self.project.clock);
impl_has!(Dialog: |self: App|self.dialog);
impl_has!(Jack<'static>: |self: App|self.jack);
impl_has!(Pool: |self: App|self.pool);
impl_has!(Selection: |self: App|self.project.selection);
impl_as_ref!(Option<MidiEditor>: |self: App|self.project.as_ref());
impl_as_mut!(Option<MidiEditor>: |self: App|self.project.as_mut());
primitive!(u8: try_to_u8);
primitive!(u16: try_to_u16);
primitive!(usize: try_to_usize);
primitive!(isize: try_to_isize);
impl HasClips for App {
fn clip (&self) -> Option<Arc<RwLock<MidiClip>>> {
self.project.clip()
}
fn clip_mut (&self) -> &mut Option<Arc<RwLock<MidiClip>>> {
self.project.clip_mut()
}
fn clips <'a> (&'a self) -> RwLockReadGuard<'a, Clips> {
self.project.clips()
}
fn clips_mut <'a> (&'a self) -> RwLockWriteGuard<'a, Clips> {
self.project.clips_mut()
}
}
tui_keys!(self: App, input {
#[cfg(feature = "prof2")] profiling::scope!("App::tui_keys!");
Self::input_command(self, input)
});
impl App {
fn input_command (state: &mut Self, input: &TuiEvent) -> Usually<()> {
if let Some(bindings) = Self::input_command_find(state, input) {
return Self::input_command_exec(state, bindings)
}
Ok(())
}
fn input_command_find (state: &mut Self, input: &TuiEvent) -> Option<Arc<[Binding<Arc<str>>]>> {
for name in state.modes.read().iter().rev() {
let mode = state.config.get_mode(name).as_ref().map(Arc::clone);
if let Some(mode) = mode {
let binds = state.config.binds.clone();
for id in mode.keys.iter() {
if let Some(event_map) = binds.find(id)
&& let Some(bindings) = event_map.query(input) {
return Some(bindings.into())
}
}
}
}
None
}
fn input_command_exec (state: &mut Self, bindings: Arc<[Binding<Arc<str>>]>) -> Usually<()> {
for binding in bindings.iter() {
for command in binding.commands.iter() {
if Self::command::<AppCommand>(state, command)? {
return Ok(())
}
}
}
Ok(())
}
fn command <
T: Dispatch<Self> + Clone + Into<AppCommand>
> (state: &mut Self, src: &str) -> Usually<bool> where
Self: Namespace<T>,
{
if let Some(command) = Namespace::<T>::namespace(state, &src)? {
return Self::command_dispatch(state, command.into())
}
Ok(false)
}
fn command_dispatch (state: &mut Self, command: AppCommand) -> Usually<bool> where
Self: Namespace<AppCommand>,
{
match command.clone().dispatch(state) {
Err(err) => {
state.history.push((command.into(), None));
return Err(err)
},
Ok(undo) => {
state.history.push((command.into(), undo.map(Into::into)));
return Ok(true)
}
}
}
}
#[tek_proc::commands(AppCommand)]
impl App {
/// Exit the application.
#[command(Exit = "exit")]
pub fn command_exit (&mut self) -> Perhaps<AppCommand> {
self.exit.exit();
Ok(None)
}
/// Cancel current selection.
#[command(Cancel = "cancel")]
pub fn command_cancel (&mut self) -> Perhaps<AppCommand> {
todo!("cancel")
}
/// Confirm current selection.
#[command(Confirm = "confirm")]
pub fn command_confirm (&mut self) -> Perhaps<AppCommand> {
Ok(match self.dialog.clone() {
Dialog::Menu(index, items) => {
for command in items[index].1.iter() {
App::command_dispatch(self, command.clone())?;
}
None
}
_ => todo!(),
})
}
/// Swap currently active modal dialog.
///
/// ```
/// let _ = tek::App::default().command_dialog(tek::Dialog::welcome());
/// ```
#[command(Dialog = "dialog")]
pub fn command_dialog (&mut self, dialog: Dialog) -> Perhaps<AppCommand> {
let mut dialog = dialog.clone();
std::mem::swap(&mut self.dialog, &mut dialog);
Ok(Some(AppCommand::Dialog { dialog }))
}
#[command(Mode = "mode")]
pub fn command_mode (&mut self, command: ModeCommand) -> Perhaps<AppCommand> {
Ok(command.dispatch(self)?.map(Into::into))
}
#[command(Project = "session")]
pub fn command_session (&mut self, command: ProjectCommand) -> Perhaps<AppCommand> {
Ok(command.dispatch(self)?.map(Into::into))
}
#[command(Axis = "axis")]
pub fn command_axis (&mut self, command: AxisCommand) -> Perhaps<AppCommand> {
Ok(command.dispatch(self)?.map(Into::into))
}
#[command(Browse = "browse")]
pub fn command_browse (&mut self, command: BrowseCommand) -> Perhaps<AppCommand> {
Ok(command.dispatch(self)?.map(Into::into))
}
#[command(MidiClip = "clip")]
pub fn command_midi_clip (&mut self, command: MidiClipCommand) -> Perhaps<AppCommand> {
Ok(command.dispatch(self)?.map(Into::into))
}
#[command(MidiEdit = "edit")]
pub fn command_midi_edit (&mut self, command: MidiEditCommand) -> Perhaps<AppCommand> {
Ok(command.dispatch(self)?.map(Into::into))
}
#[command(Pool = "pool")]
pub fn command_pool (&mut self, command: PoolCommand) -> Perhaps<AppCommand> {
Ok(command.dispatch(self)?.map(Into::into))
}
#[command(Scene = "scene")]
pub fn command_scene (&mut self, command: SceneCommand) -> Perhaps<AppCommand> {
Ok(command.dispatch(self)?.map(Into::into))
}
#[command(Scenes = "scenes")]
pub fn command_scenes (&mut self, command: ScenesCommand) -> Perhaps<AppCommand> {
Ok(command.dispatch(self)?.map(Into::into))
}
#[command(Track = "track")]
pub fn command_track (&mut self, command: TrackCommand) -> Perhaps<AppCommand> {
Ok(command.dispatch(self)?.map(Into::into))
}
#[command(Tracks = "tracks")]
pub fn command_tracks (&mut self, command: TracksCommand) -> Perhaps<AppCommand> {
Ok(command.dispatch(self)?.map(Into::into))
}
}
/// A control axis.
///
/// ```
/// let axis = tek::ControlAxis::X;
/// ```
#[derive(Debug, Copy, Clone)] pub enum ControlAxis {
X, XFine,
Y, YFine,
Z, ZFine,
I, IFine,
}
#[tek_proc::commands(AxisCommand)]
impl App {
/// Increment a given data axis.
#[command(Inc = "inc")]
pub fn inc (&mut self, axis: ControlAxis) -> Perhaps<AxisCommand> {
Ok(match (&self.dialog, axis) {
(Dialog::None, _) => todo!(),
(Dialog::Menu(_, _), ControlAxis::Y) => {
AppCommand::Dialog { dialog: self.dialog.menu_next() }.dispatch(self)?;
None
},
_ => todo!()
})
}
/// Decrement a given data axis.
#[command(Dec = "dec")]
pub fn dec (&mut self, axis: ControlAxis) -> Perhaps<AxisCommand> {
Ok(match (&self.dialog, axis) {
(Dialog::None, _) => None,
(Dialog::Menu(_, _), ControlAxis::Y) => {
AppCommand::Dialog { dialog: self.dialog.menu_prev() }.dispatch(self)?;
None
},
_ => todo!()
})
}
}
impl_from!(AppCommand: |x: AxisCommand| AppCommand::Axis { command: x });
impl_from!(AppCommand: |x: BrowseCommand| AppCommand::Browse { command: x });
impl_from!(AppCommand: |x: MidiClipCommand| AppCommand::MidiClip { command: x });
impl_from!(AppCommand: |x: MidiEditCommand| AppCommand::MidiEdit { command: x });
impl_from!(AppCommand: |x: ModeCommand| AppCommand::Mode { command: x });
impl_from!(AppCommand: |x: PoolCommand| AppCommand::Pool { command: x });
impl_from!(AppCommand: |x: ProjectCommand| AppCommand::Project { command: x });
impl_from!(AppCommand: |x: SceneCommand| AppCommand::Scene { command: x });
impl_from!(AppCommand: |x: ScenesCommand| AppCommand::Scenes { command: x });
impl_from!(AppCommand: |x: TrackCommand| AppCommand::Track { command: x });
impl_from!(AppCommand: |x: TracksCommand| AppCommand::Tracks { command: x });
/// 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) -> Drawn<u16> {
#[cfg(feature = "prof2")] profiling::scope!("App::draw");
//self.perf.cycle(&mut |_|{
self.draw_error(to)?;
self.draw_modes(to)?;
self.draw_debug(to)?;
#[cfg(feature = "prof2")] profiling::finish_frame!();
Ok(Some(to.area().into()))
//})
}
}
impl App {
fn draw_error (&self, to: &mut Tui) -> Usually<()> {
if let Some(e) = self.error.try_read().unwrap().as_ref() {
e.as_ref().align_c().draw(to)?;
}
Ok(())
}
fn draw_modes (&self, to: &mut Tui) -> Usually<()> {
for name in self.modes.read().iter() {
let mut error = false;
if let Some(mode) = self.config.get_mode(name).as_ref().map(Arc::clone) {
for (index, view) in mode.view.iter().enumerate() {
match (view.render)(self, to) {
Ok(None) => {},
Ok(Some(XYWH(.., w, h))) => {
self.size.0.store(w as usize, Relaxed);
self.size.1.store(h as usize, Relaxed);
},
Err(e) => {
let message = format!(
"Mode: {:?}\n\nLayer: #{index}\n\nError: {e}\n\nSource:\n{}",
&mode.name,
&view.source
);
*self.error.try_write().unwrap() = Some(message.into());
error = true;
break;
}
}
}
if error {
break
} else {
*self.error.try_write().unwrap() = None;
}
}
}
Ok(())
}
#[allow(unused)] fn draw_debug (&self, to: &mut Tui) -> Drawn<u16> {
Split::Above.bar(0,
Split::West.bar(16,
format!("{}/{} {} ",
self.perf.used.load(Relaxed),
self.perf.window.load(Relaxed),
self.perf.clock.raw() / 10000000).align_e(),
format!("{}x{} ",
self.size.0.load(Relaxed),
self.size.1.load(Relaxed)).align_w(),
).align_se(),
Split::East.bar(16,
format!("{}", self.modes.read()[0]),
format!("{}", self.modes.read()[0])
).align_sw()
).draw(to)
}
}
impl_has!(Sizer: |self: App|self.size);
pub trait HasWidth {
const MIN_WIDTH: usize;
/// Increment track width.
fn width_inc (&mut self);
/// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH].
fn width_dec (&mut self);
}
pub fn view_templates <'a> (state: &'a App) -> impl Draw<Tui> + use<'a> {
let height = (state.config.modes.len() * 2) as u16;
draw(move |to: &mut Tui|{
let mut index = 0;
state.config.modes.for_each(&mut |id: &str, profile: &Mode| {
let b = 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 = fg(fg1, name).align_w().full_w();
let field_id = fg(fg2, id).align_e().full_w();
let field_info = info.align_w().full_w();
let _ = bg(b, south(above(field_name, field_id), field_info))
.full_w().exact_h(2).push_y((2 * index) as u16).draw(to);
index += 1;
});
Ok(Some(to.area().into()))
}).min_w(30).exact_h(height)
}
pub fn draw_status_bar (to: &mut Tui, state: &App) -> Drawn<u16> {
bar(Split::South, 1u16,
dim(true, "Mode"),
bold(true, state.modes.read().get(0))
).push_x(1).draw(to)
}

View file

@ -1,78 +0,0 @@
use crate::*;
/// Contains memoized renders of clock values.
///
/// Performance optimization.
#[derive(Debug)] pub struct ClockView {
pub sr: Memo<Option<(bool, f64)>, String>,
pub buf: Memo<Option<f64>, String>,
pub lat: Memo<Option<f64>, String>,
pub bpm: Memo<Option<f64>, String>,
pub beat: Memo<Option<f64>, String>,
pub time: Memo<Option<f64>, String>,
}
impl_default!(ClockView: {
let mut beat = String::with_capacity(16);
let _ = write!(beat, "{}", Self::BEAT_EMPTY);
let mut time = String::with_capacity(16);
let _ = write!(time, "{}", Self::TIME_EMPTY);
let mut bpm = String::with_capacity(16);
let _ = write!(bpm, "{}", Self::BPM_EMPTY);
Self {
beat: Memo::new(None, beat),
time: Memo::new(None, time),
bpm: Memo::new(None, bpm),
sr: Memo::new(None, String::with_capacity(16)),
buf: Memo::new(None, String::with_capacity(16)),
lat: Memo::new(None, String::with_capacity(16)),
}
});
impl ClockView {
pub const BEAT_EMPTY: &'static str = "-.-.--";
pub const TIME_EMPTY: &'static str = "-.---s";
pub const BPM_EMPTY: &'static str = "---.---";
pub fn update_clock (cache: &Arc<RwLock<Self>>, clock: &Clock, compact: bool) {
let rate = clock.timebase.sr.get();
let chunk = clock.chunk.load(Relaxed) as f64;
let lat = chunk / rate * 1000.;
let delta = |start: &Moment|clock.global.usec.get() - start.usec.get();
let mut cache = cache.try_write().unwrap();
cache.buf.update(
Some(chunk), rewrite!(buf, "{chunk}")
);
cache.lat.update(
Some(lat), rewrite!(buf, "{lat:.1}ms")
);
cache.sr.update(
Some((compact, rate)), |buf: &mut String,_,_|{
buf.clear();
if compact {
write!(buf, "{:.1}kHz", rate / 1000.)
} else {
write!(buf, "{:.0}Hz", rate)
}
}
);
if let Some(now) = clock.started.try_read().unwrap().as_ref().map(delta) {
let pulse = clock.timebase.usecs_to_pulse(now);
let time = now/1000000.;
let bpm = clock.timebase.bpm.get();
cache.beat.update(Some(pulse), |buf, _, _|{
buf.clear();
clock.timebase.format_beats_1_to(buf, pulse)
});
cache.time.update(Some(time), rewrite!(buf, "{:.3}s", time));
cache.bpm.update(Some(bpm), rewrite!(buf, "{:.3}", bpm));
} else {
cache.beat.update(None, rewrite!(buf, "{}", ClockView::BEAT_EMPTY));
cache.time.update(None, rewrite!(buf, "{}", ClockView::TIME_EMPTY));
cache.bpm.update(None, rewrite!(buf, "{}", ClockView::BPM_EMPTY));
}
}
}

View file

@ -7,13 +7,10 @@ use notify_debouncer_full::notify::{RecursiveMode, PollWatcher, Config as Notify
/// ``` /// ```
/// use ::{std::sync::Arc, tek::{Config, Modes, Mode}}; /// use ::{std::sync::Arc, tek::{Config, Modes, Mode}};
/// let config: Config = Default::default(); /// let config: Config = Default::default();
/// Modes::add(&config.modes, stringify!( /// let mode = stringify!(mode :menu (name Menu) (info Mode selector.) (keys Y Confirm)
/// (mode :menu (name Menu) /// (view (bg (g 0) (bsp/s PortsOut (bsp/n PortsIn
/// (info Mode selector.) (keys :axis/y :confirm) /// (bg (g 30) (bsp/s (exact/y 7 Logo) (fill Dialog))))))));
/// (view (bg (g 0) (bsp/s :ports/out /// Modes::add(&config.modes, mode).unwrap();
/// (bsp/n :ports/in
/// (bg (g 30) (bsp/s (fixed/y 7 :logo)
/// (fill :dialog/menu)))))))))).unwrap();
/// let mode: Arc<Mode> = config.get_mode(":menu").unwrap(); /// let mode: Arc<Mode> = config.get_mode(":menu").unwrap();
/// ``` /// ```
#[derive(Default, Debug)] #[derive(Default, Debug)]
@ -60,7 +57,7 @@ impl Config {
if let Some(expr) = src.expr()? { if let Some(expr) = src.expr()? {
match expr.head()? { match expr.head()? {
Some("mode") => Modes::add(&config.as_ref().modes, expr.tail()), Some("mode") => Modes::add(&config.as_ref().modes, expr.tail()),
Some("keys") => Binds::add(&config.as_ref().binds, expr.tail()), Some("keys") => config.as_ref().binds.add(expr.tail()),
Some("view") => Views::add(&config.as_ref().views, expr.tail()), Some("view") => Views::add(&config.as_ref().views, expr.tail()),
_ => return Err(format!("Config::load: expected view/keys/mode expr, got: {src:?}").into()) _ => return Err(format!("Config::load: expected view/keys/mode expr, got: {src:?}").into())
}?; }?;

View file

@ -8,49 +8,13 @@ pub struct Binds(pub Arc<RwLock<BTreeMap<
>>>); >>>);
impl Binds { impl Binds {
pub fn add <'a> (binds: &Binds, expr: impl Language) -> UsuallyRef<'a, ()> { pub fn add <'a> (&self, expr: impl Language) -> UsuallyRef<'a, ()> {
let name = expr.head()?.ok_or("bind: missing name")?; let name = expr.head()?.ok_or("bind: missing name")?;
let body = expr.tail()?.unwrap_or(""); let body = expr.tail()?.unwrap_or("");
binds.0.try_write().unwrap().insert(name.into(), { self.0.try_write().unwrap().insert(name.into(), Bind::load(body)?.into());
let mut map: Bind<TuiEvent, Arc<str>> = Bind::new();
body.each((), |_, item: &str|if matches!(item.expr().head(), Ok(Some("see"))) {
Binds::add_ref(&mut map, item)
} else if let Ok(Some(_word)) = item.expr().head().word() {
Binds::add_def(&mut map, item)
} else {
return err!("Binds::add: unexpected: {item:?}")
})?;
map.into()
});
Ok(()) Ok(())
} }
fn add_ref (_map: &mut Bind<TuiEvent, Arc<str>>, _item: &str) -> Usually<()> {
// TODO
Ok(())
}
fn add_def (map: &mut Bind<TuiEvent, Arc<str>>, item: &str) -> Usually<()> {
let expr = item.expr()?;
let head = expr.head()?;
if let Some(event) = TuiKey::from_dsl(&head)?.to_crossterm() {
let mut commands: Vec<Arc<str>> = vec![];
item.expr()?.tail()?.each((), |_, def|Ok(commands.push(def.into())))?;
map.add(TuiEvent(event), Binding {
commands: commands.into(),
condition: None,
description: None,
source: None
});
Ok(())
} else if Some(":char") == item.expr()?.head()? {
// TODO
Ok(())
} else {
err!("Binds::add: invalid key: {:?}", item.expr()?.head()?)
}
}
pub fn clear (&self) { pub fn clear (&self) {
*self.0.write() = Default::default(); *self.0.write() = Default::default();
} }
@ -86,7 +50,7 @@ pub struct Bind<E, C>(
pub BTreeMap<E, Vec<Binding<C>>> pub BTreeMap<E, Vec<Binding<C>>>
); );
/// A sequence of zero or more commands (e.g. [AppCommand]), /// A sequence of zero or more commands (e.g. [AppCmd]),
/// optionally filtered by [Condition] to form layers. /// optionally filtered by [Condition] to form layers.
/// ///
/// ``` /// ```
@ -124,7 +88,48 @@ impl<C: Default> Default for Binding<C> {
} }
} }
impl Bind<TuiEvent, Arc<str>> {
pub fn load (body: impl Language) -> Usually<Self> {
let mut bind: Bind<TuiEvent, Arc<str>> = Bind::new();
body.each((), |_, item: &str|if matches!(item.expr().head(), Ok(Some("see"))) {
bind.add_ref(item)
} else if let Ok(Some(_word)) = item.expr().head().word() {
bind.add_def(item)
} else {
return err!("Binds::add: unexpected: {item:?}")
})?;
Ok(bind)
}
fn add_ref (&mut self, _item: &str) -> Usually<()> {
// TODO
Ok(())
}
fn add_def (&mut self, item: &str) -> Usually<()> {
let expr = item.expr()?;
let head = expr.head()?;
if let Some(event) = TuiKey::from_dsl(&head)?.to_crossterm() {
let mut commands: Vec<Arc<str>> = vec![];
item.expr()?.tail()?.each((), |_, def|Ok(commands.push(def.into())))?;
self.add(TuiEvent(event), Binding {
commands: commands.into(),
condition: None,
description: None,
source: None
});
Ok(())
} else if Some(":char") == item.expr()?.head()? {
// TODO
Ok(())
} else {
err!("Binds::add: invalid key: {:?}", item.expr()?.head()?)
}
}
}
impl<E: Clone + Ord, C> Bind<E, C> { impl<E: Clone + Ord, C> Bind<E, C> {
/// Create a new event map /// Create a new event map
pub fn new () -> Self { pub fn new () -> Self {
Default::default() Default::default()
@ -156,5 +161,5 @@ impl<E: Clone + Ord, C> Bind<E, C> {
impl_debug!(Condition |self, w| { write!(w, "*") }); impl_debug!(Condition |self, w| { write!(w, "*") });
//take!(DeviceCommand|state: Project, iter|state.selected_device().as_ref() //take!(DeviceCmd|state: Project, iter|state.selected_device().as_ref()
//.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten()));

View file

@ -5,7 +5,7 @@ use crate::*;
/// ``` /// ```
/// let mut modes = tek::Modes::default(); /// let mut modes = tek::Modes::default();
/// assert_eq!(modes.len(), 0); /// assert_eq!(modes.len(), 0);
/// let _ = modes.add(&":foo", &"(mode (name Foo) (info Bar))"); /// let _ = modes.add(&"mode Foo (name Foo) (info Bar)").unwrap();
/// assert_eq!(modes.len(), 1); /// assert_eq!(modes.len(), 1);
/// ``` /// ```
#[derive(Default, Debug, Clone)] #[derive(Default, Debug, Clone)]
@ -22,7 +22,7 @@ impl Modes {
let body = expr.tail()?.ok_or("mode: missing body")?; let body = expr.tail()?.ok_or("mode: missing body")?;
let mode = Mode::default(); let mode = Mode::default();
let mode = body.each(mode, |c, s|Mode::add(c, s))?; let mode = body.each(mode, |c, s|Mode::add(c, s))?;
self.0.try_write().unwrap().insert(name.into(), Arc::new(mode)); self.0.write().insert(name.into(), Arc::new(mode));
Ok(()) Ok(())
} }
@ -33,14 +33,14 @@ impl Modes {
/// Run something for each mode. /// Run something for each mode.
pub fn for_each <T> (&self, mut ator: impl FnMut(&str, &Mode)->T) { pub fn for_each <T> (&self, mut ator: impl FnMut(&str, &Mode)->T) {
for (k, v) in self.0.try_read().unwrap().iter() { for (k, v) in self.0.read().iter() {
let _ = ator(k.as_ref(), v.as_ref()); let _ = ator(k.as_ref(), v.as_ref());
} }
} }
/// Count modes. /// Count modes.
pub fn len (&self) -> usize { pub fn len (&self) -> usize {
self.0.try_read().unwrap().len() self.0.read().len()
} }
} }
@ -86,7 +86,7 @@ impl Mode {
let submode = Mode::default(); let submode = Mode::default();
let submode = body.each(submode, |c,s|Mode::add(c,s))?; let submode = body.each(submode, |c,s|Mode::add(c,s))?;
let modes = mode.as_mut().modes.clone(); let modes = mode.as_mut().modes.clone();
modes.0.try_write().unwrap().insert(name.into(), Arc::new(submode)); modes.0.write().insert(name.into(), Arc::new(submode));
mode mode
}, },
"keys" => { "keys" => {
@ -123,40 +123,9 @@ pub trait HasModeStack {
impl HasModeStack for App { impl HasModeStack for App {
fn modes (&self) -> RwLockReadGuard<'_, Vec<Arc<str>>> { fn modes (&self) -> RwLockReadGuard<'_, Vec<Arc<str>>> {
self.modes.try_read().unwrap() self.modes.read()
} }
fn modes_mut (&mut self) -> RwLockWriteGuard<'_, Vec<Arc<str>>> { fn modes_mut (&mut self) -> RwLockWriteGuard<'_, Vec<Arc<str>>> {
self.modes.try_write().unwrap() self.modes.write()
}
}
impl<T> ModeController for T where T: HasModeStack
+ Namespace<Vec<Arc<str>>>
+ Namespace<Arc<str>>
+ Namespace<usize>
{}
#[tek_proc::commands(ModeCommand)]
pub trait ModeController: HasModeStack
+ Namespace<Vec<Arc<str>>>
+ Namespace<Arc<str>>
+ Namespace<usize>
{
#[command(Reset = "reset")]
fn mode_reset (&mut self, modes: Vec<Arc<str>>) -> Perhaps<ModeCommand> {
*self.modes_mut() = modes.into();
Ok(None)
}
#[command(Push = "push")]
fn mode_push (&mut self, mode: Arc<str>) -> Perhaps<ModeCommand> {
self.modes_mut().push(mode);
Ok(Some(ModeCommand::Pop))
}
#[command(Pop = "pop")]
fn mode_pop (&mut self) -> Perhaps<ModeCommand> {
let mode = self.modes_mut().pop();
Ok(mode.map(|mode|ModeCommand::Push { mode }))
} }
} }

View file

@ -155,18 +155,6 @@ impl Draw<'_, Tui> for Lv2 {
break break
} }
} }
draw_header(self, to, x, y, width); draw_lv2_header(self, to, x, y, width);
} }
} }
fn draw_header (state: &Lv2, to: &mut Tui, x: u16, y: u16, w: u16) {
let style = Style::default().gray();
let label1 = format!(" {}", state.name);
to.blit(&label1, x + 1, y, Some(style.white().bold()));
if let Some(ref path) = state.path {
let label2 = format!("{}…", &path[..((w as usize - 10).min(path.len()))]);
to.blit(&label2, x + 2 + label1.len() as u16, y, Some(style.not_dim()));
}
//Ok(Rect { x, y, width: w, height: 1 })
}

View file

@ -1,188 +0,0 @@
use crate::*;
pub fn get_dialog <'a> (_: &App, src: impl Language) -> Perhaps<Dialog> {
eval_word(&src, |word|Ok(match word {
":none" => Dialog::None,
":options" => Dialog::Options,
":device" => Dialog::Device(0),
":device/prev" => Dialog::Device(0),
":device/next" => Dialog::Device(0),
":help" => Dialog::Help(0),
":save" => Dialog::Browse(
BrowseTarget::SaveProject,
Browse::new(None).unwrap().into()),
":load" => Dialog::Browse(
BrowseTarget::LoadProject,
Browse::new(None).unwrap().into()),
":import/clip" => Dialog::Browse(
BrowseTarget::ImportClip(Default::default()),
Browse::new(None).unwrap().into()),
":export/clip" => Dialog::Browse(
BrowseTarget::ExportClip(Default::default()),
Browse::new(None).unwrap().into()),
":import/sample" => Dialog::Browse(
BrowseTarget::ImportSample(Default::default()),
Browse::new(None).unwrap().into()),
":export/sample" => Dialog::Browse(
BrowseTarget::ExportSample(Default::default()),
Browse::new(None).unwrap().into()),
_ => return Err(format!("not a dialog: {}", src.src()?.unwrap_or("<???>")).into())
}))
}
/// Various possible dialog modes.
///
/// ```
/// let dialog: tek::Dialog = Default::default();
/// ```
#[derive(Debug, Clone, Default, PartialEq)] pub enum Dialog {
#[default] None,
Help(usize),
Menu(usize, Arc<[MenuItem]>),
Device(usize),
Message(Arc<str>),
Browse(BrowseTarget, Arc<Browse>),
Options,
}
impl Dialog {
/// ```
/// let _ = tek::Dialog::welcome();
/// ```
pub fn welcome () -> Self {
Self::Menu(1, Arc::new([
MenuItem::new(" Resume session ", [
AppCommand::Project {
command: ProjectCommand::LoadLast
}
]),
MenuItem::new(" Create session ", [
AppCommand::Dialog {
dialog: Dialog::None
},
AppCommand::Mode {
command: ModeCommand::Reset {
modes: vec!["Arranger".into()]
}
},
AppCommand::Tracks {
command: TracksCommand::Add
},
AppCommand::Scenes {
command: ScenesCommand::Add
}
]),
MenuItem::new(" Load session ", [
AppCommand::Dialog {
dialog: Dialog::Browse(
BrowseTarget::LoadProject,
Browse::new(None).unwrap().into()
)
//dialog: Dialog::Browse {
//title: "Load session".into(),
//filter: "\\.tek$",
//}
}
]),
MenuItem::new(" Exit ", [
AppCommand::Exit
]),
]))
}
/// FIXME: generalize
/// ```
/// let _ = tek::Dialog::welcome().menu_selected();
/// ```
pub fn menu_selected (&self) -> Option<usize> {
if let Self::Menu(selected, _) = self { Some(*selected) } else { None }
}
/// FIXME: generalize
/// ```
/// let _ = tek::Dialog::welcome().menu_next();
/// ```
pub fn menu_next (&self) -> Self {
match self {
Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.len()), items.clone()),
_ => Self::None
}
}
/// FIXME: generalize
/// ```
/// let _ = tek::Dialog::welcome().menu_prev();
/// ```
pub fn menu_prev (&self) -> Self {
match self {
Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.len()), items.clone()),
_ => Self::None
}
}
/// FIXME: generalize
/// ```
/// let _ = tek::Dialog::welcome().device_kind();
/// ```
pub fn device_kind (&self) -> Option<usize> {
if let Self::Device(index) = self { Some(*index) } else { None }
}
/// FIXME: generalize
/// ```
/// let _ = tek::Dialog::welcome().device_kind_next();
/// ```
pub fn device_kind_next (&self) -> Option<usize> {
self.device_kind().map(|index|(index + 1) % device_kinds().len())
}
/// FIXME: generalize
/// ```
/// let _ = tek::Dialog::welcome().device_kind_prev();
/// ```
pub fn device_kind_prev (&self) -> Option<usize> {
self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1)))
}
/// FIXME: implement
pub fn message (&self) -> Option<&str> { todo!() }
/// FIXME: implement
pub fn browser (&self) -> Option<&Arc<Browse>> { todo!() }
/// FIXME: implement
pub fn browser_target (&self) -> Option<&BrowseTarget> { todo!() }
}
/// Increment a wrapping counter.
pub const fn wrap_inc (index: usize, count: usize) -> usize {
if count > 0 { (index + 1) % count } else { 0 }
}
/// Decrement a wrapping counter.
pub const fn wrap_dec (index: usize, count: usize) -> usize {
if count > 0 {
let a = index.overflowing_sub(1).0;
let b = count.saturating_sub(1);
if a < b { a } else { b }
} else {
0
}
}

View file

@ -1,81 +0,0 @@
use crate::*;
pub fn field_h <'a, T: Screen> (
_theme: ItemTheme, _head: impl Draw<T>, _body: impl Draw<T>
) -> impl Draw<T> {
}
pub fn field_v <'a, T: Screen> (
_theme: ItemTheme, _head: impl Draw<T>, _body: impl Draw<T>
) -> impl Draw<T> {
}
pub fn view_sessions <'a> () -> impl Draw<Tui> {
let h = 6;
let w = Some(30);
let f = Rgb(224, 192, 128);
draw(move |to: &mut Tui|{
for (index, name) in ["session1", "session2", "session3"].iter().enumerate() {
let b = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) };
let y = (2 * index) as u16;
let h = 2;
bg(b, fg(f, *name).align_w()).full_w().exact_h(h).push_y(y).draw(to)?;
}
Ok(Some(to.area().into()))
}).min_w(w).exact_h(h)
}
/// ```
/// 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 <'a> (bg: Color, fg: Color, content: impl Draw<Tui>)
-> impl Draw<Tui>
{
let left = fg_bg(bg, Reset, y_repeat("▐").exact_w(1));
let right = fg_bg(bg, Reset, y_repeat("▌").exact_w(1));
east(left, west(right, fg_bg(fg, bg, content)))
}
/// ```
/// let _ = tek::button_2("", "", true);
/// let _ = tek::button_2("", "", false);
/// ```
pub fn button_2 <'a> (key: impl Draw<Tui>, label: impl Draw<Tui>, hide: bool)
-> impl Draw<Tui>
{
let c1 = tui_orange();
let c2 = tui_g(0);
let c3 = tui_g(96);
let c4 = tui_g(255);
bold(true, fg_bg(c1, c2,
bar(Split::East, 1u16, fg(c2, "▐"),
bar(Split::East, 1u16, key,
bar(Split::East, 1u16, fg(c3, "▐"),
when(!hide, fg_bg(c4, c3, label)))))))
}
/// ```
/// let _ = tek::button_3("", "", "", true);
/// let _ = tek::button_3("", "", "", false);
/// ```
pub fn button_3 <'a> (
key: impl Draw<Tui>,
label: impl Draw<Tui>,
value: impl Draw<Tui>,
editing: bool,
) -> impl Draw<Tui> {
let c1 = tui_orange();
let c2 = tui_g(0);
let c3 = tui_g(96);
let c4 = tui_g(255);
let c5 = tui_g(128);
bold(true, bar(Split::East, 3u16,
fg_bg(c1, c2,
bar(Split::East, 1u16, fg(c2, "▐"),
bar(Split::East, 1u16, key, fg(if editing { c5 } else { c3 }, "▐")))),
east(
when(!editing, east(fg_bg(c4, c3, label), fg_bg(c5, c3, "▐"),)),
east(fg_bg(tui_g(224), c5, value), fg_bg(c5, Reset, "▌"), ))))
}

View file

@ -1,54 +0,0 @@
#![allow(unused)]
use crate::*;
#[macro_export] macro_rules! rewrite {
($buf:ident, $($rest:tt)*) => {
|$buf,_,_|{
$buf.clear();
write!($buf, $($rest)*)
}
};
}
pub struct Memo<T, U> {
value: T,
view: U,
}
impl<T, U> Memo<T, U> {
pub fn new (value: T, view: U) -> Self {
Self { value, view }
}
}
impl<T: PartialEq + Debug, U: Debug> Debug for Memo<T, U> {
fn fmt (&self, _: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
todo!()
}
}
impl<T: PartialEq, U> Memo<T, U> {
pub fn update <R> (&mut self, newval: T, render: impl Fn(&mut U, &T, &T)->R) -> Option<R> {
if newval != self.value {
let result = render(&mut self.view, &newval, &self.value);
self.value = newval;
return Some(result);
}
None
}
}
pub trait Gettable<T> {
/// Returns current value
fn get (&self) -> T;
}
pub trait Mutable<T>: Gettable<T> {
/// Sets new value, returns old
fn set (&mut self, value: T) -> T;
}
pub trait InteriorMutable<T>: Gettable<T> {
/// Sets new value, returns old
fn set (&self, value: T) -> T;
}

View file

@ -1,33 +0,0 @@
use crate::*;
/// An item of a menu.
///
/// ```
/// let item: tek::MenuItem = Default::default();
/// ```
#[derive(Clone)] pub struct MenuItem(
pub Arc<str>,
pub Arc<[AppCommand]>,
);
impl MenuItem {
pub fn new <const N: usize> (name: impl AsRef<str>, actions: [AppCommand; N]) -> Self {
Self(name.as_ref().into(), Arc::new(actions))
}
}
impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) });
impl_default!(MenuItem: Self::new("", []));
impl PartialEq for MenuItem {
fn eq (&self, other: &Self) -> bool {
self.0 == other.0
}
}
//impl AsRef<Arc<[MenuItem]>> for MenuItems {
//fn as_ref (&self) -> &Arc<[MenuItem]> {
//&self.0
//}
//}

View file

@ -1,115 +0,0 @@
#![allow(unused)]
use crate::*;
#[derive(Debug, Default)] pub enum MeteringMode {
#[default] Rms,
Log10,
}
#[derive(Debug, Default, Clone)]
pub struct Log10Meter(pub f32);
#[derive(Debug, Default, Clone)]
pub struct RmsMeter(pub f32);
impl_draw!(|self: RmsMeter, to: Tui|{
let XYWH(x, y, w, h) = to.area().into();
let signal = f32::max(0.0, f32::min(100.0, self.0.abs()));
let v = (signal * h as f32).ceil() as u16;
let y2 = y + h;
//to.blit(&format!("\r{v} {} {signal}", self.0), x * 30, y, Some(Style::default()));
for y in y..(y + v) {
for x in x..(x + w) {
to.blit(&"▌", x, y2.saturating_sub(y), Some(Style::default().green()));
}
}
Ok(Some(to.area().into()))
});
impl_draw!(|self: Log10Meter, to: Tui| {
let XYWH(x, y, w, h) = to.area().into();
let signal = 100.0 - f32::max(0.0, f32::min(100.0, self.0.abs()));
let v = (signal * h as f32 / 100.0).ceil() as u16;
let y2 = y + h;
//to.blit(&format!("\r{v} {} {signal}", self.0), x * 20, y, None);
for y in y..(y + v) {
for x in x..(x + w) {
to.blit(&"▌", x, y2 - y, Some(Style::default().green()));
}
}
Ok(Some(to.area().into()))
});
fn draw_meters (meters: &[f32]) -> impl Draw<Tui> + use<'_> {
bg(Black, iter_east_fixed(1, ||meters.iter(), |value, _index|{
RmsMeter(*value).full_h()
}).exact_w(2))
}
pub fn to_log10 (samples: &[f32]) -> f32 {
let total: f32 = samples.iter().map(|x|x.abs()).sum();
let count = samples.len() as f32;
10. * (total / count).log10()
}
pub fn to_rms (samples: &[f32]) -> f32 {
let sum = samples.iter()
.map(|s|*s)
.reduce(|sum, sample|sum + sample.abs())
.unwrap_or(0.0);
(sum / samples.len() as f32).sqrt()
}
/// ```
/// 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>
{
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, bg(c, ()).exact_wh(w, 1))
}
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)
}
#[cfg(test)] mod test_view_meter {
use super::*;
use proptest::prelude::*;
proptest! {
#[test] fn proptest_view_meter (
label in "\\PC*", value in f32::MIN..f32::MAX
) {
let _ = view_meter(&label, value);
}
#[test] fn proptest_view_meters (
value1 in f32::MIN..f32::MAX,
value2 in f32::MIN..f32::MAX
) {
let _ = view_meters(&[value1, value2]);
}
}
}

View file

@ -131,50 +131,6 @@ pub type MidiData = Vec<Vec<MidiMessage>>;
impl PartialEq for MidiClip { fn eq (&self, other: &Self) -> bool { self.uuid == other.uuid } } impl PartialEq for MidiClip { fn eq (&self, other: &Self) -> bool { self.uuid == other.uuid } }
impl Eq for MidiClip {} impl Eq for MidiClip {}
impl HasMidiClip for Project {
fn clip (&self) -> Option<Arc<RwLock<MidiClip>>> { None }
}
impl HasMidiClip for App {
fn clip (&self) -> Option<Arc<RwLock<MidiClip>>> { self.project.clip() }
}
impl<T: HasMidiClip
+ Namespace<Option<bool>>
+ Namespace<Option<ItemTheme>>
> MidiClipController for T {}
#[tek_proc::commands(MidiClipCommand)]
pub trait MidiClipController: HasMidiClip
+ Namespace<Option<bool>>
+ Namespace<Option<ItemTheme>>
{
/// Create a clip at the current position if it does not already exist.
#[command(Upsert = "upsert")]
fn clip_upsert (&mut self) -> Perhaps<MidiClipCommand> {
let clip = self.clip();
//let old = self.scenes[scene].clips[track].clone();
//self.scenes[scene].clips[track] = clip;
//old
todo!()
}
#[command(SetColor = "color")]
fn clip_set_color (&mut self, color: Option<ItemTheme>) -> Perhaps<MidiClipCommand> {
Ok(self.clip().map(|clip|MidiClipCommand::SetColor {
color: Some(clip.write().set_color(color))
}))
}
/// Toggle looping for the active clip
#[command(SetLoop = "loop")]
fn clip_toggle_loop (&mut self, looping: Option<bool>) -> Perhaps<MidiClipCommand> {
Ok(self.clip().map(|clip|MidiClipCommand::SetLoop {
looping: Some(clip.write().toggle_loop(looping))
}))
}
}
impl HasClipsSize for App { fn clips_size (&self) -> &Sizer { &self.project.size_inner } } impl HasClipsSize for App { fn clips_size (&self) -> &Sizer { &self.project.size_inner } }
impl HasClipsSize for Project { fn clips_size (&self) -> &Sizer { &self.size_inner } } impl HasClipsSize for Project { fn clips_size (&self) -> &Sizer { &self.size_inner } }

View file

@ -1 +0,0 @@
use crate::*;

View file

@ -19,140 +19,17 @@ pub trait HasEditor: AsRef<Option<MidiEditor>> + AsMut<Option<MidiEditor>> {
} }
} }
impl<T> HasEditor for T where T: impl<T> HasEditor for T where T: AsRef<Option<MidiEditor>> + AsMut<Option<MidiEditor>> {}
AsRef<Option<MidiEditor>> +
AsMut<Option<MidiEditor>> {}
impl<T: HasEditor
+ Namespace<u32>
+ Namespace<f64>
+ Namespace<bool>
+ Namespace<usize>
+ Namespace<Option<u32>>
+ Namespace<Option<Arc<RwLock<MidiClip>>>>
> MidiEditController for T {}
#[tek_proc::commands(MidiEditCommand)]
pub trait MidiEditController: HasEditor
+ Namespace<u32>
+ Namespace<f64>
+ Namespace<bool>
+ Namespace<usize>
+ Namespace<Option<u32>>
+ Namespace<Option<Arc<RwLock<MidiClip>>>>
{
#[command(Toggle = "toggle")]
fn edit_toggle (&mut self) -> Perhaps<MidiEditCommand> {
if self.is_editing() {
*self.editor_mut() = None;
} else {
todo!()
}
Ok(Some(MidiEditCommand::Toggle))
}
#[command(Show = "show")]
fn edit_show (&mut self, clip: Option<Arc<RwLock<MidiClip>>>) -> Perhaps<MidiEditCommand> {
Ok(self.editor_mut().as_mut().map(|mut editor|{
editor.set_clip(clip.as_ref());
editor.redraw();
None
}).flatten())
}
#[command(DeleteNote = "delete")]
fn edit_note_delete (&mut self) -> Perhaps<MidiEditCommand> {
Ok(self.editor_mut().as_mut().map(|editor|{
editor.redraw();
todo!()
}).flatten())
}
#[command(AppendNote = "append")]
fn edit_note_append (&mut self, advance: bool) -> Perhaps<MidiEditCommand> {
Ok(self.editor_mut().as_mut().map(|mut editor|{
editor.put_note(advance);
editor.redraw();
None
}).flatten())
}
#[command(SetNotePos = "note-pos")]
fn edit_note_set_pos (&mut self, pos: usize) -> Perhaps<MidiEditCommand> {
Ok(self.editor_mut().as_mut().map(|editor|{
editor.set_note_pos((pos).min(127));
editor.redraw();
None
}).flatten())
}
#[command(SetNoteLen = "note-len")]
fn edit_note_set_len (&mut self, len: usize) -> Perhaps<MidiEditCommand> {
Ok(self.editor_mut().as_mut().map(|editor|{
editor.set_note_len(len);
editor.redraw();
None
}).flatten())
}
#[command(SetNoteScroll = "note-scroll")]
fn edit_note_set_scroll (&mut self, scroll: usize) -> Perhaps<MidiEditCommand> {
Ok(self.editor_mut().as_mut().map(|editor|{
editor.set_note_lo((scroll).min(127));
editor.redraw();
None
}).flatten())
}
#[command(SetTimePos = "time-pos")]
fn edit_time_set_pos (&mut self, pos: usize) -> Perhaps<MidiEditCommand> {
Ok(self.editor_mut().as_mut().map(|editor|{
editor.set_time_pos(pos);
editor.redraw();
None
}).flatten())
}
#[command(SetTimeScroll = "time-scroll")]
fn edit_time_set_scroll (&mut self, scroll: usize) -> Perhaps<MidiEditCommand> {
Ok(self.editor_mut().as_mut().map(|editor|{
editor.set_time_start(scroll);
editor.redraw();
None
}).flatten())
}
#[command(SetTimeZoom = "time-zoom")]
fn edit_time_set_zoom (&mut self, zoom: usize) -> Perhaps<MidiEditCommand> {
Ok(self.editor_mut().as_mut().map(|editor|{
editor.set_time_zoom(zoom);
editor.redraw();
None
}).flatten())
}
#[command(SetTimeLock = "time-lock")]
fn edit_time_set_lock (&mut self, lock: bool) -> Perhaps<MidiEditCommand> {
Ok(self.editor_mut().as_mut().map(|editor|{
editor.set_time_lock(lock);
editor.redraw();
None
}).flatten())
}
// TODO: 1-9 seek markers that by default start every 8th of the clip
}
/// Contains state for viewing and editing a clip. /// Contains state for viewing and editing a clip.
/// ///
/// ``` /// ```
/// use std::sync::{Arc, RwLock}; /// use std::sync::Arc;
/// use parking_lot::RwLock;
/// let clip = tek::MidiClip::stop_all(); /// let clip = tek::MidiClip::stop_all();
/// let mut editor = tek::MidiEditor { /// let mode = tek::PianoHorizontal::new(Some(&Arc::new(RwLock::new(clip))));
/// mode: tek::PianoHorizontal::new(Some(&Arc::new(RwLock::new(clip)))), /// let mut editor = tek::MidiEditor { mode, size: Default::default() };
/// size: Default::default(), ///
/// //keys: Default::default(),
/// };
/// let _ = editor.put_note(true); /// let _ = editor.put_note(true);
/// let _ = editor.put_note(false); /// let _ = editor.put_note(false);
/// let _ = editor.clip_status(); /// let _ = editor.clip_status();
@ -275,8 +152,8 @@ impl MidiEditor {
/// use tek::{*, tengri::*}; /// use tek::{*, tengri::*};
/// ///
/// struct Test(Option<MidiEditor>); /// struct Test(Option<MidiEditor>);
/// impl_as_ref_opt!(MidiEditor: |self: Test|self.0.as_ref()); /// impl_as_ref!(Option<MidiEditor>: |self: Test|&self.0);
/// impl_as_mut_opt!(MidiEditor: |self: Test|self.0.as_mut()); /// impl_as_mut!(Option<MidiEditor>: |self: Test|&mut self.0);
/// ///
/// let mut host = Test(Some(MidiEditor::default())); /// let mut host = Test(Some(MidiEditor::default()));
/// let _ = host.editor(); /// let _ = host.editor();
@ -427,7 +304,7 @@ impl App {
&& slot.is_none() && slot.is_none()
&& let Some(track) = self.project.tracks.get_mut(track) && let Some(track) = self.project.tracks.get_mut(track)
{ {
let (_index, clip) = self.pool.add_new_clip(); let (_index, clip) = self.project.pool.add_new_clip();
// autocolor: new clip colors from scene and track color // autocolor: new clip colors from scene and track color
let color = track.color.base.mix(scene.color.base, 0.5); let color = track.color.base.mix(scene.color.base, 0.5);
clip.try_write().unwrap().color = ItemColor::random_near(color, 0.2).into(); clip.try_write().unwrap().color = ItemColor::random_near(color, 0.2).into();
@ -450,7 +327,7 @@ impl App {
std::mem::swap(&mut swapped, slot); std::mem::swap(&mut swapped, slot);
} }
if let Some(clip) = swapped { if let Some(clip) = swapped {
self.pool.delete_clip(&clip.try_read().unwrap()); self.project.pool.delete_clip(&clip.try_read().unwrap());
} }
} }
} }

View file

@ -18,7 +18,7 @@ pub trait HasSequencer: AsRef<Sequencer> + AsMut<Sequencer> {
/// ///
/// let mut clip = tek::MidiClip::new("clip", true, 1, None, None); /// let mut clip = tek::MidiClip::new("clip", true, 1, None, None);
/// clip.set_length(96); /// clip.set_length(96);
/// clip.toggle_loop(); /// clip.toggle_loop(None);
/// clip.record_event(12, ::tengri::midly::MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }); /// clip.record_event(12, ::tengri::midly::MidiMessage::NoteOn { key: 36.into(), vel: 100.into() });
/// assert!(clip.contains_note_on(36.into(), 6, 18)); /// assert!(clip.contains_note_on(36.into(), 6, 18));
/// assert_eq!(&clip.notes, &clip.duplicate().notes); /// assert_eq!(&clip.notes, &clip.duplicate().notes);

View file

@ -1,42 +0,0 @@
use crate::*;
#[derive(Debug, Default)]
pub enum MixingMode {
#[default] Summing,
Average,
}
pub fn mix_summing <const N: usize> (
buffer: &mut [Vec<f32>], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>,
) -> bool {
let channels = buffer.len();
for index in 0..frames {
if let Some(frame) = next() {
for (channel, sample) in frame.iter().enumerate() {
let channel = channel % channels;
buffer[channel][index] += sample * gain;
}
} else {
return false
}
}
true
}
pub fn mix_average <const N: usize> (
buffer: &mut [Vec<f32>], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>,
) -> bool {
let channels = buffer.len();
for index in 0..frames {
if let Some(frame) = next() {
for (channel, sample) in frame.iter().enumerate() {
let channel = channel % channels;
let value = buffer[channel][index];
buffer[channel][index] = (value + sample * gain) / 2.0;
}
} else {
return false
}
}
true
}

View file

@ -1 +0,0 @@
use crate::*;

View file

@ -1,493 +0,0 @@
use crate::*;
/// A clip pool.
///
/// ```
/// let pool = tek::Pool::default();
/// ```
#[derive(Debug)] pub struct Pool {
pub visible: bool,
/// Selected clip
pub clip: AtomicUsize,
/// Mode switch
pub mode: Option<PoolMode>,
/// Embedded file browse
#[cfg(feature = "browse")] pub browse: Option<Browse>,
/// Collection of MIDI clips.
#[cfg(feature = "clip")] pub clips: Arc<RwLock<Vec<Arc<RwLock<MidiClip>>>>>,
/// Collection of sound samples.
#[cfg(feature = "sampler")] pub samples: Arc<RwLock<Vec<Arc<RwLock<Sample>>>>>,
}
impl HasClips for Pool {
fn clip (&self) -> Option<Arc<RwLock<MidiClip>>> {
self.clips.read().get(self.clip_index()).map(|c|c.clone())
}
fn clip_mut (&self) -> &mut Option<Arc<RwLock<MidiClip>>> {
todo!()
}
fn clips <'a> (&'a self) -> RwLockReadGuard<'a, Clips> {
self.clips.read()
}
fn clips_mut <'a> (&'a self) -> RwLockWriteGuard<'a, Clips> {
self.clips.write()
}
}
/// Displays and edits clip length.
#[derive(Clone, Debug, Default)] pub struct ClipLength {
/// Pulses per beat (quaver)
pub ppq: usize,
/// Beats per bar
pub bpb: usize,
/// Length of clip in pulses
pub pulses: usize,
/// Selected subdivision
pub focus: Option<ClipLengthFocus>,
}
/// Some sort of wrapper again?
pub struct PoolView<'a>(pub &'a Pool);
/// Modes for clip pool
#[derive(Debug, Clone)]
pub enum PoolMode {
/// Renaming a pattern
Rename(usize, Arc<str>),
/// Editing the length of a pattern
Length(usize, usize, ClipLengthFocus),
/// Load clip from disk
Import(usize, Browse),
/// Save clip to disk
Export(usize, Browse),
}
/// Focused field of `ClipLength`
#[derive(Copy, Clone, Debug)]
pub enum ClipLengthFocus {
/// Editing the number of bars
Bar,
/// Editing the number of beats
Beat,
/// Editing the number of ticks
Tick,
}
impl<T: AsRef<Pool> + AsMut<Pool>> HasPool for T {}
pub trait HasPool: AsRef<Pool> + AsMut<Pool> {
fn pool (&self) -> &Pool {
self.as_ref()
}
fn pool_mut (&mut self) -> &mut Pool {
self.as_mut()
}
}
impl<T: HasPool
+ Namespace<bool>
+ Namespace<usize>
+ Namespace<Arc<str>>
+ Namespace<PathBuf>
+ Namespace<MidiClip>
+ Namespace<ItemColor>
+ Namespace<BrowseCommand>
> PoolController for T {}
#[tek_proc::commands(PoolCommand = "pool")]
pub trait PoolController: HasPool
+ Namespace<bool>
+ Namespace<usize>
+ Namespace<Arc<str>>
+ Namespace<PathBuf>
+ Namespace<MidiClip>
+ Namespace<ItemColor>
+ Namespace<BrowseCommand>
{
#[command(Show = "show")]
/// Toggle visibility of pool
fn show (&mut self, visible: bool) -> Perhaps<PoolCommand> {
self.pool_mut().visible = visible;
Ok(Some(PoolCommand::Show { visible: !visible }))
}
/// Set selected clip
#[command(Select = "select")]
fn select (&mut self, index: usize) -> Perhaps<PoolCommand> {
self.pool_mut().set_clip_index(index);
Ok(None)
}
///// Import from file
//#[command(Browse = "browse")]
//fn browse (&mut self, command: BrowseCommand) -> Perhaps<PoolCommand> {
//Ok(if let Some(browse) = self.pool_mut().browse.as_mut() {
//command.act(browse)?.map(|command|PoolCommand::Browse{command})
//} else {
//None
//})
//}
/// Import clip to pool from file
#[command(Import = "import")]
fn clip_import (&mut self, index: usize, path: PathBuf) -> Perhaps<PoolCommand>
where Self: Sized
{
let bytes = std::fs::read(&path)?;
let smf = Smf::parse(bytes.as_slice())?;
let mut t = 0u32;
let mut events = vec![];
for track in smf.tracks.iter() {
for event in track.iter() {
t += event.delta.as_int();
if let TrackEventKind::Midi { channel, message } = event.kind {
events.push((t, channel.as_int(), message));
}
}
}
let mut clip = MidiClip::new("imported", true, t as usize + 1, None, None);
for event in events.iter() {
clip.notes[event.0 as usize].push(event.2);
}
Ok(PoolCommand::Add { index, clip }.dispatch(self)?)
}
///// Export to file
//#[command(Export = "export")]
//fn export (&mut self, command: BrowseCommand) -> Perhaps<PoolCommand> {
//Ok(if let Some(browse) = self.pool_mut().browse.as_mut() {
//command.act(browse)?.map(|command|PoolCommand::Export{command})
//} else {
//None
//})
//}
/// Delete a clip from the pool
#[command(Delete = "delete")]
fn delete (&mut self, index: usize) -> Perhaps<PoolCommand> {
let clip = self.pool_mut().clips_mut().remove(index).try_read().unwrap().clone();
Ok(Some(PoolCommand::Add { index, clip }))
}
/// Switch places of two clips in pool
#[command(Swap = "swap")]
fn swap (&mut self, index: usize, other: usize) -> Perhaps<PoolCommand> {
self.pool_mut().clips_mut().swap(index, other);
Ok(Some(PoolCommand::Swap { index, other }))
}
/// Add clip to pool
#[command(Add = "add")]
fn clip_add (&mut self, index: usize, clip: MidiClip) -> Perhaps<PoolCommand> {
let mut index = index;
let clip = Arc::new(RwLock::new(clip.clone()));
let mut clips = self.pool_mut().clips_mut();
if index >= clips.len() {
index = clips.len();
clips.push(clip)
} else {
clips.insert(index, clip);
}
Ok(Some(PoolCommand::Delete { index }))
}
/// Set name of clip
#[command(SetName = "set-name")]
fn clip_set_name (&mut self, index: usize, name: Arc<str>) -> Perhaps<PoolCommand> {
let clip = &mut self.pool_mut().clips_mut()[index];
let old_name = clip.try_read().unwrap().name.clone();
clip.try_write().unwrap().name = name.clone();
Ok(Some(PoolCommand::SetName { index, name: old_name }))
}
/// Set length of clip
#[command(SetLength = "set-length")]
fn clip_set_length (&mut self, index: usize, length: usize) -> Perhaps<PoolCommand> {
let clip = &mut self.pool_mut().clips_mut()[index];
let old_len = clip.try_read().unwrap().length;
clip.try_write().unwrap().length = length;
Ok(Some(PoolCommand::SetLength { index, length: old_len }))
}
/// Set color of clip
#[command(SetColor = "set-color")]
fn clip_set_color (&mut self, index: usize, color: ItemColor) -> Perhaps<PoolCommand> {
let mut color = ItemTheme::from(color);
std::mem::swap(&mut color, &mut self.pool().clips()[index].try_write().unwrap().color);
Ok(Some(PoolCommand::SetColor { index, color: color.base }))
}
/// Enter crop mode
#[command(CropBegin = "crop/begin")]
fn crop_begin (&mut self) -> Perhaps<PoolCommand> {
let index = self.pool().clip_index();
let length = self.pool().clips()[index].try_read().unwrap().length;
*self.pool_mut().mode_mut() = Some(PoolMode::Length(index, length, ClipLengthFocus::Bar));
Ok(None)
}
/// Exit crop mode, discard
#[command(CropCancel = "crop/cancel")]
fn crop_cancel (&mut self) -> Perhaps<PoolCommand> {
if let Some(PoolMode::Length(..)) = self.pool_mut().mode_mut().clone() {
*self.pool_mut().mode_mut() = None;
}
Ok(None)
}
#[command(CropSet = "crop/set")]
fn crop_set (&mut self, _length: usize) -> Perhaps<PoolCommand> {
if let Some(PoolMode::Length(clip, ref mut length, ref mut _focus))
= self.pool_mut().mode_mut().clone()
{
let old_length;
{
let clip = self.pool().clips()[clip].clone();//.try_write().unwrap();
old_length = Some(clip.try_read().unwrap().length);
clip.try_write().unwrap().length = *length;
}
*self.pool_mut().mode_mut() = None;
return Ok(old_length.map(|l|PoolCommand::CropSet { _length: l }))
}
Ok(None)
}
#[command(CropNext = "crop/next")]
fn crop_next (&mut self) -> Perhaps<PoolCommand> {
if let Some(PoolMode::Length(
_clip, ref mut _length, ref mut focus
)) = self.pool_mut().mode_mut().clone() {
focus.next()
};
Ok(None)
}
#[command(CropPrev = "crop/prev")]
fn crop_prev (&mut self) -> Perhaps<PoolCommand> {
if let Some(PoolMode::Length(
_clip, ref mut _length, ref mut focus
)) = self.pool_mut().mode_mut().clone() {
focus.prev()
};
Ok(None)
}
#[command(CropInc = "crop/inc")]
fn crop_inc (&mut self) -> Perhaps<PoolCommand> {
if let Some(PoolMode::Length(
_clip, ref mut length, ref mut focus
)) = self.pool_mut().mode_mut().clone() {
match focus {
ClipLengthFocus::Bar => { *length += 4 * PPQ },
ClipLengthFocus::Beat => { *length += PPQ },
ClipLengthFocus::Tick => { *length += 1 },
}
}
Ok(None)
}
#[command(CropDec = "crop/dec")]
fn crop_dec (&mut self) -> Perhaps<PoolCommand> {
if let Some(PoolMode::Length(
_clip, ref mut length, ref mut focus
)) = self.pool_mut().mode_mut().clone() {
match focus {
ClipLengthFocus::Bar => { *length = length.saturating_sub(4 * PPQ) },
ClipLengthFocus::Beat => { *length = length.saturating_sub(PPQ) },
ClipLengthFocus::Tick => { *length = length.saturating_sub(1) },
}
}
Ok(None)
}
/// Enter rename mode
#[command(RenameBegin = "rename/begin")]
fn rename_begin (&mut self) -> Perhaps<PoolCommand> {
let index = self.pool().clip_index();
let name = self.pool().clips()[index].try_read().unwrap().name.clone();
*self.pool_mut().mode_mut() = Some(PoolMode::Rename(index, name));
Ok(None)
}
/// Exit rename mode, discard
#[command(RenameCancel = "rename/cancel")]
fn rename_cancel (&mut self) -> Perhaps<PoolCommand> {
if let Some(PoolMode::Rename(clip, ref mut old_name)) = self.pool_mut().mode_mut().clone() {
self.pool().clips()[clip].try_write().unwrap().name = old_name.clone().into();
}
Ok(None)
}
/// Exit rename mode, update name
#[command(RenameConfirm = "rename/confirm")]
fn rename_confirm (&mut self) -> Perhaps<PoolCommand> {
Ok(if let Some(PoolMode::Rename(_clip, ref mut old_name)) = self.pool_mut().mode_mut().clone() {
let old_name = old_name.clone();
*self.pool_mut().mode_mut() = None;
Some(PoolCommand::RenameSet { value: old_name })
} else {
None
})
}
#[command(RenameSet = "rename/set")]
fn rename_set (&mut self, value: Arc<str>) -> Perhaps<PoolCommand> {
if let Some(PoolMode::Rename(clip, ref mut _old_name)) = self.pool_mut().mode_mut().clone() {
self.pool().clips()[clip].try_write().unwrap().name = value.clone();
}
Ok(None)
}
}
impl_from!(Pool: |clip:&Arc<RwLock<MidiClip>>|{
let model = Self::default();
model.clips.try_write().unwrap().push(clip.clone());
model.clip.store(1, Relaxed);
model
});
impl_default!(Pool: Self {
browse: None,
clip: 0.into(),
clips: Arc::from(RwLock::from(vec![])),
mode: None,
samples: Arc::from(RwLock::from(vec![])),
visible: true,
});
impl Pool {
pub fn clip_index (&self) -> usize {
self.clip.load(Relaxed)
}
pub fn set_clip_index (&self, value: usize) {
self.clip.store(value, Relaxed);
}
pub fn mode (&self) -> &Option<PoolMode> {
&self.mode
}
pub fn mode_mut (&mut self) -> &mut Option<PoolMode> {
&mut self.mode
}
pub fn begin_import (&mut self) -> Usually<()> {
*self.mode_mut() = Some(PoolMode::Import(
self.clip_index(),
Browse::new(None)?
));
Ok(())
}
pub fn begin_export (&mut self) -> Usually<()> {
*self.mode_mut() = Some(PoolMode::Export(
self.clip_index(),
Browse::new(None)?
));
Ok(())
}
pub fn new_clip (&self) -> MidiClip {
MidiClip::new("Clip", true, 4 * PPQ, None, Some(ItemTheme::random()))
}
pub fn cloned_clip (&self) -> MidiClip {
let index = self.clip_index();
let mut clip = self.clips()[index].try_read().unwrap().duplicate();
clip.color = ItemTheme::random_near(clip.color, 0.25);
clip
}
pub fn add_new_clip (&self) -> (usize, Arc<RwLock<MidiClip>>) {
let clip = Arc::new(RwLock::new(self.new_clip()));
let index = {
let mut clips = self.clips.try_write().unwrap();
clips.push(clip.clone());
clips.len().saturating_sub(1)
};
self.clip.store(index, Relaxed);
(index, clip)
}
pub fn delete_clip (&mut self, clip: &MidiClip) -> bool {
let index = self.clips.try_read().unwrap().iter().position(|x|*x.try_read().unwrap()==*clip);
if let Some(index) = index {
self.clips.try_write().unwrap().remove(index);
return true
}
false
}
}
impl ClipLengthFocus {
pub fn next (&mut self) {
use ClipLengthFocus::*;
*self = match self { Bar => Beat, Beat => Tick, Tick => Bar, }
}
pub fn prev (&mut self) {
use ClipLengthFocus::*;
*self = match self { Bar => Tick, Beat => Bar, Tick => Beat, }
}
}
impl ClipLength {
pub fn _new (pulses: usize, focus: Option<ClipLengthFocus>) -> Self {
Self { ppq: PPQ, bpb: 4, pulses, focus }
}
pub fn bars (&self) -> usize {
self.pulses / (self.bpb * self.ppq)
}
pub fn beats (&self) -> usize {
(self.pulses % (self.bpb * self.ppq)) / self.ppq
}
pub fn ticks (&self) -> usize {
self.pulses % self.ppq
}
}
impl Pool {
fn _clip_new (&self) -> MidiClip { self.new_clip() }
fn _clip_cloned (&self) -> MidiClip { self.cloned_clip() }
fn _clip_index_current (&self) -> usize { 0 }
fn _clip_index_after (&self) -> usize { 0 }
fn _clip_index_previous (&self) -> usize { 0 }
fn _clip_index_next (&self) -> usize { 0 }
fn _color_random (&self) -> ItemColor { ItemColor::random() }
}
impl<'a> PoolView<'a> {
//fn tui (&self) -> impl Draw<'_, Tui> {
//let Self(pool) = self;
////let color = self.1.clip().map(|c|c.try_read().unwrap().color).unwrap_or_else(||g(32).into());
////let on_bg = |x|x;//below(Repeat(" "), bg(color.darkest.term, x));
////let border = |x|x;//Outer(Style::default().fg(color.dark.term).bg(color.darkest.term)).enclose(x);
////let height = pool.clips.try_read().unwrap().len() as u16;
//iter(
//||pool.clips().clone().into_iter(),
//move|clip: Arc<RwLock<MidiClip>>, i: usize|{
//let MidiClip { ref name, color, length, .. } = *clip.try_read().unwrap();
//let item_height = 1;
//let _item_offset = i as u16 * item_height;
//let selected = i == pool.clip_index();
//let b = if selected { color.light.term } else { color.base.term };
//let f = color.lightest.term;
//let name = if false { format!(" {i:>3}") } else { format!(" {i:>3} {name}") };
//let length = if false { String::default() } else { format!("{length} ") };
//bg(b, below!(
//fg(f, bold(selected, name)).origin_w().full_w(),
//fg(f, bold(selected, length)).origin_e().full_w(),
//when(selected, bold(true, fg(g(255), "▶"))).origin_w().full_w(),
//when(selected, bold(true, fg(g(255), "◀"))).origin_e().full_w(),
//)).exact_h(1)
//}).origin_n().full_h().exact_w(20)
//}
}
impl ClipLength {
//fn tui (&self) -> impl Draw<'_, Tui> {
//use ClipLengthFocus::*;
//let bars = format!("{}", self.bars());
//let beats = format!("{}", self.beats());
//let ticks = format!("{:>02}", self.ticks());
//match self.focus {
//None => east!(" ", bars, ".", beats, ".", ticks),
//Some(Bar) => east!("[", bars, "]", beats, ".", ticks),
//Some(Beat) => east!(" ", bars, "[", beats, "]", ticks),
//Some(Tick) => east!(" ", bars, ".", beats, "[", ticks),
//}
//}
}

View file

@ -1,87 +0,0 @@
use crate::*;
def_sizes_iter!(PortsSizes => Arc<str>, [Connect]);
impl_has!(Vec<MidiInput>: |self: Project| self.midi_ins);
impl_has!(Vec<MidiOutput>: |self: Project| self.midi_outs);
impl_has!(Vec<MidiInput>: |self: App|self.project.midi_ins);
impl_has!(Vec<MidiOutput>: |self: App|self.project.midi_outs);
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_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)));
border(true, frame, field_v(theme, title, iter_south({
move||ports.iter().enumerate().map(|(index, port)|{
east!(" ", index, " ", port.port_name()).align_w().full_h()
})
})).exact_wh(20, 1 + ins)).exact_wh(20, 1 + ins)
}
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>, _| south(
bold(true, fg_bg(fg, bg, east(" 󰣲 ", name).align_w())).full_h(),
iter(||connections.iter(), move|connect: &'a Connect, index|{
bold(false, fg_bg(fg, bg, &connect.info)).exact_h(1).align_w().push_y(index as u16)
})
).exact_h((y2 - y) as u16).push_y(y as u16))
}
pub struct Junction<T: JackPort>(T);
impl<T: JackPort> Draw<Tui> for Junction<T> {
fn draw (&self, to: &mut Tui) -> Drawn<u16> {
T::KIND.draw(to)
}
}
#[tek_proc::command(AudioInput)]
#[tek_proc::keyword(AudioInput)]
#[derive(Debug)]
pub enum AudioInputCommand {
Close,
Connect(Arc<str>),
}
#[tek_proc::command(AudioOutput)]
#[tek_proc::keyword(AudioOutput)]
#[derive(Debug)]
pub enum AudioOutputCommand {
Close,
Connect(Arc<str>),
}
#[tek_proc::command(MidiInput)]
#[tek_proc::keyword(MidiInput)]
#[derive(Debug)]
pub enum MidiInputCommand {
Close,
Connect(Arc<str>),
}
#[tek_proc::command(MidiOutput)]
#[tek_proc::keyword(MidiOutput)]
#[derive(Debug)]
pub enum MidiOutputCommand {
Close,
Connect(Arc<str>),
}

View file

@ -1,38 +1,9 @@
use crate::*; use crate::*;
impl_as_ref!(Option<Project>: |self: App|todo!());
impl AsRef<Option<Project>> for App { impl_as_mut!(Option<Project>: |self: App|todo!());
fn as_ref (&self) -> &Option<Project> {
todo!()
}
}
impl AsMut<Option<Project>> for App {
fn as_mut (&mut self) -> &mut Option<Project> {
todo!()
}
}
pub trait HasProject: AsRef<Option<Project>> + AsMut<Option<Project>> {} pub trait HasProject: AsRef<Option<Project>> + AsMut<Option<Project>> {}
impl<T: AsRef<Option<Project>> + AsMut<Option<Project>>> HasProject for T {} impl<T: AsRef<Option<Project>> + AsMut<Option<Project>>> HasProject for T {}
impl<T: HasProject> ProjectController for T {}
#[tek_proc::commands(ProjectCommand)]
pub trait ProjectController: HasProject {
#[command(LoadLast = "load-last")]
fn load_last (&mut self) -> Perhaps<ProjectCommand> {
todo!()
}
#[command(Load = "load")]
fn load (&mut self) -> Perhaps<ProjectCommand> {
todo!()
}
}
/// Arranger. /// Arranger.
/// ///
/// ``` /// ```
@ -55,6 +26,8 @@ pub struct Project {
pub size_inner: Sizer, pub size_inner: Sizer,
/// Selected UI element /// Selected UI element
pub selection: Selection, pub selection: Selection,
/// Contains all recently created clips.
pub pool: Pool,
/// Source of time /// Source of time
#[cfg(feature = "clock")] pub clock: Clock, #[cfg(feature = "clock")] pub clock: Clock,
/// Allows one MIDI clip to be edited /// Allows one MIDI clip to be edited
@ -137,7 +110,6 @@ impl Project {
} }
} }
impl HasJack<'static> for Project { fn jack (&self) -> &Jack<'static> { &self.jack } }
impl_has!(Jack<'static>: |self: Project| self.jack); impl_has!(Jack<'static>: |self: Project| self.jack);
impl_has!(Sizer: |self: Project| self.size); impl_has!(Sizer: |self: Project| self.size);
impl_has!(Clock: |self: Project| self.clock); impl_has!(Clock: |self: Project| self.clock);
@ -147,6 +119,15 @@ impl_as_ref_opt!(Scene: |self: Project| self.selected_scene());
impl_as_mut_opt!(Scene: |self: Project| self.selected_scene_mut()); impl_as_mut_opt!(Scene: |self: Project| self.selected_scene_mut());
impl_as_ref!(Option<MidiEditor>: |self: Project| &self.editor); impl_as_ref!(Option<MidiEditor>: |self: Project| &self.editor);
impl_as_mut!(Option<MidiEditor>: |self: Project| &mut self.editor); impl_as_mut!(Option<MidiEditor>: |self: Project| &mut self.editor);
impl HasJack<'static> for Project {
fn jack (&self) -> &Jack<'static> { &self.jack }
}
impl HasClips for Project {
fn clip (&self) -> Option<Arc<RwLock<MidiClip>>> { todo!() }
fn clip_mut (&self) -> &mut Option<Arc<RwLock<MidiClip>>> { todo!() }
fn clips (&self) -> RwLockReadGuard<'_, Clips> { todo!() }
fn clips_mut (&self) -> RwLockWriteGuard<'_, Clips> { todo!() }
}
pub fn swap_value <T: Clone + PartialEq, U> ( pub fn swap_value <T: Clone + PartialEq, U> (
target: &mut T, value: &T, returned: impl Fn(T)->U target: &mut T, value: &T, returned: impl Fn(T)->U

View file

@ -57,82 +57,7 @@ pub trait HasClock: AsRef<Clock> + AsMut<Clock> {
} }
} }
impl<T: HasClock //impl<T: HasClock> Act<T> for ClockCmd {
+ Namespace<u32>
+ Namespace<f64>
+ Namespace<Option<u32>>
> ClockController for T {}
#[tek_proc::commands(ClockCommand = "clock")]
pub trait ClockController: HasClock
+ Namespace<u32>
+ Namespace<f64>
+ Namespace<Option<u32>>
{
#[command(SeekUsec = "usec")]
fn seek_usec (&mut self, usec: f64) -> Perhaps<ClockCommand> {
self.clock().playhead.update_from_usec(usec);
Ok(None)
}
#[command(SeekSample = "sample")]
fn seek_sample (&mut self, sample: f64) -> Perhaps<ClockCommand> {
self.clock().playhead.update_from_sample(sample);
Ok(None)
}
#[command(SeekPulse = "pulse")]
fn seek_pulse (&mut self, pulse: f64) -> Perhaps<ClockCommand> {
self.clock().playhead.update_from_pulse(pulse);
Ok(None)
}
#[command(SetBpm = "bpm")]
fn set_bpm (&mut self, bpm: f64) -> Perhaps<ClockCommand> {
Ok(Some(ClockCommand::SetBpm {
bpm: self.clock().timebase().bpm.set(bpm)
}))
}
#[command(SetQuant = "quant")]
fn set_quant (&mut self, quant: f64) -> Perhaps<ClockCommand> {
Ok(Some(ClockCommand::SetQuant {
quant: self.clock().quant.set(quant)
}))
}
#[command(SetSync = "sync")]
fn set_sync (&mut self, sync: f64) -> Perhaps<ClockCommand> {
Ok(Some(ClockCommand::SetSync {
sync: self.clock().sync.set(sync)
}))
}
#[command(Play = "play")]
fn play (&mut self, position: Option<u32>) -> Perhaps<ClockCommand> {
self.clock().play_from(position)?;
Ok(None) /* TODO Some(Pause(previousPosition)) */
}
#[command(Pause = "pause")]
fn pause (&mut self, position: Option<u32>) -> Perhaps<ClockCommand> {
self.clock().pause_at(position)?;
Ok(None)
}
#[command(TogglePlayback = "toggle")]
fn toggle (&mut self, position: u32) -> Perhaps<ClockCommand> {
Ok(if self.clock().is_rolling() {
self.clock().pause_at(Some(position))?;
None
} else {
self.clock().play_from(Some(position))?;
None
})
}
}
//impl<T: HasClock> Act<T> for ClockCommand {
//fn act (&self, state: &mut T) -> Perhaps<Self> { //fn act (&self, state: &mut T) -> Perhaps<Self> {
//self.act(state.clock_mut()) // awesome //self.act(state.clock_mut()) // awesome
//} //}
@ -449,22 +374,6 @@ impl_time_unit!(Pulse);
impl_time_unit!(Bpm); impl_time_unit!(Bpm);
impl_time_unit!(LaunchSync); impl_time_unit!(LaunchSync);
/// ```
/// let x = "";
/// let _ = tek::view_status(None, x.as_ref(), x.as_ref(), x.as_ref());
/// let _ = tek::view_status(Some("".into()), x.as_ref(), x.as_ref(), x.as_ref());
/// ```
pub fn view_status <'a> (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(
sel.map(|sel|field_h(theme, "Selected", sel)).align_w().full_wh(),
east!(sr, buf, lat).align_e().full_wh(),
)))
}
/// ``` /// ```
/// let _ = tek::button_play_pause(true, true); /// let _ = tek::button_play_pause(true, true);
/// let _ = tek::button_play_pause(true, false); /// let _ = tek::button_play_pause(true, false);
@ -485,3 +394,80 @@ pub fn button_play_pause <'a> (playing: bool, compact: bool) -> impl Draw<Tui> {
) )
) )
} }
/// Contains memoized renders of clock values.
///
/// Performance optimization.
#[derive(Debug)] pub struct ClockView {
pub sr: Memo<Option<(bool, f64)>, String>,
pub buf: Memo<Option<f64>, String>,
pub lat: Memo<Option<f64>, String>,
pub bpm: Memo<Option<f64>, String>,
pub beat: Memo<Option<f64>, String>,
pub time: Memo<Option<f64>, String>,
}
impl_default!(ClockView: {
let mut beat = String::with_capacity(16);
let _ = write!(beat, "{}", Self::BEAT_EMPTY);
let mut time = String::with_capacity(16);
let _ = write!(time, "{}", Self::TIME_EMPTY);
let mut bpm = String::with_capacity(16);
let _ = write!(bpm, "{}", Self::BPM_EMPTY);
Self {
beat: Memo::new(None, beat),
time: Memo::new(None, time),
bpm: Memo::new(None, bpm),
sr: Memo::new(None, String::with_capacity(16)),
buf: Memo::new(None, String::with_capacity(16)),
lat: Memo::new(None, String::with_capacity(16)),
}
});
impl ClockView {
pub const BEAT_EMPTY: &'static str = "-.-.--";
pub const TIME_EMPTY: &'static str = "-.---s";
pub const BPM_EMPTY: &'static str = "---.---";
pub fn update_clock (cache: &Arc<RwLock<Self>>, clock: &Clock, compact: bool) {
let rate = clock.timebase.sr.get();
let chunk = clock.chunk.load(Relaxed) as f64;
let lat = chunk / rate * 1000.;
let delta = |start: &Moment|clock.global.usec.get() - start.usec.get();
let mut cache = cache.try_write().unwrap();
cache.buf.update(
Some(chunk), rewrite!(buf, "{chunk}")
);
cache.lat.update(
Some(lat), rewrite!(buf, "{lat:.1}ms")
);
cache.sr.update(
Some((compact, rate)), |buf: &mut String,_,_|{
buf.clear();
if compact {
write!(buf, "{:.1}kHz", rate / 1000.)
} else {
write!(buf, "{:.0}Hz", rate)
}
}
);
if let Some(now) = clock.started.try_read().unwrap().as_ref().map(delta) {
let pulse = clock.timebase.usecs_to_pulse(now);
let time = now/1000000.;
let bpm = clock.timebase.bpm.get();
cache.beat.update(Some(pulse), |buf, _, _|{
buf.clear();
clock.timebase.format_beats_1_to(buf, pulse)
});
cache.time.update(Some(time), rewrite!(buf, "{:.3}s", time));
cache.bpm.update(Some(bpm), rewrite!(buf, "{:.3}", bpm));
} else {
cache.beat.update(None, rewrite!(buf, "{}", ClockView::BEAT_EMPTY));
cache.time.update(None, rewrite!(buf, "{}", ClockView::TIME_EMPTY));
cache.bpm.update(None, rewrite!(buf, "{}", ClockView::BPM_EMPTY));
}
}
}

View file

@ -99,7 +99,7 @@ fn tek_jack_event (state: &mut App, event: JackEvent) {
#[tek_proc::command(Device)] #[tek_proc::command(Device)]
#[tek_proc::keyword(Device)] #[tek_proc::keyword(Device)]
#[derive(Debug)] #[derive(Debug)]
pub enum DeviceCommand {} pub enum DeviceCmd {}
impl Device { impl Device {
pub fn name (&self) -> &str { pub fn name (&self) -> &str {
@ -198,15 +198,3 @@ pub trait HasDevices: AsRef<Vec<Device>> + AsMut<Vec<Device>> {
self.as_mut() self.as_mut()
} }
} }
pub fn view_device <'a> (state: &'a App) -> impl Draw<Tui> {
let selected = state.dialog.device_kind().unwrap();
south(
bold(true, "Add device"),
iter_south(move||device_kinds().iter().enumerate().map(move|(i, _label)|{
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 { " " };
bg(b, east(l, west(r, "FIXME device name"))).full_w()
})))
}

95
src/proj_mixer.rs Normal file
View file

@ -0,0 +1,95 @@
use crate::*;
#[derive(Debug, Default)]
pub enum MixingMode {
#[default] Summing,
Average,
}
pub fn mix_summing <const N: usize> (
buffer: &mut [Vec<f32>], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>,
) -> bool {
let channels = buffer.len();
for index in 0..frames {
if let Some(frame) = next() {
for (channel, sample) in frame.iter().enumerate() {
let channel = channel % channels;
buffer[channel][index] += sample * gain;
}
} else {
return false
}
}
true
}
pub fn mix_average <const N: usize> (
buffer: &mut [Vec<f32>], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>,
) -> bool {
let channels = buffer.len();
for index in 0..frames {
if let Some(frame) = next() {
for (channel, sample) in frame.iter().enumerate() {
let channel = channel % channels;
let value = buffer[channel][index];
buffer[channel][index] = (value + sample * gain) / 2.0;
}
} else {
return false
}
}
true
}
#[derive(Debug, Default)] pub enum MeteringMode {
#[default] Rms,
Log10,
}
#[derive(Debug, Default, Clone)]
pub struct Log10Meter(pub f32);
#[derive(Debug, Default, Clone)]
pub struct RmsMeter(pub f32);
impl_draw!(|self: RmsMeter, to: Tui|{
let XYWH(x, y, w, h) = to.area().into();
let signal = f32::max(0.0, f32::min(100.0, self.0.abs()));
let v = (signal * h as f32).ceil() as u16;
let y2 = y + h;
//to.blit(&format!("\r{v} {} {signal}", self.0), x * 30, y, Some(Style::default()));
for y in y..(y + v) {
for x in x..(x + w) {
to.blit(&"▌", x, y2.saturating_sub(y), Some(Style::default().green()));
}
}
Ok(Some(to.area().into()))
});
impl_draw!(|self: Log10Meter, to: Tui| {
let XYWH(x, y, w, h) = to.area().into();
let signal = 100.0 - f32::max(0.0, f32::min(100.0, self.0.abs()));
let v = (signal * h as f32 / 100.0).ceil() as u16;
let y2 = y + h;
//to.blit(&format!("\r{v} {} {signal}", self.0), x * 20, y, None);
for y in y..(y + v) {
for x in x..(x + w) {
to.blit(&"▌", x, y2 - y, Some(Style::default().green()));
}
}
Ok(Some(to.area().into()))
});
pub fn to_log10 (samples: &[f32]) -> f32 {
let total: f32 = samples.iter().map(|x|x.abs()).sum();
let count = samples.len() as f32;
10. * (total / count).log10()
}
pub fn to_rms (samples: &[f32]) -> f32 {
let sum = samples.iter()
.map(|s|*s)
.reduce(|sum, sample|sum + sample.abs())
.unwrap_or(0.0);
(sum / samples.len() as f32).sqrt()
}

236
src/proj_pool.rs Normal file
View file

@ -0,0 +1,236 @@
use crate::*;
/// A clip pool.
///
/// ```
/// let pool = tek::Pool::default();
/// ```
#[derive(Debug)] pub struct Pool {
pub visible: bool,
/// Selected clip
pub clip: AtomicUsize,
/// Mode switch
pub mode: Option<PoolMode>,
/// Embedded file browse
#[cfg(feature = "browse")] pub browse: Option<Browse>,
/// Collection of MIDI clips.
#[cfg(feature = "clip")] pub clips: Arc<RwLock<Vec<Arc<RwLock<MidiClip>>>>>,
/// Collection of sound samples.
#[cfg(feature = "sampler")] pub samples: Arc<RwLock<Vec<Arc<RwLock<Sample>>>>>,
}
impl HasClips for Pool {
fn clip (&self) -> Option<Arc<RwLock<MidiClip>>> {
self.clips.read().get(self.clip_index()).map(|c|c.clone())
}
fn clip_mut (&self) -> &mut Option<Arc<RwLock<MidiClip>>> {
todo!()
}
fn clips <'a> (&'a self) -> RwLockReadGuard<'a, Clips> {
self.clips.read()
}
fn clips_mut <'a> (&'a self) -> RwLockWriteGuard<'a, Clips> {
self.clips.write()
}
}
/// Displays and edits clip length.
#[derive(Clone, Debug, Default)] pub struct ClipLength {
/// Pulses per beat (quaver)
pub ppq: usize,
/// Beats per bar
pub bpb: usize,
/// Length of clip in pulses
pub pulses: usize,
/// Selected subdivision
pub focus: Option<ClipLengthFocus>,
}
/// Some sort of wrapper again?
pub struct PoolView<'a>(pub &'a Pool);
/// Modes for clip pool
#[derive(Debug, Clone)]
pub enum PoolMode {
/// Renaming a pattern
Rename(usize, Arc<str>),
/// Editing the length of a pattern
Length(usize, usize, ClipLengthFocus),
/// Load clip from disk
Import(usize, Browse),
/// Save clip to disk
Export(usize, Browse),
}
/// Focused field of `ClipLength`
#[derive(Copy, Clone, Debug)]
pub enum ClipLengthFocus {
/// Editing the number of bars
Bar,
/// Editing the number of beats
Beat,
/// Editing the number of ticks
Tick,
}
impl<T: AsRef<Pool> + AsMut<Pool>> HasPool for T {}
pub trait HasPool: AsRef<Pool> + AsMut<Pool> {
fn pool (&self) -> &Pool {
self.as_ref()
}
fn pool_mut (&mut self) -> &mut Pool {
self.as_mut()
}
}
impl_from!(Pool: |clip:&Arc<RwLock<MidiClip>>|{
let model = Self::default();
model.clips.try_write().unwrap().push(clip.clone());
model.clip.store(1, Relaxed);
model
});
impl_default!(Pool: Self {
browse: None,
clip: 0.into(),
clips: Arc::from(RwLock::from(vec![])),
mode: None,
samples: Arc::from(RwLock::from(vec![])),
visible: true,
});
impl Pool {
pub fn clip_index (&self) -> usize {
self.clip.load(Relaxed)
}
pub fn set_clip_index (&self, value: usize) {
self.clip.store(value, Relaxed);
}
pub fn mode (&self) -> &Option<PoolMode> {
&self.mode
}
pub fn mode_mut (&mut self) -> &mut Option<PoolMode> {
&mut self.mode
}
pub fn begin_import (&mut self) -> Usually<()> {
*self.mode_mut() = Some(PoolMode::Import(
self.clip_index(),
Browse::new(None)?
));
Ok(())
}
pub fn begin_export (&mut self) -> Usually<()> {
*self.mode_mut() = Some(PoolMode::Export(
self.clip_index(),
Browse::new(None)?
));
Ok(())
}
pub fn new_clip (&self) -> MidiClip {
MidiClip::new("Clip", true, 4 * PPQ, None, Some(ItemTheme::random()))
}
pub fn cloned_clip (&self) -> MidiClip {
let index = self.clip_index();
let mut clip = self.clips()[index].try_read().unwrap().duplicate();
clip.color = ItemTheme::random_near(clip.color, 0.25);
clip
}
pub fn add_new_clip (&self) -> (usize, Arc<RwLock<MidiClip>>) {
let clip = Arc::new(RwLock::new(self.new_clip()));
let index = {
let mut clips = self.clips.try_write().unwrap();
clips.push(clip.clone());
clips.len().saturating_sub(1)
};
self.clip.store(index, Relaxed);
(index, clip)
}
pub fn delete_clip (&mut self, clip: &MidiClip) -> bool {
let index = self.clips.try_read().unwrap().iter().position(|x|*x.try_read().unwrap()==*clip);
if let Some(index) = index {
self.clips.try_write().unwrap().remove(index);
return true
}
false
}
}
impl ClipLengthFocus {
pub fn next (&mut self) {
use ClipLengthFocus::*;
*self = match self { Bar => Beat, Beat => Tick, Tick => Bar, }
}
pub fn prev (&mut self) {
use ClipLengthFocus::*;
*self = match self { Bar => Tick, Beat => Bar, Tick => Beat, }
}
}
impl ClipLength {
pub fn _new (pulses: usize, focus: Option<ClipLengthFocus>) -> Self {
Self { ppq: PPQ, bpb: 4, pulses, focus }
}
pub fn bars (&self) -> usize {
self.pulses / (self.bpb * self.ppq)
}
pub fn beats (&self) -> usize {
(self.pulses % (self.bpb * self.ppq)) / self.ppq
}
pub fn ticks (&self) -> usize {
self.pulses % self.ppq
}
}
impl Pool {
fn _clip_new (&self) -> MidiClip { self.new_clip() }
fn _clip_cloned (&self) -> MidiClip { self.cloned_clip() }
fn _clip_index_current (&self) -> usize { 0 }
fn _clip_index_after (&self) -> usize { 0 }
fn _clip_index_previous (&self) -> usize { 0 }
fn _clip_index_next (&self) -> usize { 0 }
fn _color_random (&self) -> ItemColor { ItemColor::random() }
}
impl<'a> PoolView<'a> {
//fn tui (&self) -> impl Draw<'_, Tui> {
//let Self(pool) = self;
////let color = self.1.clip().map(|c|c.try_read().unwrap().color).unwrap_or_else(||g(32).into());
////let on_bg = |x|x;//below(Repeat(" "), bg(color.darkest.term, x));
////let border = |x|x;//Outer(Style::default().fg(color.dark.term).bg(color.darkest.term)).enclose(x);
////let height = pool.clips.try_read().unwrap().len() as u16;
//iter(
//||pool.clips().clone().into_iter(),
//move|clip: Arc<RwLock<MidiClip>>, i: usize|{
//let MidiClip { ref name, color, length, .. } = *clip.try_read().unwrap();
//let item_height = 1;
//let _item_offset = i as u16 * item_height;
//let selected = i == pool.clip_index();
//let b = if selected { color.light.term } else { color.base.term };
//let f = color.lightest.term;
//let name = if false { format!(" {i:>3}") } else { format!(" {i:>3} {name}") };
//let length = if false { String::default() } else { format!("{length} ") };
//bg(b, below!(
//fg(f, bold(selected, name)).origin_w().full_w(),
//fg(f, bold(selected, length)).origin_e().full_w(),
//when(selected, bold(true, fg(g(255), "▶"))).origin_w().full_w(),
//when(selected, bold(true, fg(g(255), "◀"))).origin_e().full_w(),
//)).exact_h(1)
//}).origin_n().full_h().exact_w(20)
//}
}
impl ClipLength {
//fn tui (&self) -> impl Draw<'_, Tui> {
//use ClipLengthFocus::*;
//let bars = format!("{}", self.bars());
//let beats = format!("{}", self.beats());
//let ticks = format!("{:>02}", self.ticks());
//match self.focus {
//None => east!(" ", bars, ".", beats, ".", ticks),
//Some(Bar) => east!("[", bars, "]", beats, ".", ticks),
//Some(Beat) => east!(" ", bars, "[", beats, "]", ticks),
//Some(Tick) => east!(" ", bars, ".", beats, "[", ticks),
//}
//}
}

46
src/proj_port.rs Normal file
View file

@ -0,0 +1,46 @@
use crate::*;
def_sizes_iter!(PortsSizes => Arc<str>, [Connect]);
impl_has!(Vec<MidiInput>: |self: Project| self.midi_ins);
impl_has!(Vec<MidiOutput>: |self: Project| self.midi_outs);
impl_has!(Vec<MidiInput>: |self: App|self.project.midi_ins);
impl_has!(Vec<MidiOutput>: |self: App|self.project.midi_outs);
pub struct Junction<T: JackPort>(T);
impl<T: JackPort> Draw<Tui> for Junction<T> {
fn draw (&self, to: &mut Tui) -> Drawn<u16> {
T::KIND.draw(to)
}
}
#[tek_proc::command(AudioInput)]
#[tek_proc::keyword(AudioInput)]
#[derive(Debug)]
pub enum AudioInputCmd {
Close,
Connect(Arc<str>),
}
#[tek_proc::command(AudioOutput)]
#[tek_proc::keyword(AudioOutput)]
#[derive(Debug)]
pub enum AudioOutputCmd {
Close,
Connect(Arc<str>),
}
#[tek_proc::command(MidiInput)]
#[tek_proc::keyword(MidiInput)]
#[derive(Debug)]
pub enum MidiInputCmd {
Close,
Connect(Arc<str>),
}
#[tek_proc::command(MidiOutput)]
#[tek_proc::keyword(MidiOutput)]
#[derive(Debug)]
pub enum MidiOutputCmd {
Close,
Connect(Arc<str>),
}

View file

@ -67,55 +67,6 @@ pub trait HasScene: AsRefOpt<Scene> + AsMutOpt<Scene> {
} }
} }
impl<T: HasScene
+ Namespace<usize>
+ Namespace<Arc<str>>
+ Namespace<ItemTheme>
> SceneController for T {}
#[tek_proc::commands(SceneCommand)]
pub trait SceneController: HasScene
+ Namespace<usize>
+ Namespace<Arc<str>>
+ Namespace<ItemTheme>
{
#[command(SetSize = "size")]
fn scene_set_size (&mut self, _size: usize) -> Perhaps<SceneCommand>
where Self: Namespace<usize>
{
todo!()
}
#[command(SetZoom = "zoom")]
fn scene_set_zoom (&mut self, _size: usize) -> Perhaps<SceneCommand>
where Self: Namespace<usize>
{
todo!()
}
#[command(SetName = "name")]
fn scene_set_name (&mut self, name: Arc<str>) -> Perhaps<SceneCommand>
where Self: Namespace<Arc<str>>
{
Ok(self.scene_mut().map(|scene|swap_value(
&mut scene.name,
&name,
|name|SceneCommand::SetName { name }
)).transpose()?.flatten())
}
#[command(SetColor = "color")]
fn scene_set_color (&mut self, color: ItemTheme) -> Perhaps<SceneCommand>
where Self: Namespace<ItemTheme>
{
Ok(self.scene_mut().map(|scene|swap_value(
&mut scene.color,
&color,
|color|SceneCommand::SetColor { color }
)).transpose()?.flatten())
}
}
pub type SceneWith<'a, T> = (usize, &'a Scene, usize, usize, T); pub type SceneWith<'a, T> = (usize, &'a Scene, usize, usize, T);
def_sizes_iter!(ScenesSizes => Scene); def_sizes_iter!(ScenesSizes => Scene);
@ -170,59 +121,6 @@ pub trait HasScenes: AsRef<Vec<Scene>> + AsMut<Vec<Scene>> {
} }
} }
impl<T: HasScenes
+ HasTracks
+ HasTrackScroll
+ HasSelection
+ HasJack<'static>
+ Namespace<usize>
+ Namespace<Option<ItemTheme>>
+ Namespace<Option<Arc<str>>>
> ScenesController for T {}
#[tek_proc::commands(ScenesCommand)]
pub trait ScenesController: HasScenes
+ HasTracks
+ HasTrackScroll
+ HasSelection
+ HasJack<'static>
+ Namespace<usize>
+ Namespace<Option<ItemTheme>>
+ Namespace<Option<Arc<str>>>
{
#[command(Add = "add")]
fn scenes_add (&mut self) -> Perhaps<ScenesCommand>
where Self: HasScenes + HasJack<'static>
{
let (_index, _) = self.scenes_add_one(None, None)?;
Ok(None)
}
// TODO
#[command(Select = "select")]
fn scene_select (&mut self) -> Perhaps<ScenesCommand> {
let scenes = self.scenes().len();
let select = self.selection_mut().select_scene(scenes);
*self.selection_mut() = select;
Ok(None)
}
#[command(Next = "next")]
fn scene_select_next (&mut self) -> Perhaps<ScenesCommand> {
let scenes = self.scenes().len();
let select = self.selection().select_scene_next(scenes);
*self.selection_mut() = select;
Ok(None)
}
#[command(Prev = "prev")]
fn scene_select_prev (&mut self) -> Perhaps<ScenesCommand> {
let select = self.selection_mut().select_scene_prev();
*self.selection_mut() = select;
Ok(None)
}
}
pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + Send + Sync { pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + Send + Sync {
fn h_scenes (&self) -> u16; fn h_scenes (&self) -> u16;
fn w_side (&self) -> u16; fn w_side (&self) -> u16;
@ -280,30 +178,6 @@ impl ScenesView for Project {
} }
} }
pub fn view_scene_name <'a> (
select: &Selection,
editor: Option<&'a MidiEditor>,
index: usize,
scene: &Scene,
editing: bool
) -> impl Draw<Tui> {
let h = if select.scene() == Some(index) && let Some(_editor) = editor {
7
} else {
Scene::DEFAULT_HEIGHT as u16
};
let a = east!("·s", index, " ", fg(g(255), bold(true, &scene.name))).align_w().full_w();
let b = when(select.scene() == Some(index) && editing, south(
editor.as_ref().map(|e|e.clip_status()),
editor.as_ref().map(|e|e.edit_status())).align_nw().full_wh());
let c = if select.scene() == Some(index) {
scene.color.light.term
} else {
scene.color.base.term
};
bg(c, south(a, b).align_nw().exact_h(h))
}
pub trait HasSceneScroll: HasScenes { pub trait HasSceneScroll: HasScenes {
fn scene_scroll (&self) -> usize; fn scene_scroll (&self) -> usize;
} }
@ -319,51 +193,3 @@ impl HasSceneScroll for App {
self.project.scene_scroll() self.project.scene_scroll()
} }
} }
pub fn view_scene_name_theme (scene: &Scene, track_index: usize) -> (Arc<str>, ItemTheme) {
if let Some(Some(clip)) = &scene.clips.get(track_index) {
let clip = clip.try_read().unwrap();
(format!(" ⏹ {}", &clip.name).into(), clip.color)
} else {
(" ⏹ -- ".into(), ItemTheme::G[32])
}
}
pub fn view_scene_bg (
theme: ItemTheme, select: &Selection, track_index: usize, scene_index: usize
) -> (Color, Color) {
let mut outline = theme.base.term;
(if select.track() == Some(track_index) && select.scene() == Some(scene_index) {
outline = theme.lighter.term;
theme.light.term
} else if select.track() == Some(track_index) || select.scene() == Some(scene_index) {
outline = theme.darkest.term;
theme.base.term
} else {
theme.dark.term
}, outline)
}
pub fn view_scene_w (
track: &Track, select: &Selection, track_index: usize, editor: Option<&MidiEditor>
) -> u16 {
if select.track() == Some(track_index) && let Some(editor) = editor {
(editor.size.w() as usize).max(24).max(track.width) as u16
} else {
track.width as u16
}
}
pub fn view_scene_y (
select: &Selection, scene_index: usize, editor: Option<&MidiEditor>
) -> u16 {
if select.scene() == Some(scene_index) && let Some(editor) = editor {
editor.size.h().max(12)
} else {
Scene::DEFAULT_HEIGHT as u16
}
}
pub fn view_scene_sel (select: &Selection, track_index: usize, scene_index: usize, editing: bool) -> bool {
editing && select.track() == Some(track_index) && select.scene() == Some(scene_index)
}

View file

@ -58,7 +58,7 @@ impl Track {
).exact_w((x2 - x1) as u16) ).exact_w((x2 - x1) as u16)
})) }))
} }
pub fn stop (&mut self) -> Perhaps<TrackCommand> { pub fn stop (&mut self) -> Perhaps<TrackCmd> {
self.sequencer.enqueue_next(None); self.sequencer.enqueue_next(None);
Ok(None) Ok(None)
} }
@ -85,71 +85,6 @@ pub trait HasTrack: AsRefOpt<Track> + AsMutOpt<Track> {
} }
} }
impl<T: HasTrack
+ Namespace<usize>
+ Namespace<Arc<str>>
+ Namespace<ItemTheme>
+ Namespace<Option<bool>>
> TrackController for T {}
#[tek_proc::commands(TrackCommand)]
pub trait TrackController: HasTrack
+ Namespace<usize>
+ Namespace<Arc<str>>
+ Namespace<ItemTheme>
+ Namespace<Option<bool>>
{
#[command(Stop = "track/stop")]
fn track_stop (&mut self) -> Perhaps<TrackCommand> {
self.track_mut().map(|track|track.sequencer.enqueue_next(None));
Ok(None)
}
#[command(SetMute = "track/mute")]
fn track_set_mute (&mut self, _mute: Option<bool>) -> Perhaps<TrackCommand> {
todo!()
}
#[command(SetSolo = "track/solo")]
fn track_set_solo (&mut self, _solo: Option<bool>) -> Perhaps<TrackCommand> {
todo!()
}
#[command(SetSize = "track/size")]
fn track_set_size (&mut self, _size: usize) -> Perhaps<TrackCommand> {
todo!()
}
#[command(SetZoom = "track/zoom")]
fn track_set_zoom (&mut self, _zoom: usize) -> Perhaps<TrackCommand> {
todo!()
}
#[command(SetName = "track/name")]
fn track_set_name (&mut self, name: Arc<str>) -> Perhaps<TrackCommand> {
self.track_mut()
.map(|track|swap_value(&mut track.name, &name, |name|TrackCommand::SetName { name }))
.transpose()
.map(Option::flatten)
}
#[command(SetColor = "track/color")]
fn track_set_color (&mut self, color: ItemTheme) -> Perhaps<TrackCommand> {
self.track_mut()
.map(|track|swap_value(&mut track.color, &color, |color|TrackCommand::SetColor { color }))
.transpose()
.map(Option::flatten)
}
#[command(SetRec = "track/rec")]
fn track_set_rec (&mut self, rec: Option<bool>) -> Perhaps<TrackCommand> {
self.track_mut()
.map(|track|toggle_bool(&mut track.sequencer.recording, &rec, |rec|TrackCommand::SetRec { rec }))
.transpose()
.map(Option::flatten)
}
#[command(SetMon = "track/mon")]
fn track_set_mon (&mut self, mon: Option<bool>) -> Perhaps<TrackCommand> {
self.track_mut()
.map(|track|toggle_bool(&mut track.sequencer.monitoring, &mon, |mon|TrackCommand::SetMon { mon }))
.transpose()
.map(Option::flatten)
}
}
pub(crate) fn track_width (_index: usize, track: &Track) -> u16 { pub(crate) fn track_width (_index: usize, track: &Track) -> u16 {
track.width as u16 track.width as u16
} }
@ -260,90 +195,6 @@ pub trait HasTracks: AsRef<Vec<Track>> + AsMut<Vec<Track>> + HasClock + HasTrack
} }
} }
impl<T: HasTracks
+ HasScenes
+ HasSelection
+ HasJack<'static>
+ Namespace<usize>
+ Namespace<Arc<[Connect]>>
+ Namespace<Option<usize>>
+ Namespace<Option<Arc<str>>>
+ Namespace<Option<ItemTheme>>
+ Namespace<Option<Arc<[Connect]>>>
+ Namespace<Option<Vec<Option<Arc<RwLock<MidiClip>>>>>>
> TracksController for T {}
#[tek_proc::commands(TracksCommand)]
pub trait TracksController: HasTracks
+ HasScenes
+ HasSelection
+ HasJack<'static>
+ Namespace<usize>
+ Namespace<Arc<[Connect]>>
+ Namespace<Option<usize>>
+ Namespace<Option<Arc<str>>>
+ Namespace<Option<ItemTheme>>
+ Namespace<Option<Arc<[Connect]>>>
+ Namespace<Option<Vec<Option<Arc<RwLock<MidiClip>>>>>>
{
#[command(Stop = "stop")]
/// Stop all playing clips
fn tracks_stop_all (&mut self) -> Perhaps<TracksCommand> {
for track in self.tracks_mut().iter_mut() {
track.sequencer.enqueue_next(None);
}
Ok(None)
}
#[command(Launch = "launch")]
/// Launch multiple clips
fn tracks_launch (
&mut self, clips: Option<Vec<Option<Arc<RwLock<MidiClip>>>>>
) -> Perhaps<TracksCommand> {
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);
}
}
Ok(None)
}
#[command(Add = "add")]
fn tracks_add (&mut self) -> Perhaps<TracksCommand>
where Self: HasScenes + HasJack<'static>
{
let (_index, _) = self.tracks_add_one(None, None, [].into(), [].into())?;
Ok(None)
}
#[command(Select = "select")]
fn track_select (&mut self) -> Perhaps<TracksCommand> {
let tracks = self.tracks().len();
let select = self.selection_mut().select_track(tracks);
*self.selection_mut() = select;
Ok(None)
}
#[command(Next = "next")]
fn track_select_next (&mut self) -> Perhaps<TracksCommand> {
let tracks = self.tracks().len();
let select = self.selection().select_track_next(tracks);
*self.selection_mut() = select;
Ok(None)
}
#[command(Prev = "prev")]
fn track_select_prev (&mut self) -> Perhaps<TracksCommand> {
let select = self.selection_mut().select_track_prev();
*self.selection_mut() = select;
Ok(None)
}
}
impl< impl<
T: HasTracks + ScenesView + HasMidiIns + HasMidiOuts + HasTrack T: HasTracks + ScenesView + HasMidiIns + HasMidiOuts + HasTrack
> TracksView for T {} > TracksView for T {}
@ -696,31 +547,6 @@ impl HasTrackScroll for App {
} }
} }
fn view_track_row_section <'a> (
_theme: ItemTheme,
button: impl Draw<Tui>,
button_add: impl Draw<Tui>,
content: impl Draw<Tui>,
) -> impl Draw<Tui> {
bar(
Split::West,
4u16,
button_add.align_ne().full_wh(),
bar(
Split::East,
20u16,
button.full_wh().align_nw(),
content.align_c().full_wh()
)
)
}
pub fn view_track_header <'a> (theme: ItemTheme, content: impl Draw<Tui>)
-> impl Draw<Tui>
{
bg(theme.darker.term, content.align_e().full_w()).exact_w(12)
}
//pub fn per_track <'a, T: Draw<'_, Tui> + 'a, U: TracksSizes<'a>> ( //pub fn per_track <'a, T: Draw<'_, Tui> + 'a, U: TracksSizes<'a>> (
//tracks: impl Fn() -> U + Send + Sync + 'a, //tracks: impl Fn() -> U + Send + Sync + 'a,
//callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a //callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a

View file

@ -8,81 +8,6 @@ pub(crate) use symphonia::{
}, },
}; };
pub trait HasSampler: AsRef<Sampler> + AsMut<Sampler> {
fn sampler (&self) -> &Sampler {
self.as_ref()
}
fn sampler_mut (&mut self) -> &mut Sampler {
self.as_mut()
}
}
impl<T: AsRef<Sampler> + AsMut<Sampler>> HasSampler for T {}
#[tek_proc::commands(SamplerCommand = "sampler")]
pub trait SamplerController: HasSampler
+ Namespace<usize>
{
#[command(RecordToggle = "rec-toggle")]
fn record_toggle (&mut self, slot: usize) -> Perhaps<SamplerCommand>
where Self: Sized
{
let sampler = self.sampler_mut();
let recording = sampler.recording.as_ref().map(|x|x.0);
let _ = SamplerCommand::RecordFinish.dispatch(self)?;
// autoslice: continue recording at next slot
if recording != Some(slot) {
SamplerCommand::RecordBegin { slot }.dispatch(self)
} else {
Ok(None)
}
}
#[command(RecordBegin = "rec-begin")]
fn record_begin (&mut self, slot: usize) -> Perhaps<SamplerCommand> {
let sampler = self.sampler_mut();
sampler.recording = Some((
slot,
Some(Arc::new(RwLock::new(Sample::new(
"Sample", 0, 0, vec![vec![]; sampler.audio_ins.len()]
))))
));
Ok(None)
}
#[command(RecordFinish = "rec-finish")]
fn record_finish (&mut self) -> Perhaps<SamplerCommand> {
let sampler = self.sampler_mut();
let _prev_sample = sampler.recording.as_mut().map(|(index, sample)|{
std::mem::swap(sample, &mut sampler.samples.0[*index]);
sample
}); // TODO: undo
Ok(None)
}
#[command(RecordCancel = "rec-cancel")]
fn record_cancel (&mut self) -> Perhaps<SamplerCommand> {
self.sampler_mut().recording = None;
Ok(None)
}
#[command(PlaySample = "sample-play")]
fn sample_play (&mut self, slot: usize) -> Perhaps<SamplerCommand> {
let sampler = self.sampler_mut();
if let Some(ref sample) = sampler.samples.0[slot] {
sampler.voices.try_write().unwrap().push(Sample::play(sample, 0, &u7::from(128)));
}
Ok(None)
}
#[command(StopSample = "sample-stop")]
fn sample_stop (&mut self, slot: usize) -> Perhaps<SamplerCommand> {
todo!();
//Ok(None)
}
}
/// Plays [Voice]s from [Sample]s. /// Plays [Voice]s from [Sample]s.
/// ///
/// ``` /// ```
@ -133,6 +58,17 @@ pub trait SamplerController: HasSampler
#[cfg(feature = "meter")] pub output_meters: Vec<f32>, #[cfg(feature = "meter")] pub output_meters: Vec<f32>,
} }
pub trait HasSampler: AsRef<Sampler> + AsMut<Sampler> {
fn sampler (&self) -> &Sampler {
self.as_ref()
}
fn sampler_mut (&mut self) -> &mut Sampler {
self.as_mut()
}
}
impl<T: AsRef<Sampler> + AsMut<Sampler>> HasSampler for T {}
impl_audio!(Sampler: sampler_jack_process); impl_audio!(Sampler: sampler_jack_process);
pub(crate) fn sampler_jack_process (state: &mut Sampler, _: &Client, scope: &ProcessScope) -> Control { pub(crate) fn sampler_jack_process (state: &mut Sampler, _: &Client, scope: &ProcessScope) -> Control {
@ -314,84 +250,6 @@ impl Sampler {
} }
} }
fn draw_list_item (sample: &Option<Arc<RwLock<Sample>>>) -> String {
if let Some(sample) = sample {
let sample = sample.try_read().unwrap();
format!("{:8}", sample.name)
//format!("{:8} {:3} {:6}-{:6}/{:6}",
//sample.name,
//sample.gain,
//sample.start,
//sample.end,
//sample.channels[0].len()
//)
} else {
String::from("........")
}
}
fn draw_viewer (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> + use<'_> {
let min_db = -64.0;
draw(move|to: &mut Tui|{
let xywh = to.area().into();
let XYWH(x, y, width, height) = xywh;
let area = Rect { x, y, width, height };
if let Some(sample) = &sample {
let sample = sample.try_read().unwrap();
let start = sample.start as f64;
let end = sample.end as f64;
let length = end - start;
let step = length / width as f64;
let mut t = start;
let mut lines = vec![];
while t < end {
let chunk = &sample.channels[0][t as usize..((t + step) as usize).min(sample.end)];
let total: f32 = chunk.iter().map(|x|x.abs()).sum();
let count = chunk.len() as f32;
let meter = 10. * (total / count).log10();
let x = t as f64;
let y = meter as f64;
lines.push(Line::new(x, min_db, x, y, Color::Green));
t += step / 2.;
}
to.buffer().map(|buf|{
Canvas::default()
.x_bounds([sample.start as f64, sample.end as f64])
.y_bounds([min_db, 0.])
.paint(|ctx| {
for line in lines.iter() {
ctx.draw(line);
}
//FIXME: proportions
//let text = "press record to finish sampling";
//ctx.print(
//(width - text.len() as u16) as f64 / 2.0,
//height as f64 / 2.0,
//text.red()
//);
})
.render(area, buf);
});
} else {
to.buffer().map(|buf|{
Canvas::default()
.x_bounds([0.0, width as f64])
.y_bounds([0.0, height as f64])
.paint(|_ctx| {
//let text = "press record to begin sampling";
//ctx.print(
//(width - text.len() as u16) as f64 / 2.0,
//height as f64 / 2.0,
//text.red()
//);
})
.render(area, buf);
});
}
Ok(Some(xywh))
})
}
/// Create [Voice]s from [Sample]s in response to MIDI input. /// Create [Voice]s from [Sample]s in response to MIDI input.
fn sampler_midi_in ( fn sampler_midi_in (
samples: &SampleKit<128>, voices: &Arc<RwLock<Vec<Voice>>>, RawMidi { time, bytes }: RawMidi samples: &SampleKit<128>, voices: &Arc<RwLock<Vec<Voice>>>, RawMidi { time, bytes }: RawMidi
@ -411,71 +269,6 @@ fn sampler_midi_in (
} }
} }
fn draw_sample (
to: &mut Tui, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool
) -> Usually<usize> {
let style = if focus { Style::default().green() } else { Style::default() };
if focus {
to.blit(&"🬴", x+1, y, Some(style.bold()));
}
let label1 = format!("{:3} {:12}",
note.map(|n|n.to_string()).unwrap_or(String::default()),
sample.name);
let label2 = format!("{:>6} {:>6} +0.0",
sample.start,
sample.end);
to.blit(&label1, x+2, y, Some(style.bold()));
to.blit(&label2, x+3+label1.len()as u16, y, Some(style));
Ok(label1.len() + label2.len() + 4)
}
pub fn view_sample_info (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> + use<'_> {
when(sample.is_some(), draw(move|to: &mut Tui|{
let sample = sample.unwrap().try_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 = draw(move|to: &mut Tui|{
let sample = sample.unwrap().try_read().unwrap();
let theme = sample.color;
south!(
field_h(theme, "Name ", format!("{:<10}", sample.name.clone())) .align_w().full_w(),
field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())).align_w().full_w(),
field_h(theme, "Start ", format!("{:<8}", sample.start)) .align_w().full_w(),
field_h(theme, "End ", format!("{:<8}", sample.end)) .align_w().full_w(),
field_h(theme, "Trans ", "0") .align_w().full_w(),
field_h(theme, "Gain ", format!("{}", sample.gain)) .align_w().full_w(),
).exact_w(20).draw(to)
});
let b = draw(|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.try_read().unwrap();
format!("Sample {}-{}", sample.start, sample.end)
})
.unwrap_or_else(||"No sample".to_string())))
}
/// Get the first sampler of the active track /// Get the first sampler of the active track
impl Project { impl Project {
pub fn sampler (&self) -> Option<&Sampler> { pub fn sampler (&self) -> Option<&Sampler> {

View file

@ -1,5 +1,34 @@
#![allow(clippy::unit_arg)] #![allow(clippy::unit_arg)]
mod deps; pub use self::deps::*;
mod config; pub use self::config::*;
mod config_bind; pub use self::config_bind::*;
mod config_mode; pub use self::config_mode::*;
mod config_view; pub use self::config_view::*;
mod midi_octave; pub use self::midi_octave::*;
mod midi_piano; pub use self::midi_piano::*;
mod midi_point; pub use self::midi_point::*;
mod midi_range; pub use self::midi_range::*;
mod smpl; pub use self::smpl::*;
mod smpl_sample; pub use self::smpl_sample::*;
mod smpl_add; pub use self::smpl_add::*;
mod smpl_kit; pub use self::smpl_kit::*;
mod smpl_voice; pub use self::smpl_voice::*;
mod midi_clip; pub use self::midi_clip::*;
mod midi_edit; pub use self::midi_edit::*;
mod midi_mon; pub use self::midi_mon::*;
mod midi_play; pub use self::midi_play::*;
mod midi_rec; pub use self::midi_rec::*;
mod midi_seq; pub use self::midi_seq::*;
mod time_base; pub use self::time_base::*;
mod time_moment; pub use self::time_moment::*;
mod time_ticker; pub use self::time_ticker::*;
/// Define a type alias for iterators of sized items (columns). /// Define a type alias for iterators of sized items (columns).
macro_rules! def_sizes_iter { macro_rules! def_sizes_iter {
($Type:ident => $($Item:ty),+) => { ($Type:ident => $($Item:ty),+) => {
@ -8,57 +37,24 @@ macro_rules! def_sizes_iter {
} }
} }
mod app; pub use self::app::*; mod proj; pub use self::proj::*;
mod browse; pub use self::browse::*; mod proj_clock; pub use self::proj_clock::*;
mod cli; pub use self::cli::*; mod proj_device; pub use self::proj_device::*;
mod clock; pub use self::clock::*; mod proj_mixer; pub use self::proj_mixer::*;
mod clock_view; pub use self::clock_view::*; mod proj_port; pub use self::proj_port::*;
mod config; pub use self::config::*; mod proj_scene; pub use self::proj_scene::*;
mod config_bind; pub use self::config_bind::*; mod proj_select; pub use self::proj_select::*;
mod config_mode; pub use self::config_mode::*; mod proj_track; pub use self::proj_track::*;
mod config_view; pub use self::config_view::*; mod proj_pool; pub use self::proj_pool::*;
mod deps; pub use self::deps::*;
mod device; pub use self::device::*;
mod dialog; pub use self::dialog::*;
mod field; pub use self::field::*;
mod memo; pub use self::memo::*;
mod menu_item; pub use self::menu_item::*;
mod meter; pub use self::meter::*;
mod mix; pub use self::mix::*;
mod moment; pub use self::moment::*;
mod octave; pub use self::octave::*;
mod piano; pub use self::piano::*;
mod point; pub use self::point::*;
mod pool; pub use self::pool::*;
mod port; pub use self::port::*;
mod project; pub use self::project::*;
mod range; pub use self::range::*;
mod sampler; pub use self::sampler::*;
mod sample; pub use self::sample::*;
mod sample_add; pub use self::sample_add::*;
mod sample_kit; pub use self::sample_kit::*;
mod sample_voice; pub use self::sample_voice::*;
mod scene; pub use self::scene::*;
mod select; pub use self::select::*;
mod midi_clip; pub use self::midi_clip::*;
mod midi_clips; pub use self::midi_clips::*;
mod midi_edit; pub use self::midi_edit::*;
mod midi_mon; pub use self::midi_mon::*;
mod midi_play; pub use self::midi_play::*;
mod midi_rec; pub use self::midi_rec::*;
mod midi_seq; pub use self::midi_seq::*;
mod ticker; pub use self::ticker::*;
mod timebase; pub use self::timebase::*;
mod track; pub use self::track::*;
#[cfg(feature = "lv2")] mod lv2; mod tek_browse; pub use self::tek_browse::*;
#[cfg(feature = "lv2")] pub use self::lv2::*; mod tek_cli; pub use self::tek_cli::*;
mod tek_ctrl; pub use self::tek_ctrl::*;
mod tek_view; pub use self::tek_view::*;
#[cfg(feature = "lv2_gui")] mod lv2_gui; #[cfg(feature = "lv2")] mod device_lv2; #[cfg(feature = "lv2")] pub use self::device_lv2::*;
#[cfg(feature = "lv2_gui")] pub use self::lv2_gui::*; #[cfg(feature = "lv2_gui")] mod device_lv2_gui; #[cfg(feature = "lv2_gui")] pub use self::device_lv2_gui::*;
#[cfg(feature = "vst2")] mod device_vst2; #[cfg(feature = "vst2")] pub use self::device_vst2::*;
#[cfg(feature = "vst2")] mod vst2;
#[cfg(feature = "vst2")] pub use self::vst2::*;
/// Command-line entrypoint. /// Command-line entrypoint.
#[allow(unused)] #[allow(unused)]
@ -102,3 +98,689 @@ fn run_new_plain (config: Config) -> Usually<()> {
Ok(app) Ok(app)
})?) })?)
} }
/// Total application state.
///
/// ```
/// use tek::{HasTracks, HasScenes, TracksView, ScenesView};
/// let mut app = tek::App::default();
/// let _ = app.scenes_add_one(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)]
#[namespace(Arc<[Connect]> get_arc_array_connect)]
#[namespace(Arc<str> get_arc_str)]
#[namespace(Color get_color)]
#[namespace(ControlAxis get_axis)]
#[namespace(Dialog get_dialog)]
#[namespace(ItemColor)]
#[namespace(ItemTheme)]
#[namespace(MidiClip)]
#[namespace(Option<Arc<[Connect]>> get_opt_arc_array_connect)]
#[namespace(Option<Arc<str>> get_opt_arc_str)]
#[namespace(Option<ItemTheme> get_opt_itemtheme)]
#[namespace(Option<Arc<RwLock<MidiClip>>> get_opt_arc_rwlock_midiclip)]
#[namespace(Option<Vec<Option<Arc<RwLock<MidiClip>>>>> get_opt_vec_opt_arc_rwlock_midiclip)]
#[namespace(Option<bool> get_opt_bool)]
#[namespace(Option<u16> get_opt_u16)]
#[namespace(Option<u32> get_opt_u32)]
#[namespace(Option<usize> get_opt_usize)]
#[namespace(PathBuf get_path_buf)]
#[namespace(Vec<Arc<str>> get_vec_arc_str)]
#[namespace(bool get_bool)]
#[namespace(f64 get_f64)]
#[namespace(isize)]
#[namespace(u16 get_u16)]
#[namespace(u32)]
#[namespace(u8)]
#[namespace(usize get_usize)]
pub struct App {
/// Exit flag
pub exit: Exit,
/// Base color.
pub color: ItemTheme,
/// Must not be dropped for the duration of the process
pub jack: Jack<'static>,
/// Display size
pub size: Sizer,
/// Performance counter
pub perf: PerfModel,
/// Available view modes and input bindings
pub config: Arc<Config>,
/// Currently selected mode
pub modes: Arc<RwLock<Vec<Arc<str>>>>,
/// Undo history
pub history: Vec<(AppCmd, Option<AppCmd>)>,
/// Dialog overlay
pub dialog: Dialog,
/// Contains the currently edited musical arrangement
pub project: Project,
/// Error, if any
pub error: Arc<RwLock<Option<Arc<str>>>>,
#[cfg(feature = "prof2")]
/// Tracing guard
pub guard: Option<tracing_flame::FlushGuard<std::io::BufWriter<std::fs::File>>>,
}
impl App {
/// Create a new application instance from a backend, project, config, and mode
///
/// ```
/// let mut proj = tek::Project::default();
/// proj.jack = tek::tengri::Jack::new(&"test_tek").expect("failed to connect to jack");
/// let config = "(mode hi (name Hello) (info World))";
/// let mut conf = tek::Config::load(tek::Config::default(), config).unwrap();
/// let tek = tek::App::new(None, proj, conf.into(), "hello");
/// ```
pub fn new (
exit: Option<Exit>,
project: Project,
config: Arc<Config>,
mode: impl AsRef<str>
) -> Self {
App {
exit: exit.unwrap_or_default(),
jack: project.jack.clone(),
color: ItemTheme::random(),
dialog: Dialog::welcome(),
modes: Arc::new(RwLock::new(vec![mode.as_ref().into()])),
config,
project,
..Default::default()
}
}
}
fn get_arc_str <'a> (_: &App, src: impl Language) -> Perhaps<Arc<str>> {
Ok(src.src()?.map(|x|x.into()))
}
fn get_u16 <'a> (state: &App, src: impl Language) -> Perhaps<u16> {
Ok(Some(match src.word()? {
Some(":w/sidebar") => state.project.w_sidebar(state.editor().is_some()),
Some(":h/sample-detail") => 6.max(state.size.h() as u16 * 3 / 9),
_ => return try_to_u16(src)
}))
}
fn get_usize <'a> (state: &App, src: impl Language) -> Perhaps<usize> {
Ok(Some(match src.word()? {
Some(":scene-count") => state.scenes().len(),
Some(":track-count") => state.tracks().len(),
Some(":device-kind") => state.dialog.device_kind().unwrap_or(0),
Some(":device-kind/next") => state.dialog.device_kind_next().unwrap_or(0),
Some(":device-kind/prev") => state.dialog.device_kind_prev().unwrap_or(0),
_ => return try_to_usize(src)
}))
}
fn get_f64 <'a> (_state: &App, _src: impl Language) -> Perhaps<f64> {
todo!()
}
fn get_bool <'a> (state: &App, src: impl Language) -> Perhaps<bool> {
src.word()?.map(|word|Ok(match word {
"Y" => true,
"N" => false,
":mode/editor" => state.project.editor.is_some(),
":focused/dialog" => !matches!(state.dialog, Dialog::None),
":focused/message" => matches!(state.dialog, Dialog::Message(..)),
":focused/add_device" => matches!(state.dialog, Dialog::Device(..)),
":focused/browser" => state.dialog.browser().is_some(),
":focused/pool/import" => matches!(state.project.pool.mode, Some(PoolMode::Import(..))),
":focused/pool/export" => matches!(state.project.pool.mode, Some(PoolMode::Export(..))),
":focused/pool/rename" => matches!(state.project.pool.mode, Some(PoolMode::Rename(..))),
":focused/pool/length" => matches!(state.project.pool.mode, Some(PoolMode::Length(..))),
":focused/clip" => !state.editor_focused() && matches!(state.selection(), Selection::TrackClip{..}),
":focused/track" => !state.editor_focused() && matches!(state.selection(), Selection::Track(..)),
":focused/scene" => !state.editor_focused() && matches!(state.selection(), Selection::Scene(..)),
":focused/mix" => !state.editor_focused() && matches!(state.selection(), Selection::Mix),
_ => return err!("not bool: {word}")
})).transpose()
}
#[allow(unused)]
fn get_selection <'a> (state: &App, src: impl Language) -> Perhaps<Selection> {
src.word()?.map(|word|Ok(match word {
":select/scene" => state.selection().select_scene(state.tracks().len()),
":select/scene/next" => state.selection().select_scene_next(state.scenes().len()),
":select/scene/prev" => state.selection().select_scene_prev(),
":select/track" => state.selection().select_track(state.tracks().len()),
":select/track/next" => state.selection().select_track_next(state.tracks().len()),
":select/track/prev" => state.selection().select_track_prev(),
_ => return err!("not selection: {word}")
})).transpose()
}
fn get_color <'a> (_: &App, src: impl Language) -> Perhaps<Color> {
if let Some(expr) = src.expr()? {
match (expr.head()?, expr.tail()?) {
(Some("g"), Some(tail)) => {
let n = try_to_u8(tail.head().map_err(Into::into))?
.ok_or(LanguageError::domain("not gray"))?;
Ok(Some(Color::Rgb(n, n, n)))
},
(Some("rgb"), Some(tail)) => {
let r = try_to_u8(tail.head().map_err(Into::into))?
.ok_or(LanguageError::domain("not red"))?;
let g = try_to_u8(tail.tail().head().map_err(Into::into))?
.ok_or(LanguageError::domain("not green"))?;
let b = try_to_u8(tail.tail().tail().head().map_err(Into::into))?
.ok_or(LanguageError::domain("not blue"))?;
Ok(Some(Color::Rgb(r, g, b)))
},
(Some(_), _) => return err!("not a color expression: {expr}"),
(None, _) => return err!("not a color expression: {expr}"),
}
} else if let Ok(Some(sym)) = src.word() {
Ok(match sym {
":color/bg" => Some(Color::Rgb(28, 32, 36)),
":color/fg" => Some(Color::Rgb(98, 92, 96)),
_ => return err!("not a color: {sym}")
})
} else {
return err!("not a color: {:?}", src.src()?)
}
}
#[allow(unused)]
fn get_opt_u7 <'a> (state: &App, src: impl Language) -> Perhaps<Option<u7>> {
eval_word(src, |word|Ok(match word {
":editor/pitch" => Some((
state.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8
).into()),
_ => return err!("unknown midi note: {word}")
}))
}
fn get_opt_usize <'a> (state: &App, src: impl Language) -> Perhaps<Option<usize>> {
eval_word(src, |word|Ok(match word {
":selected/scene" => state.selection().scene(),
":selected/track" => state.selection().track(),
_ => return err!("unknown opt<usize>: {word}")
}))
}
fn get_axis <'a> (_: &App, src: impl Language) -> Perhaps<ControlAxis> {
eval_word(src, |word|Ok(match word {
"X" => ControlAxis::X, "XFine" | "X2" => ControlAxis::XFine,
"Y" => ControlAxis::Y, "YFine" | "Y2" => ControlAxis::YFine,
"Z" => ControlAxis::Z, "ZFine" | "Z2" => ControlAxis::ZFine,
_ => return err!("unknown axis {word}")
}))
}
fn get_opt_u16 <'a> (_: &App, _src: impl Language) -> Perhaps<Option<u16>> {
Ok(None)
}
fn get_opt_u32 <'a> (_: &App, _src: impl Language) -> Perhaps<Option<u32>> {
Ok(None)
}
fn get_opt_itemtheme <'a> (_: &App, _: impl Language) -> Perhaps<Option<ItemTheme>> {
todo!()
}
fn get_opt_arc_array_connect <'a> (_: &App, _: impl Language) -> Perhaps<Option<Arc<[Connect]>>> {
todo!()
}
fn get_arc_array_connect <'a> (_: &App, _: impl Language) -> Perhaps<Arc<[Connect]>> {
todo!()
}
fn get_opt_arc_str <'a> (_: &App, _: impl Language) -> Perhaps<Option<Arc<str>>> {
todo!()
}
fn get_opt_bool <'a> (_: &App, _: impl Language) -> Perhaps<Option<bool>> {
todo!()
}
fn get_path_buf <'a> (_: &App, _: impl Language) -> Perhaps<PathBuf> {
todo!()
}
fn get_vec_arc_str <'a> (_: &App, _: impl Language) -> Perhaps<Vec<Arc<str>>> {
todo!()
}
impl_has!(Clock: |self: App|self.project.clock);
impl_has!(Dialog: |self: App|self.dialog);
impl_has!(Jack<'static>: |self: App|self.jack);
impl_has!(Pool: |self: App|self.project.pool);
impl_has!(Selection: |self: App|self.project.selection);
impl_as_ref!(Option<MidiEditor>: |self: App|self.project.as_ref());
impl_as_mut!(Option<MidiEditor>: |self: App|self.project.as_mut());
primitive!(u8: try_to_u8);
primitive!(u16: try_to_u16);
primitive!(usize: try_to_usize);
primitive!(isize: try_to_isize);
impl HasClips for App {
fn clip (&self) -> Option<Arc<RwLock<MidiClip>>> {
self.project.clip()
}
fn clip_mut (&self) -> &mut Option<Arc<RwLock<MidiClip>>> {
self.project.clip_mut()
}
fn clips <'a> (&'a self) -> RwLockReadGuard<'a, Clips> {
self.project.clips()
}
fn clips_mut <'a> (&'a self) -> RwLockWriteGuard<'a, Clips> {
self.project.clips_mut()
}
}
tui_keys!(self: App, input {
#[cfg(feature = "prof2")] profiling::scope!("App::tui_keys!");
Self::input_cmd(self, input)
});
impl App {
fn input_cmd (state: &mut Self, input: &TuiEvent) -> Usually<()> {
if let Some(bindings) = Self::input_cmd_find(state, input) {
return Self::input_cmd_exec(state, bindings)
}
Ok(())
}
fn input_cmd_find (state: &mut Self, input: &TuiEvent) -> Option<Arc<[Binding<Arc<str>>]>> {
for name in state.modes.read().iter().rev() {
let mode = state.config.get_mode(name).as_ref().map(Arc::clone);
if let Some(mode) = mode {
let binds = state.config.binds.clone();
for id in mode.keys.iter() {
if let Some(event_map) = binds.find(id)
&& let Some(bindings) = event_map.query(input) {
return Some(bindings.into())
}
}
}
}
None
}
fn input_cmd_exec (state: &mut Self, bindings: Arc<[Binding<Arc<str>>]>) -> Usually<()> {
for binding in bindings.iter() {
for cmd in binding.commands.iter() {
if Self::cmd::<AppCmd>(state, cmd)? {
return Ok(())
}
}
}
Ok(())
}
fn cmd <
T: Dispatch<Self> + Clone + Into<AppCmd>
> (state: &mut Self, src: &str) -> Usually<bool> where
Self: Namespace<T>,
{
if let Some(cmd) = Namespace::<T>::namespace(state, &src)? {
return Self::cmd_dispatch(state, cmd.into())
}
Ok(false)
}
fn cmd_dispatch (state: &mut Self, cmd: AppCmd) -> Usually<bool> where
Self: Namespace<AppCmd>,
{
match cmd.clone().dispatch(state) {
Err(err) => {
state.history.push((cmd.into(), None));
return Err(err)
},
Ok(undo) => {
state.history.push((cmd.into(), undo.map(Into::into)));
return Ok(true)
}
}
}
}
/// A control axis.
///
/// ```
/// let axis = tek::ControlAxis::X;
/// ```
#[derive(Debug, Copy, Clone)] pub enum ControlAxis {
X, XFine,
Y, YFine,
Z, ZFine,
I, IFine,
}
/// 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) -> Drawn<u16> {
#[cfg(feature = "prof2")] profiling::scope!("App::draw");
//self.perf.cycle(&mut |_|{
self.draw_error(to)?;
self.draw_modes(to)?;
self.draw_debug(to)?;
#[cfg(feature = "prof2")] profiling::finish_frame!();
Ok(Some(to.area().into()))
//})
}
}
impl App {
fn draw_error (&self, to: &mut Tui) -> Usually<()> {
if let Some(e) = self.error.try_read().unwrap().as_ref() {
e.as_ref().align_c().draw(to)?;
}
Ok(())
}
fn draw_modes (&self, to: &mut Tui) -> Usually<()> {
for name in self.modes.read().iter() {
let mut error = false;
if let Some(mode) = self.config.get_mode(name).as_ref().map(Arc::clone) {
for (index, view) in mode.view.iter().enumerate() {
match (view.render)(self, to) {
Ok(None) => {},
Ok(Some(XYWH(.., w, h))) => {
self.size.0.store(w as usize, Relaxed);
self.size.1.store(h as usize, Relaxed);
},
Err(e) => {
let message = format!(
"Mode: {:?}\n\nLayer: #{index}\n\nError: {e}\n\nSource:\n{}",
&mode.name,
&view.source
);
*self.error.try_write().unwrap() = Some(message.into());
error = true;
break;
}
}
}
if error {
break
} else {
*self.error.try_write().unwrap() = None;
}
}
}
Ok(())
}
#[allow(unused)] fn draw_debug (&self, to: &mut Tui) -> Drawn<u16> {
Split::Above.bar(0,
Split::West.bar(16,
format!("{}/{} {} ",
self.perf.used.load(Relaxed),
self.perf.window.load(Relaxed),
self.perf.clock.raw() / 10000000).align_e(),
format!("{}x{} ",
self.size.0.load(Relaxed),
self.size.1.load(Relaxed)).align_w(),
).align_se(),
Split::East.bar(16,
format!("{}", self.modes.read()[0]),
format!("{}", self.modes.read()[0])
).align_sw()
).draw(to)
}
}
impl_has!(Sizer: |self: App|self.size);
pub trait HasWidth {
const MIN_WIDTH: usize;
/// Increment track width.
fn width_inc (&mut self);
/// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH].
fn width_dec (&mut self);
}
pub fn get_dialog <'a> (_: &App, src: impl Language) -> Perhaps<Dialog> {
eval_word(&src, |word|Ok(match word {
":none" => Dialog::None,
":options" => Dialog::Options,
":device" => Dialog::Device(0),
":device/prev" => Dialog::Device(0),
":device/next" => Dialog::Device(0),
":help" => Dialog::Help(0),
":save" => Dialog::Browse(
BrowseTarget::SaveProject,
Browse::new(None).unwrap().into()),
":load" => Dialog::Browse(
BrowseTarget::LoadProject,
Browse::new(None).unwrap().into()),
":import/clip" => Dialog::Browse(
BrowseTarget::ImportClip(Default::default()),
Browse::new(None).unwrap().into()),
":export/clip" => Dialog::Browse(
BrowseTarget::ExportClip(Default::default()),
Browse::new(None).unwrap().into()),
":import/sample" => Dialog::Browse(
BrowseTarget::ImportSample(Default::default()),
Browse::new(None).unwrap().into()),
":export/sample" => Dialog::Browse(
BrowseTarget::ExportSample(Default::default()),
Browse::new(None).unwrap().into()),
_ => return Err(format!("not a dialog: {}", src.src()?.unwrap_or("<???>")).into())
}))
}
/// Various possible dialog modes.
///
/// ```
/// let dialog: tek::Dialog = Default::default();
/// ```
#[derive(Debug, Clone, Default, PartialEq)] pub enum Dialog {
#[default] None,
Help(usize),
Menu(usize, Arc<[MenuItem]>),
Device(usize),
Message(Arc<str>),
Browse(BrowseTarget, Arc<Browse>),
Options,
}
impl Dialog {
/// ```
/// let _ = tek::Dialog::welcome();
/// ```
pub fn welcome () -> Self {
Self::Menu(1, Arc::new([
MenuItem::new(" Resume session ", [
AppCmd::Project {
command: ProjectCmd::LoadLast
}
]),
MenuItem::new(" Create session ", [
AppCmd::Dialog {
dialog: Dialog::None
},
AppCmd::Mode {
command: ModeCmd::Reset {
modes: vec!["Arranger".into()]
}
},
AppCmd::Tracks {
command: TracksCmd::Add
},
AppCmd::Scenes {
command: ScenesCmd::Add
}
]),
MenuItem::new(" Load session ", [
AppCmd::Dialog {
dialog: Dialog::Browse(
BrowseTarget::LoadProject,
Browse::new(None).unwrap().into()
)
//dialog: Dialog::Browse {
//title: "Load session".into(),
//filter: "\\.tek$",
//}
}
]),
MenuItem::new(" Exit ", [
AppCmd::Exit
]),
]))
}
/// FIXME: generalize
/// ```
/// let _ = tek::Dialog::welcome().menu_selected();
/// ```
pub fn menu_selected (&self) -> Option<usize> {
if let Self::Menu(selected, _) = self { Some(*selected) } else { None }
}
/// FIXME: generalize
/// ```
/// let _ = tek::Dialog::welcome().menu_next();
/// ```
pub fn menu_next (&self) -> Self {
match self {
Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.len()), items.clone()),
_ => Self::None
}
}
/// FIXME: generalize
/// ```
/// let _ = tek::Dialog::welcome().menu_prev();
/// ```
pub fn menu_prev (&self) -> Self {
match self {
Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.len()), items.clone()),
_ => Self::None
}
}
/// FIXME: generalize
/// ```
/// let _ = tek::Dialog::welcome().device_kind();
/// ```
pub fn device_kind (&self) -> Option<usize> {
if let Self::Device(index) = self { Some(*index) } else { None }
}
/// FIXME: generalize
/// ```
/// let _ = tek::Dialog::welcome().device_kind_next();
/// ```
pub fn device_kind_next (&self) -> Option<usize> {
self.device_kind().map(|index|(index + 1) % device_kinds().len())
}
/// FIXME: generalize
/// ```
/// let _ = tek::Dialog::welcome().device_kind_prev();
/// ```
pub fn device_kind_prev (&self) -> Option<usize> {
self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1)))
}
/// FIXME: implement
pub fn message (&self) -> Option<&str> { todo!() }
/// FIXME: implement
pub fn browser (&self) -> Option<&Arc<Browse>> { todo!() }
/// FIXME: implement
pub fn browser_target (&self) -> Option<&BrowseTarget> { todo!() }
}
/// Increment a wrapping counter.
pub const fn wrap_inc (index: usize, count: usize) -> usize {
if count > 0 { (index + 1) % count } else { 0 }
}
/// Decrement a wrapping counter.
pub const fn wrap_dec (index: usize, count: usize) -> usize {
if count > 0 {
let a = index.overflowing_sub(1).0;
let b = count.saturating_sub(1);
if a < b { a } else { b }
} else {
0
}
}
/// An item of a menu.
///
/// ```
/// let item: tek::MenuItem = Default::default();
/// ```
#[derive(Clone)] pub struct MenuItem(
pub Arc<str>,
pub Arc<[AppCmd]>,
);
impl MenuItem {
pub fn new <const N: usize> (name: impl AsRef<str>, actions: [AppCmd; N]) -> Self {
Self(name.as_ref().into(), Arc::new(actions))
}
}
impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) });
impl_default!(MenuItem: Self::new("", []));
impl PartialEq for MenuItem {
fn eq (&self, other: &Self) -> bool {
self.0 == other.0
}
}
//impl AsRef<Arc<[MenuItem]>> for MenuItems {
//fn as_ref (&self) -> &Arc<[MenuItem]> {
//&self.0
//}
//}

View file

@ -1,50 +1,5 @@
use crate::*; use crate::*;
impl App {
/// 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 }
}
}
impl<T:
Namespace<usize> +
Namespace<PathBuf> +
Namespace<Arc<str>>
> BrowseController for T {}
#[tek_proc::commands(BrowseCommand = "browse")]
pub trait BrowseController:
Namespace<usize> +
Namespace<PathBuf> +
Namespace<Arc<str>>
{
/// Toggle visibility of browser
#[command(Show = "show")]
fn show (&mut self) -> Perhaps<BrowseCommand> {
todo!()
}
/// Set current directory
#[command(Chdir = "chdir")]
fn chdir (&mut self, _path: PathBuf) -> Perhaps<BrowseCommand> {
todo!()
}
/// Set current filter
#[command(SetSearch = "set-search")]
fn set_search (&mut self, _filter: Arc<str>) -> Perhaps<BrowseCommand> {
todo!()
}
/// Set selected item
#[command(SetCursor = "set-cursor")]
fn set_cursor (&mut self, _index: usize) -> Perhaps<BrowseCommand> {
todo!()
}
}
/// Browses for files to load/save. /// Browses for files to load/save.
/// ///
/// ``` /// ```
@ -179,13 +134,13 @@ pub fn scan (dir: &PathBuf) -> Usually<(Vec<OsString>, Vec<OsString>)> {
Ok((subdirs, files)) Ok((subdirs, files))
} }
pub fn view_browse_title (state: &App) -> impl Draw<Tui> { impl App {
field_v(ItemTheme::default(), match state.dialog.browser_target().unwrap() { /// Return reference to content browser if open.
BrowseTarget::SaveProject => "Save project:", ///
BrowseTarget::LoadProject => "Load project:", /// ```
BrowseTarget::ImportSample(_) => "Import sample:", /// assert_eq!(tek::App::default().browser(), None);
BrowseTarget::ExportSample(_) => "Export sample:", /// ```
BrowseTarget::ImportClip(_) => "Import clip:", pub fn browser (&self) -> Option<&Browse> {
BrowseTarget::ExportClip(_) => "Export clip:", if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None }
}, fg(g(96), x_repeat("🭻")).exact_h(1)).align_w().full_w() }
} }

822
src/tek_ctrl.rs Normal file
View file

@ -0,0 +1,822 @@
use crate::*;
impl_from!(AppCmd: |x: AxisCmd| AppCmd::Axis { command: x });
impl_from!(AppCmd: |x: BrowseCmd| AppCmd::Browse { command: x });
impl_from!(AppCmd: |x: MidiClipCmd| AppCmd::MidiClip { command: x });
impl_from!(AppCmd: |x: MidiEditCmd| AppCmd::MidiEdit { command: x });
impl_from!(AppCmd: |x: ModeCmd| AppCmd::Mode { command: x });
impl_from!(AppCmd: |x: PoolCmd| AppCmd::Pool { command: x });
impl_from!(AppCmd: |x: ProjectCmd| AppCmd::Project { command: x });
impl_from!(AppCmd: |x: SceneCmd| AppCmd::Scene { command: x });
impl_from!(AppCmd: |x: ScenesCmd| AppCmd::Scenes { command: x });
impl_from!(AppCmd: |x: TrackCmd| AppCmd::Track { command: x });
impl_from!(AppCmd: |x: TracksCmd| AppCmd::Tracks { command: x });
#[tek_proc::commands(AppCmd)] impl App {
/// Exit the application.
#[command(Exit = "exit")] pub fn cmd_exit (&mut self) -> Perhaps<AppCmd> {
self.exit.exit();
Ok(None)
}
/// Cancel current selection.
#[command(Cancel = "cancel")] pub fn cmd_cancel (&mut self) -> Perhaps<AppCmd> {
todo!("cancel")
}
/// Confirm current selection.
#[command(Confirm = "confirm")] pub fn cmd_confirm (&mut self) -> Perhaps<AppCmd> {
Ok(match self.dialog.clone() {
Dialog::Menu(index, items) => {
for command in items[index].1.iter() {
App::cmd_dispatch(self, command.clone())?;
}
None
}
_ => todo!(),
})
}
/// Swap currently active modal dialog.
///
/// ```
/// let _ = tek::App::default().cmd_dialog(tek::Dialog::welcome());
/// ```
#[command(Dialog = "dialog")] pub fn cmd_dialog (&mut self, dialog: Dialog) -> Perhaps<AppCmd> {
let mut dialog = dialog.clone();
std::mem::swap(&mut self.dialog, &mut dialog);
Ok(Some(AppCmd::Dialog { dialog }))
}
#[command(Mode = "mode")] pub fn cmd_mode (&mut self, command: ModeCmd) -> Perhaps<AppCmd> {
Ok(command.dispatch(self)?.map(Into::into))
}
#[command(Project = "session")] pub fn cmd_session (&mut self, command: ProjectCmd) -> Perhaps<AppCmd> {
Ok(command.dispatch(self)?.map(Into::into))
}
#[command(Axis = "axis")] pub fn cmd_axis (&mut self, command: AxisCmd) -> Perhaps<AppCmd> {
Ok(command.dispatch(self)?.map(Into::into))
}
#[command(Browse = "browse")] pub fn cmd_browse (&mut self, command: BrowseCmd) -> Perhaps<AppCmd> {
Ok(command.dispatch(self)?.map(Into::into))
}
#[command(MidiClip = "clip")] pub fn cmd_midi_clip (&mut self, command: MidiClipCmd) -> Perhaps<AppCmd> {
Ok(command.dispatch(self)?.map(Into::into))
}
#[command(MidiEdit = "edit")] pub fn cmd_midi_edit (&mut self, command: MidiEditCmd) -> Perhaps<AppCmd> {
Ok(command.dispatch(self)?.map(Into::into))
}
#[command(Pool = "pool")] pub fn cmd_pool (&mut self, command: PoolCmd) -> Perhaps<AppCmd> {
Ok(command.dispatch(self)?.map(Into::into))
}
#[command(Scene = "scene")] pub fn cmd_scene (&mut self, command: SceneCmd) -> Perhaps<AppCmd> {
Ok(command.dispatch(self)?.map(Into::into))
}
#[command(Scenes = "scenes")] pub fn cmd_scenes (&mut self, command: ScenesCmd) -> Perhaps<AppCmd> {
Ok(command.dispatch(self)?.map(Into::into))
}
#[command(Track = "track")] pub fn cmd_track (&mut self, command: TrackCmd) -> Perhaps<AppCmd> {
Ok(command.dispatch(self)?.map(Into::into))
}
#[command(Tracks = "tracks")] pub fn cmd_tracks (&mut self, command: TracksCmd) -> Perhaps<AppCmd> {
Ok(command.dispatch(self)?.map(Into::into))
}
}
impl<T: HasProject> ProjectControl for T {}
#[tek_proc::commands(ProjectCmd)] pub trait ProjectControl: HasProject {
#[command(LoadLast = "load-last")] fn load_last (&mut self) -> Perhaps<ProjectCmd> {
todo!()
}
#[command(Load = "load")] fn load (&mut self) -> Perhaps<ProjectCmd> {
todo!()
}
}
impl<T: HasClock + Namespace<u32> + Namespace<f64> + Namespace<Option<u32>>> ClockControl for T {}
#[tek_proc::commands(ClockCmd = "clock")] pub trait ClockControl: HasClock
+ Namespace<u32>
+ Namespace<f64>
+ Namespace<Option<u32>>
{
#[command(SeekUsec = "usec")] fn seek_usec (&mut self, usec: f64) -> Perhaps<ClockCmd> {
self.clock().playhead.update_from_usec(usec);
Ok(None)
}
#[command(SeekSample = "sample")] fn seek_sample (&mut self, sample: f64) -> Perhaps<ClockCmd> {
self.clock().playhead.update_from_sample(sample);
Ok(None)
}
#[command(SeekPulse = "pulse")] fn seek_pulse (&mut self, pulse: f64) -> Perhaps<ClockCmd> {
self.clock().playhead.update_from_pulse(pulse);
Ok(None)
}
#[command(SetBpm = "bpm")] fn set_bpm (&mut self, bpm: f64) -> Perhaps<ClockCmd> {
Ok(Some(ClockCmd::SetBpm {
bpm: self.clock().timebase().bpm.set(bpm)
}))
}
#[command(SetQuant = "quant")] fn set_quant (&mut self, quant: f64) -> Perhaps<ClockCmd> {
Ok(Some(ClockCmd::SetQuant {
quant: self.clock().quant.set(quant)
}))
}
#[command(SetSync = "sync")] fn set_sync (&mut self, sync: f64) -> Perhaps<ClockCmd> {
Ok(Some(ClockCmd::SetSync {
sync: self.clock().sync.set(sync)
}))
}
#[command(Play = "play")] fn play (&mut self, position: Option<u32>) -> Perhaps<ClockCmd> {
self.clock().play_from(position)?;
Ok(None) /* TODO Some(Pause(previousPosition)) */
}
#[command(Pause = "pause")] fn pause (&mut self, position: Option<u32>) -> Perhaps<ClockCmd> {
self.clock().pause_at(position)?;
Ok(None)
}
#[command(TogglePlayback = "toggle")] fn toggle (&mut self, position: u32) -> Perhaps<ClockCmd> {
Ok(if self.clock().is_rolling() {
self.clock().pause_at(Some(position))?;
None
} else {
self.clock().play_from(Some(position))?;
None
})
}
}
impl<T: HasTracks
+ HasScenes
+ HasSelection
+ HasJack<'static>
+ Namespace<usize>
+ Namespace<Arc<[Connect]>>
+ Namespace<Option<usize>>
+ Namespace<Option<Arc<str>>>
+ Namespace<Option<ItemTheme>>
+ Namespace<Option<Arc<[Connect]>>>
+ Namespace<Option<Vec<Option<Arc<RwLock<MidiClip>>>>>>
> TracksControl for T {}
#[tek_proc::commands(TracksCmd)] pub trait TracksControl: HasTracks
+ HasScenes
+ HasSelection
+ HasJack<'static>
+ Namespace<usize>
+ Namespace<Arc<[Connect]>>
+ Namespace<Option<usize>>
+ Namespace<Option<Arc<str>>>
+ Namespace<Option<ItemTheme>>
+ Namespace<Option<Arc<[Connect]>>>
+ Namespace<Option<Vec<Option<Arc<RwLock<MidiClip>>>>>>
{
/// Stop all playing clips
#[command(Stop = "stop")] fn tracks_stop_all (&mut self) -> Perhaps<TracksCmd> {
for track in self.tracks_mut().iter_mut() {
track.sequencer.enqueue_next(None);
}
Ok(None)
}
/// Launch multiple clips
#[command(Launch = "launch")] fn tracks_launch (
&mut self, clips: Option<Vec<Option<Arc<RwLock<MidiClip>>>>>
) -> Perhaps<TracksCmd> {
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);
}
}
Ok(None)
}
#[command(Add = "add")] fn tracks_add (&mut self) -> Perhaps<TracksCmd>
where Self: HasScenes + HasJack<'static>
{
let (_index, _) = self.tracks_add_one(None, None, [].into(), [].into())?;
Ok(None)
}
#[command(Select = "select")] fn track_select (&mut self) -> Perhaps<TracksCmd> {
let tracks = self.tracks().len();
let select = self.selection_mut().select_track(tracks);
*self.selection_mut() = select;
Ok(None)
}
#[command(Next = "next")] fn track_select_next (&mut self) -> Perhaps<TracksCmd> {
let tracks = self.tracks().len();
let select = self.selection().select_track_next(tracks);
*self.selection_mut() = select;
Ok(None)
}
#[command(Prev = "prev")] fn track_select_prev (&mut self) -> Perhaps<TracksCmd> {
let select = self.selection_mut().select_track_prev();
*self.selection_mut() = select;
Ok(None)
}
}
impl<T: HasTrack
+ Namespace<usize>
+ Namespace<Arc<str>>
+ Namespace<ItemTheme>
+ Namespace<Option<bool>>
> TrackControl for T {}
#[tek_proc::commands(TrackCmd)] pub trait TrackControl: HasTrack
+ Namespace<usize>
+ Namespace<Arc<str>>
+ Namespace<ItemTheme>
+ Namespace<Option<bool>>
{
#[command(Stop = "track/stop")]
fn track_stop (&mut self) -> Perhaps<TrackCmd> {
self.track_mut().map(|track|track.sequencer.enqueue_next(None));
Ok(None)
}
#[command(SetMute = "track/mute")]
fn track_set_mute (&mut self, _mute: Option<bool>) -> Perhaps<TrackCmd> {
todo!()
}
#[command(SetSolo = "track/solo")]
fn track_set_solo (&mut self, _solo: Option<bool>) -> Perhaps<TrackCmd> {
todo!()
}
#[command(SetSize = "track/size")]
fn track_set_size (&mut self, _size: usize) -> Perhaps<TrackCmd> {
todo!()
}
#[command(SetZoom = "track/zoom")]
fn track_set_zoom (&mut self, _zoom: usize) -> Perhaps<TrackCmd> {
todo!()
}
#[command(SetName = "track/name")]
fn track_set_name (&mut self, name: Arc<str>) -> Perhaps<TrackCmd> {
self.track_mut()
.map(|track|swap_value(&mut track.name, &name, |name|TrackCmd::SetName { name }))
.transpose()
.map(Option::flatten)
}
#[command(SetColor = "track/color")]
fn track_set_color (&mut self, color: ItemTheme) -> Perhaps<TrackCmd> {
self.track_mut()
.map(|track|swap_value(&mut track.color, &color, |color|TrackCmd::SetColor { color }))
.transpose()
.map(Option::flatten)
}
#[command(SetRec = "track/rec")]
fn track_set_rec (&mut self, rec: Option<bool>) -> Perhaps<TrackCmd> {
self.track_mut()
.map(|track|toggle_bool(&mut track.sequencer.recording, &rec, |rec|TrackCmd::SetRec { rec }))
.transpose()
.map(Option::flatten)
}
#[command(SetMon = "track/mon")]
fn track_set_mon (&mut self, mon: Option<bool>) -> Perhaps<TrackCmd> {
self.track_mut()
.map(|track|toggle_bool(&mut track.sequencer.monitoring, &mon, |mon|TrackCmd::SetMon { mon }))
.transpose()
.map(Option::flatten)
}
}
impl<T: HasScenes
+ HasTracks
+ HasTrackScroll
+ HasSelection
+ HasJack<'static>
+ Namespace<usize>
+ Namespace<Option<ItemTheme>>
+ Namespace<Option<Arc<str>>>
> ScenesControl for T {}
#[tek_proc::commands(ScenesCmd)] pub trait ScenesControl: HasScenes
+ HasTracks
+ HasTrackScroll
+ HasSelection
+ HasJack<'static>
+ Namespace<usize>
+ Namespace<Option<ItemTheme>>
+ Namespace<Option<Arc<str>>>
{
#[command(Add = "add")] fn scenes_add (&mut self) -> Perhaps<ScenesCmd> {
let (_index, _) = self.scenes_add_one(None, None)?;
Ok(None)
}
#[command(Select = "select")] fn scene_select (&mut self) -> Perhaps<ScenesCmd> {
let scenes = self.scenes().len();
let select = self.selection_mut().select_scene(scenes);
*self.selection_mut() = select;
Ok(None)
}
#[command(Next = "next")] fn scene_select_next (&mut self) -> Perhaps<ScenesCmd> {
let scenes = self.scenes().len();
let select = self.selection().select_scene_next(scenes);
*self.selection_mut() = select;
Ok(None)
}
#[command(Prev = "prev")] fn scene_select_prev (&mut self) -> Perhaps<ScenesCmd> {
let select = self.selection_mut().select_scene_prev();
*self.selection_mut() = select;
Ok(None)
}
}
impl<T: HasScene
+ Namespace<usize>
+ Namespace<Arc<str>>
+ Namespace<ItemTheme>
> SceneControl for T {}
#[tek_proc::commands(SceneCmd)] pub trait SceneControl: HasScene
+ Namespace<usize>
+ Namespace<Arc<str>>
+ Namespace<ItemTheme>
{
#[command(SetSize = "size")] fn scene_set_size (&mut self, _size: usize) -> Perhaps<SceneCmd> {
todo!()
}
#[command(SetZoom = "zoom")] fn scene_set_zoom (&mut self, _size: usize) -> Perhaps<SceneCmd> {
todo!()
}
#[command(SetName = "name")] fn scene_set_name (&mut self, name: Arc<str>) -> Perhaps<SceneCmd> {
Ok(self.scene_mut().map(|scene|swap_value(
&mut scene.name,
&name,
|name|SceneCmd::SetName { name }
)).transpose()?.flatten())
}
#[command(SetColor = "color")] fn scene_set_color (&mut self, color: ItemTheme) -> Perhaps<SceneCmd> {
Ok(self.scene_mut().map(|scene|swap_value(
&mut scene.color,
&color,
|color|SceneCmd::SetColor { color }
)).transpose()?.flatten())
}
}
#[tek_proc::commands(SamplerCmd = "sampler")] pub trait SamplerControl: HasSampler
+ Namespace<usize>
{
#[command(RecordToggle = "rec-toggle")]
fn record_toggle (&mut self, slot: usize) -> Perhaps<SamplerCmd>
where Self: Sized
{
let sampler = self.sampler_mut();
let recording = sampler.recording.as_ref().map(|x|x.0);
let _ = SamplerCmd::RecordFinish.dispatch(self)?;
// autoslice: continue recording at next slot
if recording != Some(slot) {
SamplerCmd::RecordBegin { slot }.dispatch(self)
} else {
Ok(None)
}
}
#[command(RecordBegin = "rec-begin")] fn record_begin (&mut self, slot: usize) -> Perhaps<SamplerCmd> {
let sampler = self.sampler_mut();
sampler.recording = Some((
slot,
Some(Arc::new(RwLock::new(Sample::new(
"Sample", 0, 0, vec![vec![]; sampler.audio_ins.len()]
))))
));
Ok(None)
}
#[command(RecordFinish = "rec-finish")] fn record_finish (&mut self) -> Perhaps<SamplerCmd> {
let sampler = self.sampler_mut();
let _prev_sample = sampler.recording.as_mut().map(|(index, sample)|{
std::mem::swap(sample, &mut sampler.samples.0[*index]);
sample
}); // TODO: undo
Ok(None)
}
#[command(RecordCancel = "rec-cancel")] fn record_cancel (&mut self) -> Perhaps<SamplerCmd> {
self.sampler_mut().recording = None;
Ok(None)
}
#[command(PlaySample = "sample-play")] fn sample_play (&mut self, slot: usize) -> Perhaps<SamplerCmd> {
let sampler = self.sampler_mut();
if let Some(ref sample) = sampler.samples.0[slot] {
sampler.voices.try_write().unwrap().push(Sample::play(sample, 0, &u7::from(128)));
}
Ok(None)
}
#[command(StopSample = "sample-stop")] fn sample_stop (&mut self, slot: usize) -> Perhaps<SamplerCmd> {
todo!();
//Ok(None)
}
}
impl<T: HasEditor
+ Namespace<u32>
+ Namespace<f64>
+ Namespace<bool>
+ Namespace<usize>
+ Namespace<Option<u32>>
+ Namespace<Option<Arc<RwLock<MidiClip>>>>
> MidiEditControl for T {}
#[tek_proc::commands(MidiEditCmd)] pub trait MidiEditControl: HasEditor
+ Namespace<u32>
+ Namespace<f64>
+ Namespace<bool>
+ Namespace<usize>
+ Namespace<Option<u32>>
+ Namespace<Option<Arc<RwLock<MidiClip>>>>
{
#[command(Toggle = "toggle")] fn edit_toggle (&mut self) -> Perhaps<MidiEditCmd> {
if self.is_editing() {
*self.editor_mut() = None;
} else {
todo!()
}
Ok(Some(MidiEditCmd::Toggle))
}
#[command(Show = "show")] fn edit_show (&mut self, clip: Option<Arc<RwLock<MidiClip>>>) -> Perhaps<MidiEditCmd> {
Ok(self.editor_mut().as_mut().map(|mut editor|{
editor.set_clip(clip.as_ref());
editor.redraw();
None
}).flatten())
}
#[command(DeleteNote = "delete")] fn edit_note_delete (&mut self) -> Perhaps<MidiEditCmd> {
Ok(self.editor_mut().as_mut().map(|editor|{
editor.redraw();
todo!()
}).flatten())
}
#[command(AppendNote = "append")] fn edit_note_append (&mut self, advance: bool) -> Perhaps<MidiEditCmd> {
Ok(self.editor_mut().as_mut().map(|mut editor|{
editor.put_note(advance);
editor.redraw();
None
}).flatten())
}
#[command(SetNotePos = "note-pos")] fn edit_note_set_pos (&mut self, pos: usize) -> Perhaps<MidiEditCmd> {
Ok(self.editor_mut().as_mut().map(|editor|{
editor.set_note_pos((pos).min(127));
editor.redraw();
None
}).flatten())
}
#[command(SetNoteLen = "note-len")] fn edit_note_set_len (&mut self, len: usize) -> Perhaps<MidiEditCmd> {
Ok(self.editor_mut().as_mut().map(|editor|{
editor.set_note_len(len);
editor.redraw();
None
}).flatten())
}
#[command(SetNoteScroll = "note-scroll")] fn edit_note_set_scroll (&mut self, scroll: usize) -> Perhaps<MidiEditCmd> {
Ok(self.editor_mut().as_mut().map(|editor|{
editor.set_note_lo((scroll).min(127));
editor.redraw();
None
}).flatten())
}
#[command(SetTimePos = "time-pos")] fn edit_time_set_pos (&mut self, pos: usize) -> Perhaps<MidiEditCmd> {
Ok(self.editor_mut().as_mut().map(|editor|{
editor.set_time_pos(pos);
editor.redraw();
None
}).flatten())
}
#[command(SetTimeScroll = "time-scroll")] fn edit_time_set_scroll (&mut self, scroll: usize) -> Perhaps<MidiEditCmd> {
Ok(self.editor_mut().as_mut().map(|editor|{
editor.set_time_start(scroll);
editor.redraw();
None
}).flatten())
}
#[command(SetTimeZoom = "time-zoom")] fn edit_time_set_zoom (&mut self, zoom: usize) -> Perhaps<MidiEditCmd> {
Ok(self.editor_mut().as_mut().map(|editor|{
editor.set_time_zoom(zoom);
editor.redraw();
None
}).flatten())
}
#[command(SetTimeLock = "time-lock")] fn edit_time_set_lock (&mut self, lock: bool) -> Perhaps<MidiEditCmd> {
Ok(self.editor_mut().as_mut().map(|editor|{
editor.set_time_lock(lock);
editor.redraw();
None
}).flatten())
}
// TODO: 1-9 seek markers that by default start every 8th of the clip
}
impl<T: HasPool
+ Namespace<bool>
+ Namespace<usize>
+ Namespace<Arc<str>>
+ Namespace<PathBuf>
+ Namespace<MidiClip>
+ Namespace<ItemColor>
+ Namespace<BrowseCmd>
> PoolControl for T {}
#[tek_proc::commands(PoolCmd = "pool")] pub trait PoolControl: HasPool
+ Namespace<bool>
+ Namespace<usize>
+ Namespace<Arc<str>>
+ Namespace<PathBuf>
+ Namespace<MidiClip>
+ Namespace<ItemColor>
+ Namespace<BrowseCmd>
{
/// Toggle visibility of pool
#[command(Show = "show")] fn show (&mut self, visible: bool) -> Perhaps<PoolCmd> {
self.pool_mut().visible = visible;
Ok(Some(PoolCmd::Show { visible: !visible }))
}
/// Set selected clip
#[command(Select = "select")] fn select (&mut self, index: usize) -> Perhaps<PoolCmd> {
self.pool_mut().set_clip_index(index);
Ok(None)
}
/// Delete a clip from the pool
#[command(Delete = "delete")] fn delete (&mut self, index: usize) -> Perhaps<PoolCmd> {
let clip = self.pool_mut().clips_mut().remove(index).try_read().unwrap().clone();
Ok(Some(PoolCmd::Add { index, clip }))
}
/// Switch places of two clips in pool
#[command(Swap = "swap")] fn swap (&mut self, index: usize, other: usize) -> Perhaps<PoolCmd> {
self.pool_mut().clips_mut().swap(index, other);
Ok(Some(PoolCmd::Swap { index, other }))
}
/// Add clip to pool
#[command(Add = "add")] fn clip_add (&mut self, index: usize, clip: MidiClip) -> Perhaps<PoolCmd> {
let mut index = index;
let clip = Arc::new(RwLock::new(clip.clone()));
let mut clips = self.pool_mut().clips_mut();
if index >= clips.len() {
index = clips.len();
clips.push(clip)
} else {
clips.insert(index, clip);
}
Ok(Some(PoolCmd::Delete { index }))
}
/// Set name of clip
#[command(SetName = "set-name")] fn clip_set_name (&mut self, index: usize, name: Arc<str>) -> Perhaps<PoolCmd> {
let clip = &mut self.pool_mut().clips_mut()[index];
let old_name = clip.try_read().unwrap().name.clone();
clip.try_write().unwrap().name = name.clone();
Ok(Some(PoolCmd::SetName { index, name: old_name }))
}
/// Set length of clip
#[command(SetLength = "set-length")] fn clip_set_length (&mut self, index: usize, length: usize) -> Perhaps<PoolCmd> {
let clip = &mut self.pool_mut().clips_mut()[index];
let old_len = clip.try_read().unwrap().length;
clip.try_write().unwrap().length = length;
Ok(Some(PoolCmd::SetLength { index, length: old_len }))
}
/// Set color of clip
#[command(SetColor = "set-color")] fn clip_set_color (&mut self, index: usize, color: ItemColor) -> Perhaps<PoolCmd> {
let mut color = ItemTheme::from(color);
std::mem::swap(&mut color, &mut self.pool().clips()[index].try_write().unwrap().color);
Ok(Some(PoolCmd::SetColor { index, color: color.base }))
}
/// Enter crop mode
#[command(CropBegin = "crop/begin")] fn crop_begin (&mut self) -> Perhaps<PoolCmd> {
let index = self.pool().clip_index();
let length = self.pool().clips()[index].try_read().unwrap().length;
*self.pool_mut().mode_mut() = Some(PoolMode::Length(index, length, ClipLengthFocus::Bar));
Ok(None)
}
/// Exit crop mode, discard
#[command(CropCancel = "crop/cancel")] fn crop_cancel (&mut self) -> Perhaps<PoolCmd> {
if let Some(PoolMode::Length(..)) = self.pool_mut().mode_mut().clone() {
*self.pool_mut().mode_mut() = None;
}
Ok(None)
}
#[command(CropSet = "crop/set")] fn crop_set (&mut self, _length: usize) -> Perhaps<PoolCmd> {
if let Some(PoolMode::Length(clip, ref mut length, ref mut _focus))
= self.pool_mut().mode_mut().clone()
{
let old_length;
{
let clip = self.pool().clips()[clip].clone();//.try_write().unwrap();
old_length = Some(clip.try_read().unwrap().length);
clip.try_write().unwrap().length = *length;
}
*self.pool_mut().mode_mut() = None;
return Ok(old_length.map(|l|PoolCmd::CropSet { _length: l }))
}
Ok(None)
}
#[command(CropNext = "crop/next")] fn crop_next (&mut self) -> Perhaps<PoolCmd> {
if let Some(PoolMode::Length(
_clip, ref mut _length, ref mut focus
)) = self.pool_mut().mode_mut().clone() {
focus.next()
};
Ok(None)
}
#[command(CropPrev = "crop/prev")] fn crop_prev (&mut self) -> Perhaps<PoolCmd> {
if let Some(PoolMode::Length(
_clip, ref mut _length, ref mut focus
)) = self.pool_mut().mode_mut().clone() {
focus.prev()
};
Ok(None)
}
#[command(CropInc = "crop/inc")] fn crop_inc (&mut self) -> Perhaps<PoolCmd> {
if let Some(PoolMode::Length(
_clip, ref mut length, ref mut focus
)) = self.pool_mut().mode_mut().clone() {
match focus {
ClipLengthFocus::Bar => { *length += 4 * PPQ },
ClipLengthFocus::Beat => { *length += PPQ },
ClipLengthFocus::Tick => { *length += 1 },
}
}
Ok(None)
}
#[command(CropDec = "crop/dec")] fn crop_dec (&mut self) -> Perhaps<PoolCmd> {
if let Some(PoolMode::Length(
_clip, ref mut length, ref mut focus
)) = self.pool_mut().mode_mut().clone() {
match focus {
ClipLengthFocus::Bar => { *length = length.saturating_sub(4 * PPQ) },
ClipLengthFocus::Beat => { *length = length.saturating_sub(PPQ) },
ClipLengthFocus::Tick => { *length = length.saturating_sub(1) },
}
}
Ok(None)
}
/// Enter rename mode
#[command(RenameBegin = "rename/begin")] fn rename_begin (&mut self) -> Perhaps<PoolCmd> {
let index = self.pool().clip_index();
let name = self.pool().clips()[index].try_read().unwrap().name.clone();
*self.pool_mut().mode_mut() = Some(PoolMode::Rename(index, name));
Ok(None)
}
/// Exit rename mode, discard
#[command(RenameCancel = "rename/cancel")] fn rename_cancel (&mut self) -> Perhaps<PoolCmd> {
if let Some(PoolMode::Rename(clip, ref mut old_name)) = self.pool_mut().mode_mut().clone() {
self.pool().clips()[clip].try_write().unwrap().name = old_name.clone().into();
}
Ok(None)
}
/// Exit rename mode, update name
#[command(RenameConfirm = "rename/confirm")] fn rename_confirm (&mut self) -> Perhaps<PoolCmd> {
Ok(if let Some(PoolMode::Rename(_clip, ref mut old_name)) = self.pool_mut().mode_mut().clone() {
let old_name = old_name.clone();
*self.pool_mut().mode_mut() = None;
Some(PoolCmd::RenameSet { value: old_name })
} else {
None
})
}
#[command(RenameSet = "rename/set")] fn rename_set (&mut self, value: Arc<str>) -> Perhaps<PoolCmd> {
if let Some(PoolMode::Rename(clip, ref mut _old_name)) = self.pool_mut().mode_mut().clone() {
self.pool().clips()[clip].try_write().unwrap().name = value.clone();
}
Ok(None)
}
///// Import from file
//#[command(Browse = "browse")]
//fn browse (&mut self, command: BrowseCmd) -> Perhaps<PoolCmd> {
//Ok(if let Some(browse) = self.pool_mut().browse.as_mut() {
//command.act(browse)?.map(|command|PoolCmd::Browse{command})
//} else {
//None
//})
//}
/// Import clip to pool from file
#[command(Import = "import")]
fn clip_import (&mut self, index: usize, path: PathBuf) -> Perhaps<PoolCmd>
where Self: Sized
{
let bytes = std::fs::read(&path)?;
let smf = Smf::parse(bytes.as_slice())?;
let mut t = 0u32;
let mut events = vec![];
for track in smf.tracks.iter() {
for event in track.iter() {
t += event.delta.as_int();
if let TrackEventKind::Midi { channel, message } = event.kind {
events.push((t, channel.as_int(), message));
}
}
}
let mut clip = MidiClip::new("imported", true, t as usize + 1, None, None);
for event in events.iter() {
clip.notes[event.0 as usize].push(event.2);
}
Ok(PoolCmd::Add { index, clip }.dispatch(self)?)
}
///// Export to file
//#[command(Export = "export")]
//fn export (&mut self, command: BrowseCmd) -> Perhaps<PoolCmd> {
//Ok(if let Some(browse) = self.pool_mut().browse.as_mut() {
//command.act(browse)?.map(|command|PoolCmd::Export{command})
//} else {
//None
//})
//}
}
impl<T> ModeControl for T where T: HasModeStack
+ Namespace<Vec<Arc<str>>>
+ Namespace<Arc<str>>
+ Namespace<usize>
{}
#[tek_proc::commands(ModeCmd)] pub trait ModeControl: HasModeStack
+ Namespace<Vec<Arc<str>>>
+ Namespace<Arc<str>>
+ Namespace<usize>
{
#[command(Reset = "reset")] fn mode_reset (&mut self, modes: Vec<Arc<str>>) -> Perhaps<ModeCmd> {
*self.modes_mut() = modes.into();
Ok(None)
}
#[command(Push = "push")] fn mode_push (&mut self, mode: Arc<str>) -> Perhaps<ModeCmd> {
self.modes_mut().push(mode);
Ok(Some(ModeCmd::Pop))
}
#[command(Pop = "pop")] fn mode_pop (&mut self) -> Perhaps<ModeCmd> {
let mode = self.modes_mut().pop();
Ok(mode.map(|mode|ModeCmd::Push { mode }))
}
}
impl<T: HasClips + Namespace<Option<bool>> + Namespace<Option<ItemTheme>> > MidiClipControl for T {}
#[tek_proc::commands(MidiClipCmd)] pub trait MidiClipControl: HasClips
+ Namespace<Option<bool>>
+ Namespace<Option<ItemTheme>>
{
/// Create a clip at the current position if it does not already exist.
#[command(Upsert = "upsert")] fn clip_upsert (&mut self) -> Perhaps<MidiClipCmd> {
let clip = self.clip();
//let old = self.scenes[scene].clips[track].clone();
//self.scenes[scene].clips[track] = clip;
//old
todo!()
}
#[command(SetColor = "color")] fn clip_set_color (&mut self, color: Option<ItemTheme>) -> Perhaps<MidiClipCmd> {
Ok(self.clip().map(|clip|MidiClipCmd::SetColor {
color: Some(clip.write().set_color(color))
}))
}
/// Toggle looping for the active clip
#[command(SetLoop = "loop")] fn clip_toggle_loop (&mut self, looping: Option<bool>) -> Perhaps<MidiClipCmd> {
Ok(self.clip().map(|clip|MidiClipCmd::SetLoop {
looping: Some(clip.write().toggle_loop(looping))
}))
}
}
impl<T: Namespace<usize> + Namespace<PathBuf> + Namespace<Arc<str>>> BrowseControl for T {}
#[tek_proc::commands(BrowseCmd = "browse")] pub trait BrowseControl:
Namespace<usize> +
Namespace<PathBuf> +
Namespace<Arc<str>>
{
/// Toggle visibility of browser
#[command(Show = "show")] fn show (&mut self) -> Perhaps<BrowseCmd> {
todo!()
}
/// Set current directory
#[command(Chdir = "chdir")] fn chdir (&mut self, _path: PathBuf) -> Perhaps<BrowseCmd> {
todo!()
}
/// Set current filter
#[command(SetSearch = "set-search")] fn set_search (&mut self, _filter: Arc<str>) -> Perhaps<BrowseCmd> {
todo!()
}
/// Set selected item
#[command(SetCursor = "set-cursor")] fn set_cursor (&mut self, _index: usize) -> Perhaps<BrowseCmd> {
todo!()
}
}
#[tek_proc::commands(AxisCmd)]
impl App {
/// Increment a given data axis.
#[command(Inc = "inc")]
pub fn inc (&mut self, axis: ControlAxis) -> Perhaps<AxisCmd> {
Ok(match (&self.dialog, axis) {
(Dialog::None, _) => todo!(),
(Dialog::Menu(_, _), ControlAxis::Y) => {
AppCmd::Dialog { dialog: self.dialog.menu_next() }.dispatch(self)?;
None
},
_ => todo!()
})
}
/// Decrement a given data axis.
#[command(Dec = "dec")]
pub fn dec (&mut self, axis: ControlAxis) -> Perhaps<AxisCmd> {
Ok(match (&self.dialog, axis) {
(Dialog::None, _) => None,
(Dialog::Menu(_, _), ControlAxis::Y) => {
AppCmd::Dialog { dialog: self.dialog.menu_prev() }.dispatch(self)?;
None
},
_ => todo!()
})
}
}

553
src/tek_view.rs Normal file
View file

@ -0,0 +1,553 @@
use crate::*;
pub fn draw_sample (
to: &mut Tui, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool
) -> Usually<usize> {
let style = if focus { Style::default().green() } else { Style::default() };
if focus {
to.blit(&"🬴", x+1, y, Some(style.bold()));
}
let label1 = format!("{:3} {:12}",
note.map(|n|n.to_string()).unwrap_or(String::default()),
sample.name);
let label2 = format!("{:>6} {:>6} +0.0",
sample.start,
sample.end);
to.blit(&label1, x+2, y, Some(style.bold()));
to.blit(&label2, x+3+label1.len()as u16, y, Some(style));
Ok(label1.len() + label2.len() + 4)
}
pub fn draw_sample_list_item (sample: &Option<Arc<RwLock<Sample>>>) -> String {
if let Some(sample) = sample {
let sample = sample.try_read().unwrap();
format!("{:8}", sample.name)
//format!("{:8} {:3} {:6}-{:6}/{:6}",
//sample.name,
//sample.gain,
//sample.start,
//sample.end,
//sample.channels[0].len()
//)
} else {
String::from("........")
}
}
pub fn draw_sample_viewer (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> + use<'_> {
let min_db = -64.0;
draw(move|to: &mut Tui|{
let xywh = to.area().into();
let XYWH(x, y, width, height) = xywh;
let area = Rect { x, y, width, height };
if let Some(sample) = &sample {
let sample = sample.try_read().unwrap();
let start = sample.start as f64;
let end = sample.end as f64;
let length = end - start;
let step = length / width as f64;
let mut t = start;
let mut lines = vec![];
while t < end {
let chunk = &sample.channels[0][t as usize..((t + step) as usize).min(sample.end)];
let total: f32 = chunk.iter().map(|x|x.abs()).sum();
let count = chunk.len() as f32;
let meter = 10. * (total / count).log10();
let x = t as f64;
let y = meter as f64;
lines.push(Line::new(x, min_db, x, y, Color::Green));
t += step / 2.;
}
to.buffer().map(|buf|{
Canvas::default()
.x_bounds([sample.start as f64, sample.end as f64])
.y_bounds([min_db, 0.])
.paint(|ctx| {
for line in lines.iter() {
ctx.draw(line);
}
//FIXME: proportions
//let text = "press record to finish sampling";
//ctx.print(
//(width - text.len() as u16) as f64 / 2.0,
//height as f64 / 2.0,
//text.red()
//);
})
.render(area, buf);
});
} else {
to.buffer().map(|buf|{
Canvas::default()
.x_bounds([0.0, width as f64])
.y_bounds([0.0, height as f64])
.paint(|_ctx| {
//let text = "press record to begin sampling";
//ctx.print(
//(width - text.len() as u16) as f64 / 2.0,
//height as f64 / 2.0,
//text.red()
//);
})
.render(area, buf);
});
}
Ok(Some(xywh))
})
}
pub fn draw_meters (meters: &[f32]) -> impl Draw<Tui> + use<'_> {
bg(Black, iter_east_fixed(1, ||meters.iter(), |value, _index|{
RmsMeter(*value).full_h()
}).exact_w(2))
}
#[cfg(feature = "lv2")]
pub fn draw_lv2_header (state: &Lv2, to: &mut Tui, x: u16, y: u16, w: u16) {
let style = Style::default().gray();
let label1 = format!(" {}", state.name);
to.blit(&label1, x + 1, y, Some(style.white().bold()));
if let Some(ref path) = state.path {
let label2 = format!("{}…", &path[..((w as usize - 10).min(path.len()))]);
to.blit(&label2, x + 2 + label1.len() as u16, y, Some(style.not_dim()));
}
//Ok(Rect { x, y, width: w, height: 1 })
}
pub fn draw_status_bar (to: &mut Tui, state: &App) -> Drawn<u16> {
bar(Split::South, 1u16,
dim(true, "Mode"),
bold(true, state.modes.read().get(0))
).push_x(1).draw(to)
}
/// ```
/// let x = "";
/// let _ = tek::view_status(None, x.as_ref(), x.as_ref(), x.as_ref());
/// let _ = tek::view_status(Some("".into()), x.as_ref(), x.as_ref(), x.as_ref());
/// ```
pub fn view_status <'a> (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(
sel.map(|sel|field_h(theme, "Selected", sel)).align_w().full_wh(),
east!(sr, buf, lat).align_e().full_wh(),
)))
}
pub fn view_device <'a> (state: &'a App) -> impl Draw<Tui> {
let selected = state.dialog.device_kind().unwrap();
south(
bold(true, "Add device"),
iter_south(move||device_kinds().iter().enumerate().map(move|(i, _label)|{
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 { " " };
bg(b, east(l, west(r, "FIXME device name"))).full_w()
})))
}
pub fn view_track_row_section <'a> (
_theme: ItemTheme,
button: impl Draw<Tui>,
button_add: impl Draw<Tui>,
content: impl Draw<Tui>,
) -> impl Draw<Tui> {
bar(
Split::West,
4u16,
button_add.align_ne().full_wh(),
bar(
Split::East,
20u16,
button.full_wh().align_nw(),
content.align_c().full_wh()
)
)
}
pub fn view_track_header <'a> (theme: ItemTheme, content: impl Draw<Tui>)
-> impl Draw<Tui>
{
bg(theme.darker.term, content.align_e().full_w()).exact_w(12)
}
pub fn view_scene_name <'a> (
select: &Selection,
editor: Option<&'a MidiEditor>,
index: usize,
scene: &Scene,
editing: bool
) -> impl Draw<Tui> {
let h = if select.scene() == Some(index) && let Some(_editor) = editor {
7
} else {
Scene::DEFAULT_HEIGHT as u16
};
let a = east!("·s", index, " ", fg(g(255), bold(true, &scene.name))).align_w().full_w();
let b = when(select.scene() == Some(index) && editing, south(
editor.as_ref().map(|e|e.clip_status()),
editor.as_ref().map(|e|e.edit_status())).align_nw().full_wh());
let c = if select.scene() == Some(index) {
scene.color.light.term
} else {
scene.color.base.term
};
bg(c, south(a, b).align_nw().exact_h(h))
}
pub fn view_scene_name_theme (scene: &Scene, track_index: usize) -> (Arc<str>, ItemTheme) {
if let Some(Some(clip)) = &scene.clips.get(track_index) {
let clip = clip.try_read().unwrap();
(format!(" ⏹ {}", &clip.name).into(), clip.color)
} else {
(" ⏹ -- ".into(), ItemTheme::G[32])
}
}
pub fn view_scene_bg (
theme: ItemTheme, select: &Selection, track_index: usize, scene_index: usize
) -> (Color, Color) {
let mut outline = theme.base.term;
(if select.track() == Some(track_index) && select.scene() == Some(scene_index) {
outline = theme.lighter.term;
theme.light.term
} else if select.track() == Some(track_index) || select.scene() == Some(scene_index) {
outline = theme.darkest.term;
theme.base.term
} else {
theme.dark.term
}, outline)
}
pub fn view_scene_w (
track: &Track, select: &Selection, track_index: usize, editor: Option<&MidiEditor>
) -> u16 {
if select.track() == Some(track_index) && let Some(editor) = editor {
(editor.size.w() as usize).max(24).max(track.width) as u16
} else {
track.width as u16
}
}
pub fn view_scene_y (
select: &Selection, scene_index: usize, editor: Option<&MidiEditor>
) -> u16 {
if select.scene() == Some(scene_index) && let Some(editor) = editor {
editor.size.h().max(12)
} else {
Scene::DEFAULT_HEIGHT as u16
}
}
pub fn view_scene_sel (select: &Selection, track_index: usize, scene_index: usize, editing: bool) -> bool {
editing && select.track() == Some(track_index) && select.scene() == Some(scene_index)
}
pub fn view_templates <'a> (state: &'a App) -> impl Draw<Tui> + use<'a> {
let height = (state.config.modes.len() * 2) as u16;
draw(move |to: &mut Tui|{
let mut index = 0;
state.config.modes.for_each(&mut |id: &str, profile: &Mode| {
let b = 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 = fg(fg1, name).align_w().full_w();
let field_id = fg(fg2, id).align_e().full_w();
let field_info = info.align_w().full_w();
let _ = bg(b, south(above(field_name, field_id), field_info))
.full_w().exact_h(2).push_y((2 * index) as u16).draw(to);
index += 1;
});
Ok(Some(to.area().into()))
}).min_w(30).exact_h(height)
}
pub fn view_browse_title (state: &App) -> impl Draw<Tui> {
field_v(ItemTheme::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:",
}, fg(g(96), x_repeat("🭻")).exact_h(1)).align_w().full_w()
}
/// ```
/// 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>
{
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, bg(c, ()).exact_wh(w, 1))
}
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_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_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)));
border(true, frame, field_v(theme, title, iter_south({
move||ports.iter().enumerate().map(|(index, port)|{
east!(" ", index, " ", port.port_name()).align_w().full_h()
})
})).exact_wh(20, 1 + ins)).exact_wh(20, 1 + ins)
}
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>, _| south(
bold(true, fg_bg(fg, bg, east(" 󰣲 ", name).align_w())).full_h(),
iter(||connections.iter(), move|connect: &'a Connect, index|{
bold(false, fg_bg(fg, bg, &connect.info)).exact_h(1).align_w().push_y(index as u16)
})
).exact_h((y2 - y) as u16).push_y(y as u16))
}
pub fn view_sample_info (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> + use<'_> {
when(sample.is_some(), draw(move|to: &mut Tui|{
let sample = sample.unwrap().try_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 = draw(move|to: &mut Tui|{
let sample = sample.unwrap().try_read().unwrap();
let theme = sample.color;
south!(
field_h(theme, "Name ", format!("{:<10}", sample.name.clone())) .align_w().full_w(),
field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())).align_w().full_w(),
field_h(theme, "Start ", format!("{:<8}", sample.start)) .align_w().full_w(),
field_h(theme, "End ", format!("{:<8}", sample.end)) .align_w().full_w(),
field_h(theme, "Trans ", "0") .align_w().full_w(),
field_h(theme, "Gain ", format!("{}", sample.gain)) .align_w().full_w(),
).exact_w(20).draw(to)
});
let b = draw(|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.try_read().unwrap();
format!("Sample {}-{}", sample.start, sample.end)
})
.unwrap_or_else(||"No sample".to_string())))
}
/// ```
/// let _ = tek::button_2("", "", true);
/// let _ = tek::button_2("", "", false);
/// ```
pub fn button_2 <'a> (key: impl Draw<Tui>, label: impl Draw<Tui>, hide: bool)
-> impl Draw<Tui>
{
let c1 = tui_orange();
let c2 = tui_g(0);
let c3 = tui_g(96);
let c4 = tui_g(255);
bold(true, fg_bg(c1, c2,
bar(Split::East, 1u16, fg(c2, "▐"),
bar(Split::East, 1u16, key,
bar(Split::East, 1u16, fg(c3, "▐"),
when(!hide, fg_bg(c4, c3, label)))))))
}
/// ```
/// let _ = tek::button_3("", "", "", true);
/// let _ = tek::button_3("", "", "", false);
/// ```
pub fn button_3 <'a> (
key: impl Draw<Tui>,
label: impl Draw<Tui>,
value: impl Draw<Tui>,
editing: bool,
) -> impl Draw<Tui> {
let c1 = tui_orange();
let c2 = tui_g(0);
let c3 = tui_g(96);
let c4 = tui_g(255);
let c5 = tui_g(128);
bold(true, bar(Split::East, 3u16,
fg_bg(c1, c2,
bar(Split::East, 1u16, fg(c2, "▐"),
bar(Split::East, 1u16, key, fg(if editing { c5 } else { c3 }, "▐")))),
east(
when(!editing, east(fg_bg(c4, c3, label), fg_bg(c5, c3, "▐"),)),
east(fg_bg(tui_g(224), c5, value), fg_bg(c5, Reset, "▌"), ))))
}
pub fn field_h <'a, T: Screen> (
_theme: ItemTheme, _head: impl Draw<T>, _body: impl Draw<T>
) -> impl Draw<T> {
}
pub fn field_v <'a, T: Screen> (
_theme: ItemTheme, _head: impl Draw<T>, _body: impl Draw<T>
) -> impl Draw<T> {
}
pub fn view_sessions <'a> () -> impl Draw<Tui> {
let h = 6;
let w = Some(30);
let f = Rgb(224, 192, 128);
draw(move |to: &mut Tui|{
for (index, name) in ["session1", "session2", "session3"].iter().enumerate() {
let b = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) };
let y = (2 * index) as u16;
let h = 2;
bg(b, fg(f, *name).align_w()).full_w().exact_h(h).push_y(y).draw(to)?;
}
Ok(Some(to.area().into()))
}).min_w(w).exact_h(h)
}
/// ```
/// 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 <'a> (bg: Color, fg: Color, content: impl Draw<Tui>)
-> impl Draw<Tui>
{
let left = fg_bg(bg, Reset, y_repeat("▐").exact_w(1));
let right = fg_bg(bg, Reset, y_repeat("▌").exact_w(1));
east(left, west(right, fg_bg(fg, bg, content)))
}
#[macro_export] macro_rules! rewrite {
($buf:ident, $($rest:tt)*) => {
|$buf,_,_|{
$buf.clear();
write!($buf, $($rest)*)
}
};
}
pub struct Memo<T, U> {
value: T,
view: U,
}
impl<T, U> Memo<T, U> {
pub fn new (value: T, view: U) -> Self {
Self { value, view }
}
}
impl<T: PartialEq + Debug, U: Debug> Debug for Memo<T, U> {
fn fmt (&self, _: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
todo!()
}
}
impl<T: PartialEq, U> Memo<T, U> {
pub fn update <R> (&mut self, newval: T, render: impl Fn(&mut U, &T, &T)->R) -> Option<R> {
if newval != self.value {
let result = render(&mut self.view, &newval, &self.value);
self.value = newval;
return Some(result);
}
None
}
}
pub trait Gettable<T> {
/// Returns current value
fn get (&self) -> T;
}
pub trait Mutable<T>: Gettable<T> {
/// Sets new value, returns old
fn set (&mut self, value: T) -> T;
}
pub trait InteriorMutable<T>: Gettable<T> {
/// Sets new value, returns old
fn set (&self, value: T) -> T;
}
#[cfg(test)] mod test_view {
use super::*;
use proptest::prelude::*;
proptest! {
#[test] fn proptest_view_meter (
label in "\\PC*", value in f32::MIN..f32::MAX
) {
let _ = view_meter(&label, value);
}
#[test] fn proptest_view_meters (
value1 in f32::MIN..f32::MAX,
value2 in f32::MIN..f32::MAX
) {
let _ = view_meters(&[value1, value2]);
}
}
}