Compare commits

..

4 commits

Author SHA1 Message Date
i do not exist
07f2290017 woo
Some checks failed
/ build (push) Has been cancelled
2026-07-26 22:34:06 +03:00
i do not exist
8144d75abe move port and midi to tengri 2026-07-26 21:50:13 +03:00
i do not exist
711e4fe6a9 simpler value getters 2026-07-25 08:46:53 +03:00
i do not exist
b19d5bef24 use #[namespace] 2026-07-25 07:14:09 +03:00
28 changed files with 2105 additions and 2586 deletions

1138
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -22,7 +22,6 @@ backtrace = { version = "0.3.72" }
builder-pattern = { version = "0.4.2" } builder-pattern = { version = "0.4.2" }
bumpalo = { version = "3.19.0" } bumpalo = { version = "3.19.0" }
konst = { version = "0.3.16", features = [ "rust_1_83" ] } konst = { version = "0.3.16", features = [ "rust_1_83" ] }
midly = { version = "0.5" }
palette = { version = "0.7.6", features = [ "random" ] } palette = { version = "0.7.6", features = [ "random" ] }
quanta = { version = "0.12.3" } quanta = { version = "0.12.3" }
rand = { version = "0.8.5" } rand = { version = "0.8.5" }
@ -36,6 +35,8 @@ winit = { optional = true, version = "0.30.4", features = [ "x11" ]}
wavers = { optional = true, version = "1.4.3" } wavers = { optional = true, version = "1.4.3" }
uuid = { optional = true, version = "1.10.0", features = [ "v4" ] } uuid = { optional = true, version = "1.10.0", features = [ "v4" ] }
gtk = { optional = true, version = "0.18.1" } gtk = { optional = true, version = "0.18.1" }
notify = "8.2.0"
notify-debouncer-mini = "0.7.0"
#once_cell = "1.19.0" #once_cell = "1.19.0"
#no_deadlocks = "1.3.2" #no_deadlocks = "1.3.2"

View file

@ -1,4 +1,4 @@
export RUSTFLAGS := "--cfg procmacro2_semver_exempt -Zmacro-backtrace -Clink-arg=-fuse-ld=mold" #export RUSTFLAGS := "--cfg procmacro2_semver_exempt -Zmacro-backtrace -Clink-arg=-fuse-ld=mold"
export RUST_BACKTRACE := "1" export RUST_BACKTRACE := "1"
default +ARGS="new": default +ARGS="new":

View file

@ -1,11 +1,14 @@
# https://dystroy.org/bacon/config/ # https://dystroy.org/bacon/config/
default_job = "test" default_job = "test"
env.CARGO_TERM_COLOR = "always" env.CARGO_TERM_COLOR = "always"
[keybindings] [keybindings]
c = "job:check" c = "job:check"
t = "job:test" t = "job:test"
n = "job:nextest" n = "job:nextest"
l = "job:clippy" l = "job:clippy"
r = "job:run"
[jobs] [jobs]
[jobs.check] [jobs.check]
command = ["cargo", "check"] command = ["cargo", "check"]
@ -47,9 +50,9 @@ command = [
"cargo", "run", "cargo", "run",
# put launch parameters for your program behind a `--` separator # put launch parameters for your program behind a `--` separator
] ]
need_stdout = true need_stdout = false
allow_warnings = true allow_warnings = true
background = true background = false
[jobs.run-long] [jobs.run-long]
watch = ["dizzle", "tengri", "app"] watch = ["dizzle", "tengri", "app"]
command = [ "cargo", "run", ] command = [ "cargo", "run", ]

View file

@ -9,6 +9,7 @@
pkgs.libclang pkgs.libclang
pkgs.mold pkgs.mold
pkgs.cloc pkgs.cloc
pkgs.watchexec
]; ];
buildInputs = [ buildInputs = [
pkgs.libclang pkgs.libclang

View file

@ -1,11 +1,25 @@
use crate::*; use crate::*;
pub mod audio; #[allow(unused)] pub use self::audio::*; pub mod audio; #[allow(unused)] pub use self::audio::*;
pub mod bind; pub use self::bind::*; pub mod bind; pub use self::bind::*;
pub mod cli; pub use self::cli::*;
pub mod config; pub use self::config::*; pub mod config; pub use self::config::*;
pub mod draw; pub use self::draw::*; pub mod draw; pub use self::draw::*;
pub mod size; pub use self::size::*; pub mod size; pub use self::size::*;
primitive!(u8: try_to_u8);
primitive!(u16: try_to_u16);
primitive!(usize: try_to_usize);
primitive!(isize: try_to_isize);
impl_has!(Clock: |self: App|self.project.clock);
impl_has!(Vec<MidiInput>: |self: App|self.project.midi_ins);
impl_has!(Vec<MidiOutput>: |self: App|self.project.midi_outs);
impl_has!(Dialog: |self: App|self.dialog);
impl_has!(Jack<'static>: |self: App|self.jack);
impl_has!(Pool: |self: App|self.pool);
impl_has!(Selection: |self: App|self.project.selection);
impl_as_ref!(Vec<Scene>: |self: App|self.project.as_ref());
impl_as_mut!(Vec<Scene>: |self: App|self.project.as_mut());
impl_as_ref_opt!(MidiEditor: |self: App|self.project.as_ref_opt());
impl_as_mut_opt!(MidiEditor: |self: App|self.project.as_mut_opt());
impl_has_clips!( |self: App|self.pool.clips);
/// Total application state. /// Total application state.
/// ///
/// ``` /// ```
@ -29,7 +43,21 @@ pub mod size; pub use self::size::*;
/// //let _ = app.project.h_outputs(); /// //let _ = app.project.h_outputs();
/// let _ = app.project.h_scenes(); /// let _ = app.project.h_scenes();
/// ``` /// ```
#[derive(Default, Debug)] pub struct App { #[derive(Default, Debug)]
#[namespace(u8)]
#[namespace(isize)]
#[namespace(ItemTheme)]
#[namespace(Arc<str> App::get_arc_str)]
#[namespace(u16 App::get_u16)]
#[namespace(usize App::get_usize)]
#[namespace(bool App::get_bool)]
#[namespace(Selection App::get_selection)]
#[namespace(Color App::get_color)]
#[namespace(Option<u7> App::get_opt_u7)]
#[namespace(Option<u16> App::get_opt_u16)]
#[namespace(Option<usize> App::get_opt_usize)]
#[namespace(Option<Arc<RwLock<MidiClip>>> App::get_clip)]
pub struct App {
/// Base color. /// Base color.
pub color: ItemTheme, pub color: ItemTheme,
/// Must not be dropped for the duration of the process /// Must not be dropped for the duration of the process
@ -53,9 +81,7 @@ pub mod size; pub use self::size::*;
/// Error, if any /// Error, if any
pub error: Arc<RwLock<Option<Arc<str>>>> pub error: Arc<RwLock<Option<Arc<str>>>>
} }
impl App { impl App {
/// Create a new application instance from a backend, project, config, and mode /// Create a new application instance from a backend, project, config, and mode
/// ///
/// ``` /// ```
@ -79,36 +105,6 @@ impl App {
..Default::default() ..Default::default()
} }
} }
pub fn confirm (&mut self) -> Perhaps<AppCommand> {
Ok(match &self.dialog {
Dialog::Menu(index, items) => {
let callback = items.0[*index].1.clone();
callback(self)?;
None
},
_ => todo!(),
})
}
pub fn inc (&mut self, axis: &ControlAxis) -> Perhaps<AppCommand> {
Ok(match (&self.dialog, axis) {
(Dialog::None, _) => todo!(),
(Dialog::Menu(_, _), ControlAxis::Y) =>
AppCommand::SetDialog { dialog: self.dialog.menu_next() }.act(self)?,
_ => todo!()
})
}
pub fn dec (&mut self, axis: &ControlAxis) -> Perhaps<AppCommand> {
Ok(match (&self.dialog, axis) {
(Dialog::None, _) => None,
(Dialog::Menu(_, _), ControlAxis::Y) =>
AppCommand::SetDialog { dialog: self.dialog.menu_prev() }.act(self)?,
_ => todo!()
})
}
/// Update memoized render of clock values. /// Update memoized render of clock values.
/// ``` /// ```
/// tek::App::default().update_clock(); /// tek::App::default().update_clock();
@ -227,98 +223,138 @@ impl App {
} }
} }
} }
} fn get_arc_str (&self, src: impl Language) -> Perhaps<Arc<str>> {
Ok(src.src()?.map(|x|x.into()))
impl_has!(Clock: |self: App|self.project.clock);
impl_has!(Vec<MidiInput>: |self: App|self.project.midi_ins);
impl_has!(Vec<MidiOutput>: |self: App|self.project.midi_outs);
impl_has!(Dialog: |self: App|self.dialog);
impl_has!(Jack<'static>: |self: App|self.jack);
impl_has!(Pool: |self: App|self.pool);
impl_has!(Selection: |self: App|self.project.selection);
impl_as_ref!(Vec<Scene>: |self: App|self.project.as_ref());
impl_as_mut!(Vec<Scene>: |self: App|self.project.as_mut());
impl_as_ref_opt!(MidiEditor: |self: App|self.project.as_ref_opt());
impl_as_mut_opt!(MidiEditor: |self: App|self.project.as_mut_opt());
impl_has_clips!( |self: App|self.pool.clips);
primitive!(u8: try_to_u8);
primitive!(u16: try_to_u16);
primitive!(usize: try_to_usize);
primitive!(isize: try_to_isize);
namespace!(App: Arc<str> { literal = |dsl|Ok(dsl.src()?.map(|x|x.into())); });
namespace!(App: u8 { literal = |dsl|try_to_u8(dsl); });
namespace!(App: u16 { literal = |dsl|try_to_u16(dsl); symbol = |app| {
":w/sidebar" => app.project.w_sidebar(app.editor().is_some()),
":h/sample-detail" => 6.max(app.size.h() as u16 * 3 / 9), }; });
namespace!(App: isize { literal = |dsl|try_to_isize(dsl); });
namespace!(App: usize { literal = |dsl|try_to_usize(dsl); symbol = |app| {
":scene-count" => app.scenes().len(),
":track-count" => app.tracks().len(),
":device-kind" => app.dialog.device_kind().unwrap_or(0),
":device-kind/next" => app.dialog.device_kind_next().unwrap_or(0),
":device-kind/prev" => app.dialog.device_kind_prev().unwrap_or(0), }; });
namespace!(App: bool { symbol = |app| { // Provide boolean values.
":mode/editor" => app.project.editor.is_some(),
":focused/dialog" => !matches!(app.dialog, Dialog::None),
":focused/message" => matches!(app.dialog, Dialog::Message(..)),
":focused/add_device" => matches!(app.dialog, Dialog::Device(..)),
":focused/browser" => app.dialog.browser().is_some(),
":focused/pool/import" => matches!(app.pool.mode, Some(PoolMode::Import(..))),
":focused/pool/export" => matches!(app.pool.mode, Some(PoolMode::Export(..))),
":focused/pool/rename" => matches!(app.pool.mode, Some(PoolMode::Rename(..))),
":focused/pool/length" => matches!(app.pool.mode, Some(PoolMode::Length(..))),
":focused/clip" => !app.editor_focused() && matches!(app.selection(), Selection::TrackClip{..}),
":focused/track" => !app.editor_focused() && matches!(app.selection(), Selection::Track(..)),
":focused/scene" => !app.editor_focused() && matches!(app.selection(), Selection::Scene(..)),
":focused/mix" => !app.editor_focused() && matches!(app.selection(), Selection::Mix),
}; });
namespace!(App: ItemTheme {}); // TODO: provide colors here
//
namespace!(App: Selection { symbol = |app| {
":select/scene" => app.selection().select_scene(app.tracks().len()),
":select/scene/next" => app.selection().select_scene_next(app.scenes().len()),
":select/scene/prev" => app.selection().select_scene_prev(),
":select/track" => app.selection().select_track(app.tracks().len()),
":select/track/next" => app.selection().select_track_next(app.tracks().len()),
":select/track/prev" => app.selection().select_track_prev(),
}; });
namespace!(App: Color {
symbol = |app| {
":color/bg" => Color::Rgb(28, 32, 36),
};
expression = |app| {
"g" (n: u8) => Color::Rgb(n, n, n),
"rgb" (r: u8, g: u8, b: u8) => Color::Rgb(r, g, b),
};
});
namespace!(App: Option<u7> { symbol = |app| {
":editor/pitch" => Some((app.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into())
}; });
namespace!(App: Option<u16> { symbol = |app| {}; });
namespace!(App: Option<usize> { symbol = |app| {
":selected/scene" => app.selection().scene(),
":selected/track" => app.selection().track(),
}; });
namespace!(App: Option<Arc<RwLock<MidiClip>>> { symbol = |app| {
":selected/clip" => if let Selection::TrackClip { track, scene } = app.selection() {
app.scenes()[*scene].clips[*track].clone()
} else {
None
} }
}; }); fn get_u16 (&self, src: impl Language) -> Perhaps<u16> {
Ok(Some(match src.word()? {
Some(":w/sidebar") => self.project.w_sidebar(self.editor().is_some()),
Some(":h/sample-detail") => 6.max(self.size.h() as u16 * 3 / 9),
_ => return try_to_u16(src)
}))
}
fn get_usize (&self, src: impl Language) -> Perhaps<usize> {
Ok(Some(match src.word()? {
Some(":scene-count") => self.scenes().len(),
Some(":track-count") => self.tracks().len(),
Some(":device-kind") => self.dialog.device_kind().unwrap_or(0),
Some(":device-kind/next") => self.dialog.device_kind_next().unwrap_or(0),
Some(":device-kind/prev") => self.dialog.device_kind_prev().unwrap_or(0),
_ => return try_to_usize(src)
}))
}
fn get_bool (&self, src: impl Language) -> Perhaps<bool> {
src.word()?.map(|word|Ok(match word {
"Y" => true,
"N" => false,
":mode/editor" => self.project.editor.is_some(),
":focused/dialog" => !matches!(self.dialog, Dialog::None),
":focused/message" => matches!(self.dialog, Dialog::Message(..)),
":focused/add_device" => matches!(self.dialog, Dialog::Device(..)),
":focused/browser" => self.dialog.browser().is_some(),
":focused/pool/import" => matches!(self.pool.mode, Some(PoolMode::Import(..))),
":focused/pool/export" => matches!(self.pool.mode, Some(PoolMode::Export(..))),
":focused/pool/rename" => matches!(self.pool.mode, Some(PoolMode::Rename(..))),
":focused/pool/length" => matches!(self.pool.mode, Some(PoolMode::Length(..))),
":focused/clip" => !self.editor_focused() && matches!(self.selection(), Selection::TrackClip{..}),
":focused/track" => !self.editor_focused() && matches!(self.selection(), Selection::Track(..)),
":focused/scene" => !self.editor_focused() && matches!(self.selection(), Selection::Scene(..)),
":focused/mix" => !self.editor_focused() && matches!(self.selection(), Selection::Mix),
_ => return Err(format!("not bool: {word}").into())
})).transpose()
}
fn get_selection (&self, src: impl Language) -> Perhaps<Selection> {
src.word()?.map(|word|Ok(match word {
":select/scene" => self.selection().select_scene(self.tracks().len()),
":select/scene/next" => self.selection().select_scene_next(self.scenes().len()),
":select/scene/prev" => self.selection().select_scene_prev(),
":select/track" => self.selection().select_track(self.tracks().len()),
":select/track/next" => self.selection().select_track_next(self.tracks().len()),
":select/track/prev" => self.selection().select_track_prev(),
_ => return Err(format!("not selection: {word}").into())
})).transpose()
}
fn get_color (&self, 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(expr.tail().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(expr.tail().map_err(Into::into))?
.ok_or(LanguageError::Domain("not red"))?;
let g = try_to_u8(expr.tail().tail().head().map_err(Into::into))?
.ok_or(LanguageError::Domain("not green"))?;
let b = try_to_u8(expr.tail().tail().tail().head().map_err(Into::into))?
.ok_or(LanguageError::Domain("not blue"))?;
Ok(Some(Color::Rgb(r, g, b)))
},
(Some(_), _) => return Err(format!("not a color expression: {expr}").into()),
(None, _) => return Err(format!("not a color expression: {expr}").into()),
}
} 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(format!("not a color: {sym}").into())
})
} else {
return Err(format!("not a color: {:?}", src.src()?).into())
}
}
fn get_opt_u7 (&self, src: impl Language) -> Perhaps<Option<u7>> {
src.word()?.map(|word|Ok(match word {
":editor/pitch" => Some((
self.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8
).into()),
_ => return Err(format!("unknown midi note: {word}").into())
})).transpose()
}
fn get_opt_u16 (&self, _src: impl Language) -> Perhaps<Option<u16>> {
Ok(None)
}
fn get_opt_usize (&self, src: impl Language) -> Perhaps<Option<usize>> {
src.word()?.map(|word|Ok(match word {
":selected/scene" => self.selection().scene(),
":selected/track" => self.selection().track(),
_ => return Err(format!("unknown opt<usize>: {word}").into())
})).transpose()
}
fn get_clip (&self, src: impl Language) -> Perhaps<Option<Arc<RwLock<MidiClip>>>> {
src.word()?.map(|word|Ok(match word {
":selected/clip" if let Selection::TrackClip { track, scene } = self.selection() =>
self.scenes()[*scene].clips[*track].clone(),
_ => return Err(format!("not a clip: {word}").into())
})).transpose()
}
pub fn inc (&mut self, axis: &ControlAxis) -> Perhaps<AppCommand> {
Ok(match (&self.dialog, axis) {
(Dialog::None, _) => todo!(),
(Dialog::Menu(_, _), ControlAxis::Y) =>
AppCommand::SetDialog { dialog: self.dialog.menu_next() }.act(self)?,
_ => todo!()
})
}
pub fn dec (&mut self, axis: &ControlAxis) -> Perhaps<AppCommand> {
Ok(match (&self.dialog, axis) {
(Dialog::None, _) => None,
(Dialog::Menu(_, _), ControlAxis::Y) =>
AppCommand::SetDialog { dialog: self.dialog.menu_prev() }.act(self)?,
_ => todo!()
})
}
pub fn confirm (&mut self) -> Perhaps<AppCommand> {
Ok(match &self.dialog {
Dialog::Menu(index, items) => {
let callback = items.0[*index].1.clone();
callback(self)?;
None
},
_ => todo!(),
})
}
}
pub fn 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
@ -360,4 +396,3 @@ pub fn scan (dir: &PathBuf) -> Usually<(Vec<OsString>, Vec<OsString>)> {
files.sort(); files.sort();
Ok((subdirs, files)) Ok((subdirs, files))
} }

View file

@ -1,6 +1,6 @@
use crate::*; use crate::*;
tui_keys!(|self: App, input| { tui_keys!(self: App, input {
let commands = tek_commands_collect(self, input)?; let commands = tek_commands_collect(self, input)?;
let results = tek_commands_execute(self, commands)?; let results = tek_commands_execute(self, commands)?;
self.history.extend(results.into_iter()); self.history.extend(results.into_iter());

View file

@ -1,259 +0,0 @@
use crate::*;
/// The command-line interface descriptor.
///
/// ```
/// let cli: tek::Cli = Default::default();
///
/// use clap::CommandFactory;
/// tek::Cli::command().debug_assert();
/// ```
#[derive(Parser)]
#[command(name = "tek", version, about = Some(HEADER), long_about = Some(HEADER))]
#[derive(Debug, Default)] pub struct Cli {
/// Pre-defined configuration modes.
///
/// TODO: Replace these with scripted configurations.
#[command(subcommand)] pub action: Action,
}
/// Application modes that can be passed to the mommand line interface.
///
/// ```
/// let action: tek::Action = Default::default();
/// ```
#[derive(Debug, Clone, Subcommand, Default)] pub enum Action {
/// Continue where you left off
#[default] Resume,
/// Run headlessly in current session.
Headless,
/// Show status of current session.
Status,
/// List known sessions.
List,
/// Continue work in a copy of the current session.
Fork,
/// Create a new empty session.
New {
/// Name of JACK client
#[arg(short='n', long)] name: Option<String>,
/// Whether to attempt to become transport master
#[arg(short='Y', long, default_value_t = false)] sync_lead: bool,
/// Whether to sync to external transport master
#[arg(short='y', long, default_value_t = true)] sync_follow: bool,
/// Initial tempo in beats per minute
#[arg(short='b', long, default_value = None)] bpm: Option<f64>,
/// Whether to include a transport toolbar (default: true)
#[arg(short='c', long, default_value_t = true)] show_clock: bool,
/// MIDI outs to connect to (multiple instances accepted)
#[arg(short='I', long)] midi_from: Vec<String>,
/// MIDI outs to connect to (multiple instances accepted)
#[arg(short='i', long)] midi_from_re: Vec<String>,
/// MIDI ins to connect to (multiple instances accepted)
#[arg(short='O', long)] midi_to: Vec<String>,
/// MIDI ins to connect to (multiple instances accepted)
#[arg(short='o', long)] midi_to_re: Vec<String>,
/// Audio outs to connect to left input
#[arg(short='l', long)] left_from: Vec<String>,
/// Audio outs to connect to right input
#[arg(short='r', long)] right_from: Vec<String>,
/// Audio ins to connect from left output
#[arg(short='L', long)] left_to: Vec<String>,
/// Audio ins to connect from right output
#[arg(short='R', long)] right_to: Vec<String>,
/// Tracks to creat
#[arg(short='t', long)] tracks: Option<usize>,
/// Scenes to create
#[arg(short='s', long)] scenes: Option<usize>,
},
/// Import media as new session.
Import,
/// Show configuration.
Config,
/// Show version.
Version,
}
/// Command-line configuration.
#[cfg(feature = "cli")]
impl Cli {
pub fn run (&self) -> Usually<()> {
self.action.run(Config::init_new(None)?)
}
}
#[cfg(feature = "cli")]
impl Action {
fn run (&self, config: Config) -> Usually<()> {
use Action::*;
match self {
Version => tek_show_version(),
Config => tek_print_config(&config),
List => todo!("list sessions"),
Resume => todo!("resume session"),
New {
name, bpm, tracks, scenes, sync_lead, sync_follow,
midi_from: mf, midi_from_re: mfr, midi_to: mt, midi_to_re: mtr,
left_from: lf, right_from: rf, left_to: lt, right_to: rt, ..
} => {
let name = name.as_ref().map_or("tek", |x|x.as_str());
let jack = Jack::new(&name)?;
let mut proj = tek_project_new(
&jack, Clock::new(&jack, *bpm)?, &lf, &lt, &rf, &rt, &mf, &mt, &mfr, &mtr
)?;
proj.tracks_add(tracks.unwrap_or(0), None, &[], &[])?;
proj.scenes_add(scenes.unwrap_or(0))?;
//if matches!(self, Action::Status) {
//// Show status and exit
//tek_print_status(&proj);
//return Ok(())
//}
// Initialize the app state
let app = Arc::new(RwLock::new(App::new(&jack, proj, config, ":menu")));
//if matches!(self, Action::Headless) {
//// TODO: Headless mode (daemon + client over IPC, then over network...)
//println!("todo headless");
//return Ok(())
//}
let (_keyboard, _terminal) = Exit::run(|exited|tui_io(
exited.as_ref(),
&app,
Duration::from_millis(100),
Duration::from_millis(10),
std::io::stdout()
))?;
// Run the [Tui] and [Jack] threads with the [App] state.
//Tui::new(Box::new(std::io::stdout()))?.run(true, &jack.run(move|jack|{
//// Between jack init and app's first cycle:
//jack.sync_lead(*sync_lead, |mut state|{
//let clock = app.clock();
//clock.playhead.update_from_sample(state.position.frame() as f64);
//state.position.bbt = Some(clock.bbt());
//state.position
//})?;
//jack.sync_follow(*sync_follow)?;
//// FIXME: They don't work properly.
//Ok(app)
//})?)?;
},
_ => todo!()
}
Ok(())
}
}
pub fn tek_project_new (
jack: &Jack<'static>,
clock: Clock,
left_from: &[impl AsRef<str>],
left_to: &[impl AsRef<str>],
right_from: &[impl AsRef<str>],
right_to: &[impl AsRef<str>],
midi_from: &[impl AsRef<str>],
midi_to: &[impl AsRef<str>],
midi_from_re: &[impl AsRef<str>],
midi_to_re: &[impl AsRef<str>],
) -> Usually<Arrangement> {
// TODO: Collect audio IO:
let empty = &[] as &[&str];
let left_froms = Connect::collect(left_from, empty, empty);
let left_tos = Connect::collect(left_to, empty, empty);
let right_froms = Connect::collect(right_from, empty, empty);
let right_tos = Connect::collect(right_to, empty, empty);
let _audio_froms = &[left_froms.as_slice(), right_froms.as_slice()];
let _audio_tos = &[left_tos.as_slice(), right_tos.as_slice()];
Ok(Arrangement::new(
jack,
None,
clock,
vec![],
vec![],
Connect::collect(&midi_from, &[] as &[&str], &midi_from_re).iter().enumerate()
.map(|(index, connect)|jack.midi_in(&format!("M/{index}"), &[connect.clone()]))
.collect::<Result<Vec<_>, _>>()?,
Connect::collect(&midi_to, &[] as &[&str], &midi_to_re).iter().enumerate()
.map(|(index, connect)|jack.midi_out(&format!("{index}/M"), &[connect.clone()]))
.collect::<Result<Vec<_>, _>>()?
))
}
pub fn tek_show_version () {
println!("todo version");
}
pub fn tek_print_config (config: &Config) {
use ::ansi_term::Color::*;
println!("{:?}", config.dirs);
for (k, v) in config.views.read().unwrap().iter() {
println!("{} {} {v}", Green.paint("VIEW"), Green.bold().paint(format!("{k:<16}")));
}
for (k, v) in config.binds.read().unwrap().iter() {
println!("{} {}", Green.paint("BIND"), Green.bold().paint(format!("{k:<16}")));
for (k, v) in v.0.iter() {
print!("{} ", &Yellow.paint(match &k.0 {
Event::Key(KeyEvent { modifiers, .. }) =>
format!("{:>16}", format!("{modifiers}")),
_ => unimplemented!()
}));
print!("{}", &Yellow.bold().paint(match &k.0 {
Event::Key(KeyEvent { code, .. }) =>
format!("{:<10}", format!("{code}")),
_ => unimplemented!()
}));
for v in v.iter() {
print!(" => {:?}", v.commands);
print!(" {}", v.condition.as_ref().map(|x|format!("{x:?}")).unwrap_or_default());
println!(" {}", v.description.as_ref().map(|x|x.as_ref()).unwrap_or_default());
//println!(" {:?}", v.source);
}
}
}
config.modes.for_each(|k, v|{
println!();
for v in v.name.iter() { print!("{}", Green.bold().paint(format!("{v} "))); }
for v in v.info.iter() { print!("\n{}", Green.paint(format!("{v}"))); }
print!("\n{} {}", Blue.paint("TOOL"), Green.bold().paint(format!("{k:<16}")));
print!("\n{}", Blue.paint("KEYS"));
for v in v.keys.iter() { print!("{}", Green.paint(format!(" {v}"))); }
println!();
v.modes.for_each(|k, v|{
print!("{} {} {:?}", Blue.paint("MODE"), Green.bold().paint(format!("{k:<16}")), v.name);
print!( " INFO={:?}", v.info);
print!( " VIEW={:?}", v.view);
println!(" KEYS={:?}", v.keys);
});
print!("{}", Blue.paint("VIEW"));
for v in v.view.iter() { print!("{}", Green.paint(format!(" {v}"))); }
println!();
});
}
pub fn tek_print_status (project: &Arrangement) {
println!("Name: {:?}", &project.name);
println!("JACK: {:?}", &project.jack);
println!("Buffer: {:?}", &project.clock.chunk);
println!("Sample rate: {:?}", &project.clock.timebase.sr);
println!("MIDI PPQ: {:?}", &project.clock.timebase.ppq);
println!("Tempo: {:?}", &project.clock.timebase.bpm);
println!("Quantize: {:?}", &project.clock.quant);
println!("Launch: {:?}", &project.clock.sync);
println!("Playhead: {:?}us", &project.clock.playhead.usec);
println!("Playhead: {:?}s", &project.clock.playhead.sample);
println!("Playhead: {:?}p", &project.clock.playhead.pulse);
println!("Started: {:?}", &project.clock.started);
println!("Tracks:");
for (i, t) in project.tracks.iter().enumerate() {
println!(" Track {i}: {} {} {:?} {:?}", t.name, t.width,
&t.sequencer.play_clip, &t.sequencer.next_clip);
}
println!("Scenes:");
for (i, t) in project.scenes.iter().enumerate() {
println!(" Scene {i}: {} {:?}", &t.name, &t.clips);
}
println!("MIDI Ins: {:?}", &project.midi_ins);
println!("MIDI Outs: {:?}", &project.midi_outs);
println!("Audio Ins: {:?}", &project.audio_ins);
println!("Audio Outs: {:?}", &project.audio_outs);
// TODO git integration
// TODO dawvert integration
}

View file

@ -38,8 +38,11 @@ impl Config {
const DEFAULTS: &'static str = include_str!("../tek.edn"); const DEFAULTS: &'static str = include_str!("../tek.edn");
pub fn watch <T> (callback: impl FnOnce(Self)->T) -> Usually<T> { pub fn watch <T> (callback: impl FnOnce(Self)->T) -> Usually<T> {
let config = Self::init_new(None)?; let config = Self::init_new(None)?;
let result = callback(config); let watcher = notify_debouncer_mini::new_debouncer(Duration::from_millis(500), |res| {
println!("{res:?}");
})?;
let result = callback(config);
Ok(result) Ok(result)
} }

View file

@ -14,7 +14,9 @@ impl View<Tui> for App {
} }
for (index, dsl) in self.mode.view.iter().enumerate() { for (index, dsl) in self.mode.view.iter().enumerate() {
if let Err(e) = self.interpret(to, dsl) { if let Err(e) = self.interpret(to, dsl) {
*self.error.write().unwrap() = Some(format!("view #{index}: {e}").into()); *self.error.write().unwrap() = Some(format!(
"mode {:?} view #{index}: {e}", &self.mode.name,
).into());
break; break;
} }
} }
@ -45,7 +47,7 @@ fn tek_draw_expr (state: &App, to: &mut Tui, lang: &impl Expression) -> Drawn<u1
fn tek_draw_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Drawn<u16> { fn tek_draw_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Drawn<u16> {
let mut frags = dsl.src()?.unwrap().split("/"); let mut frags = dsl.src()?.unwrap().split("/");
match frags.next() { match frags.next() {
Some(":logo") => view_logo().draw(to), //Some(":logo") => view_logo().draw(to),
Some(":meters") => draw_meter_section(to, frags), Some(":meters") => draw_meter_section(to, frags),
Some(":tracks") => draw_tracks(to, frags, state), Some(":tracks") => draw_tracks(to, frags, state),
Some(":scenes") => draw_scenes(to, frags), Some(":scenes") => draw_scenes(to, frags),
@ -56,6 +58,7 @@ fn tek_draw_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Drawn<u16
Some(":device") => view_device(state).draw(to), Some(":device") => view_device(state).draw(to),
Some(":status") => "TODO: Status Bar".exact_h(1).draw(to), Some(":status") => "TODO: Status Bar".exact_h(1).draw(to),
Some(":editor") => "TODO Editor".draw(to), Some(":editor") => "TODO Editor".draw(to),
Some(":transport") => view_transport(true, "", "", "").draw(to),
Some(":debug") => format!("[{:?}]", to.area()).exact_h(1).draw(to), Some(":debug") => format!("[{:?}]", to.area()).exact_h(1).draw(to),
Some(_) => { Some(_) => {
let views = state.config.views.read().unwrap(); let views = state.config.views.read().unwrap();
@ -109,7 +112,7 @@ pub fn draw_dialog (
let f = if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) }; let f = if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) };
let b = if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) }; let b = if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) };
fg_bg(f, b, item.full_w().align_w().exact_h(2)) fg_bg(f, b, item.full_w().align_w().exact_h(2))
.push_y((2 * index) as u16).draw(to)?; .push_y((4 * index) as u16).draw(to)?;
} }
Ok(Some(to.area().into())) Ok(Some(to.area().into()))
}).full_wh()) }).full_wh())
@ -141,19 +144,6 @@ pub fn draw_templates (to: &mut Tui, frags: std::str::Split<&str>, state: &App)
}).min_w(30).exact_h(height).draw(to) }).min_w(30).exact_h(height).draw(to)
} }
/// ```
/// let _ = tek::view_logo();
/// ```
pub fn view_logo () -> impl Draw<Tui> {
bold(true, fg(Rgb(240, 200, 180), south!{
"".exact_h(1),
"".exact_h(1),
"~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~".exact_h(1),
east("~~~~ ║ ~ ╟─╌ ~╟─< ~~ ", east(fg(Rgb(230,100,40), "v0.3.0"), " ~~")).exact_h(1),
"~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~".exact_h(1),
})).exact_wh(32, 7)
}
/// ``` /// ```
/// let x = ""; /// let x = "";
/// let _ = tek::view_transport(true, x.as_ref(), x.as_ref(), x.as_ref()); /// let _ = tek::view_transport(true, x.as_ref(), x.as_ref(), x.as_ref());
@ -162,7 +152,7 @@ pub fn view_logo () -> impl Draw<Tui> {
pub fn view_transport (play: bool, bpm: &str, beat: &str, time: &str) -> impl Draw<Tui> { pub fn view_transport (play: bool, bpm: &str, beat: &str, time: &str) -> impl Draw<Tui> {
let theme = ItemTheme::G[96]; let theme = ItemTheme::G[96];
bg(Black, east!(above( bg(Black, east!(above(
button_play_pause(play, false).align_w().full_wh(), button_play_pause(play, false).align_w(),
east!( east!(
field_h(theme, "BPM", bpm), field_h(theme, "BPM", bpm),
field_h(theme, "Beat", beat), field_h(theme, "Beat", beat),
@ -465,7 +455,7 @@ pub fn view_track_outputs (
south(button_2("o", "utput", false).align_w().full_w(), south(button_2("o", "utput", false).align_w().full_w(),
thunk(|to: &mut Tui|{ thunk(|to: &mut Tui|{
for port in midi_outs { for port in midi_outs {
port.port_name().align_w().full_w().draw(to); let _ = port.port_name().align_w().full_w().draw(to)?;
} }
Ok(Some(XYWH(0, 0, 0, 0))) Ok(Some(XYWH(0, 0, 0, 0)))
})), })),
@ -584,7 +574,7 @@ pub fn view_inputs (tracks: impl TracksSizes<'_>, midi_ins: &[MidiInput]) -> imp
let title_2 = button_2("I", "+", false).exact_wh(4, 1); let title_2 = button_2("I", "+", false).exact_wh(4, 1);
east(title_1, west(title_2, thunk(move|to: &mut Tui|{ east(title_1, west(title_2, thunk(move|to: &mut Tui|{
for (_index, track, x1, _x2) in tracks { for (_index, track, x1, _x2) in tracks {
south( let _ = south(
bg(track.color.dark.term, east!( bg(track.color.dark.term, east!(
either(track.sequencer.monitoring, fg(Green, "mon "), "mon "), either(track.sequencer.monitoring, fg(Green, "mon "), "mon "),
either(track.sequencer.recording, fg(Red, "rec "), "rec "), either(track.sequencer.recording, fg(Red, "rec "), "rec "),
@ -592,26 +582,24 @@ pub fn view_inputs (tracks: impl TracksSizes<'_>, midi_ins: &[MidiInput]) -> imp
).exact_w(track.width as u16)).align_w().push_x(x1 as u16), ).exact_w(track.width as u16)).align_w().push_x(x1 as u16),
thunk(move |to: &mut Tui|{ thunk(move |to: &mut Tui|{
for (index, port) in midi_ins.iter().enumerate() { for (index, port) in midi_ins.iter().enumerate() {
east( let _ = east(
east( east(
"", "",
bold(true, fg(Rgb(255,255,255), port.port_name())) bold(true, fg(Rgb(255,255,255), port.port_name()))
).align_w().exact_w(20), ).align_w().exact_w(20),
west( west(
().exact_w(4), ().exact_w(4),
thunk(move|to: &mut Tui|{ bg(track.color.darker.term, east!(
bg(track.color.darker.term, east!( either(track.sequencer.monitoring, fg(Green, ""), " · "),
either(track.sequencer.monitoring, fg(Green, ""), " · "), either(track.sequencer.recording, fg(Red, ""), " · "),
either(track.sequencer.recording, fg(Red, ""), " · "), either(track.sequencer.overdub, fg(Yellow, ""), " · "),
either(track.sequencer.overdub, fg(Yellow, ""), " · "), ).exact_w(track.width as u16).align_w())
).exact_w(track.width as u16).align_w()).draw(to); )
todo!() ).push_x(index as u16 * 10).exact_h(1).draw(to)?;
}))
).push_x(index as u16 * 10).exact_h(1).draw(to);
} }
todo!() todo!()
}) })
).draw(to); ).draw(to)?;
} }
todo!() todo!()
}))) })))
@ -646,7 +634,7 @@ pub fn view_outputs (
view_track_row_section(theme, list, button_2("O", "+", false), view_track_row_section(theme, list, button_2("O", "+", false),
bg(theme.darker.term, thunk(|to: &mut Tui|{ bg(theme.darker.term, thunk(|to: &mut Tui|{
for (index, track, _x1, _x2) in tracks { for (index, track, _x1, _x2) in tracks {
thunk(|to: &mut Tui|{ let _ = thunk(|to: &mut Tui|{
east( east(
either(true, fg(Green, "play "), "play "), either(true, fg(Green, "play "), "play "),
either(false, fg(Yellow, "solo "), "solo "), either(false, fg(Yellow, "solo "), "solo "),
@ -661,7 +649,7 @@ pub fn view_outputs (
} }
} }
todo!() todo!()
}).exact_w(track_width(index, track)).draw(to); }).exact_w(track_width(index, track)).draw(to)?;
} }
todo!() todo!()
}).align_w().full_w())).exact_h(height) }).align_w().full_w())).exact_h(height)
@ -711,10 +699,14 @@ pub fn per_track_top <'a, T: Draw<Tui> + 'a, U: TracksSizes<'a>> (
}).align_x()) }).align_x())
} }
pub fn field_h <T: Screen> (theme: ItemTheme, head: impl Draw<T>, body: impl Draw<T>) -> impl Draw<T> { pub fn field_h <T: Screen> (
_theme: ItemTheme, _head: impl Draw<T>, _body: impl Draw<T>
) -> impl Draw<T> {
} }
pub fn field_v <T: Screen> (theme: ItemTheme, head: impl Draw<T>, body: impl Draw<T>) -> impl Draw<T> { pub fn field_v <T: Screen> (
_theme: ItemTheme, _head: impl Draw<T>, _body: impl Draw<T>
) -> impl Draw<T> {
} }
pub fn view_sessions () -> impl Draw<Tui> { pub fn view_sessions () -> impl Draw<Tui> {

View file

@ -1,20 +1,3 @@
#[cfg(feature = "cli")]
pub(crate) use ::clap::{self, Parser, Subcommand};
pub(crate) use ::xdg::BaseDirectories;
pub(crate) use ::midly::{Smf, TrackEventKind, MidiMessage, Error as MidiError, num::*, live::*};
pub(crate) use tengri::{
*, lang::*,
crossterm::event::{Event, KeyEvent},
ratatui::{
self,
prelude::{Rect, Style, Stylize, Buffer, Color::{self, *}},
widgets::{Widget, canvas::{Canvas, Line}},
},
};
#[allow(unused)] #[allow(unused)]
pub(crate) use ::{ pub(crate) use ::{
std::{ std::{
@ -30,4 +13,20 @@ pub(crate) use ::{
time::Duration, time::Duration,
thread::{spawn, JoinHandle}, thread::{spawn, JoinHandle},
}, },
xdg::{
BaseDirectories,
},
tengri::{
*,
lang::*,
midly::{Smf, TrackEventKind, MidiMessage, Error as MidiError, num::*, live::*},
crossterm::event::{Event, KeyEvent},
ratatui::{
self,
prelude::{Rect, Style, Stylize, Buffer, Color::{self, *}},
widgets::{Widget, canvas::{Canvas, Line}},
},
},
}; };
#[cfg(feature = "cli")]
pub(crate) use ::clap::{self, Parser, Subcommand};

View file

@ -100,6 +100,7 @@ pub trait HasDevices: AsRef<Vec<Device>> + AsMut<Vec<Device>> {
} }
} }
pub use ::tengri::sing::*;
pub mod arrange; pub use self::arrange::*; pub mod arrange; pub use self::arrange::*;
pub mod browse; pub use self::browse::*; pub mod browse; pub use self::browse::*;
pub mod clock; pub use self::clock::*; pub mod clock; pub use self::clock::*;
@ -108,9 +109,36 @@ pub mod editor; pub use self::editor::*;
pub mod menu; pub use self::menu::*; pub mod menu; pub use self::menu::*;
pub mod meter; pub use self::meter::*; pub mod meter; pub use self::meter::*;
pub mod mix; pub use self::mix::*; pub mod mix; pub use self::mix::*;
pub mod port; pub use self::port::*;
pub mod sampler; pub use self::sampler::*; pub mod sampler; pub use self::sampler::*;
pub mod sequence; pub use self::sequence::*; pub mod sequence; pub use self::sequence::*;
#[cfg(feature = "plugin")] pub mod plugin; #[cfg(feature = "plugin")] pub mod plugin;
#[cfg(feature = "plugin")] pub use self::plugin::*; #[cfg(feature = "plugin")] pub use self::plugin::*;
def_command!(AudioInputCommand: |port: AudioInput| {
Close => todo!(),
Connect { audio_out: Arc<str> } => todo!(),
});
def_command!(AudioOutputCommand: |port: AudioOutput| {
Close => todo!(),
Connect { audio_in: Arc<str> } => todo!(),
});
def_command!(MidiInputCommand: |port: MidiInput| {
Close => todo!(),
Connect { midi_out: Arc<str> } => todo!(),
});
def_command!(MidiOutputCommand: |port: MidiOutput| {
Close => todo!(),
Connect { midi_in: Arc<str> } => todo!(),
});
pub struct Junction<T: JackPort>(T);
impl<T: JackPort> View<Tui> for Junction<T> {
fn view (&self) -> impl Draw<Tui> {
T::KIND
}
}

View file

@ -5,7 +5,8 @@ use crate::*;
/// ``` /// ```
/// let arranger = tek::Arrangement::default(); /// let arranger = tek::Arrangement::default();
/// ``` /// ```
#[derive(Default, Debug)] pub struct Arrangement { #[derive(Default, Debug)]
pub struct Arrangement {
/// Project name. /// Project name.
pub name: Arc<str>, pub name: Arc<str>,
/// Base color. /// Base color.
@ -19,6 +20,8 @@ use crate::*;
pub size: Sizer, pub size: Sizer,
/// Display size of clips area /// Display size of clips area
pub size_inner: Sizer, pub size_inner: Sizer,
/// Selected UI element
pub selection: Selection,
/// Source of time /// Source of time
#[cfg(feature = "clock")] pub clock: Clock, #[cfg(feature = "clock")] pub clock: Clock,
/// Allows one MIDI clip to be edited /// Allows one MIDI clip to be edited
@ -31,8 +34,6 @@ use crate::*;
#[cfg(feature = "port")] pub audio_ins: Vec<AudioInput>, #[cfg(feature = "port")] pub audio_ins: Vec<AudioInput>,
/// List of global audio outputs /// List of global audio outputs
#[cfg(feature = "port")] pub audio_outs: Vec<AudioOutput>, #[cfg(feature = "port")] pub audio_outs: Vec<AudioOutput>,
/// Selected UI element
#[cfg(feature = "select")] pub selection: Selection,
/// Last track number (to avoid duplicate port names) /// Last track number (to avoid duplicate port names)
#[cfg(feature = "track")] pub track_last: usize, #[cfg(feature = "track")] pub track_last: usize,
/// List of tracks /// List of tracks
@ -45,34 +46,58 @@ use crate::*;
#[cfg(feature = "scene")] pub scene_scroll: usize, #[cfg(feature = "scene")] pub scene_scroll: usize,
} }
impl HasJack<'static> for Arrangement { fn jack (&self) -> &Jack<'static> { &self.jack } } /// Represents the current user selection in the arranger
impl_has!(Jack<'static>: |self: Arrangement| self.jack); #[derive(PartialEq, Clone, Copy, Debug, Default)]
impl_has!(Sizer: |self: Arrangement| self.size); pub enum Selection {
impl_has!(Vec<MidiInput>: |self: Arrangement| self.midi_ins); #[default]
impl_has!(Vec<MidiOutput>: |self: Arrangement| self.midi_outs); /// Nothing is selected
impl_has!(Clock: |self: Arrangement| self.clock); Nothing,
impl_has!(Selection: |self: Arrangement| self.selection); /// The whole mix is selected
impl_as_ref_opt!(MidiEditor: |self: Arrangement| self.editor.as_ref()); Mix,
impl_as_mut_opt!(MidiEditor: |self: Arrangement| self.editor.as_mut()); /// A MIDI input is selected.
Input(usize),
/// A MIDI output is selected.
Output(usize),
/// A scene is selected.
#[cfg(feature = "scene")] Scene(usize),
/// A track is selected.
#[cfg(feature = "track")] Track(usize),
/// A clip (track × scene) is selected.
#[cfg(feature = "track")] TrackClip { track: usize, scene: usize },
/// A track's MIDI input connection is selected.
#[cfg(feature = "track")] TrackInput { track: usize, port: usize },
/// A track's MIDI output connection is selected.
#[cfg(feature = "track")] TrackOutput { track: usize, port: usize },
/// A track device slot is selected.
#[cfg(feature = "track")] TrackDevice { track: usize, device: usize },
}
impl Arrangement { impl Arrangement {
/// Create a new arrangement. /// Create a new arrangement.
pub fn new ( pub fn new (
jack: &Jack<'static>, jack: &Jack<'static>,
name: Option<Arc<str>>, name: Arc<str>,
clock: Clock, clock: Clock,
tracks: Vec<Track>, tracks: impl Iterator<Item = Track>,
scenes: Vec<Scene>, scenes: impl Iterator<Item = Scene>,
midi_ins: Vec<MidiInput>, midi_ins: impl Iterator<Item = MidiInput>,
midi_outs: Vec<MidiOutput>, midi_outs: impl Iterator<Item = MidiOutput>,
audio_ins: impl Iterator<Item = AudioInput>,
audio_outs: impl Iterator<Item = AudioOutput>,
) -> Self { ) -> Self {
Self { Self {
clock, tracks, scenes, midi_ins, midi_outs, jack: jack.clone(),
jack: jack.clone(), color: ItemTheme::random(),
name: name.unwrap_or_default(), selection: Selection::TrackClip { track: 0, scene: 0 },
color: ItemTheme::random(), tracks: tracks.collect(),
selection: Selection::TrackClip { track: 0, scene: 0 }, scenes: scenes.collect(),
midi_ins: midi_ins.collect(),
midi_outs: midi_outs.collect(),
audio_ins: audio_ins.collect(),
audio_outs: audio_outs.collect(),
clock,
name,
..Default::default() ..Default::default()
} }
} }
@ -117,7 +142,759 @@ impl Arrangement {
} }
#[cfg(feature = "clip")] mod clip; #[cfg(feature = "clip")] pub use self::clip::*; impl Selection {
#[cfg(feature = "scene")] mod scene; #[cfg(feature = "scene")] pub use self::scene::*; pub fn describe (
#[cfg(feature = "track")] mod track; #[cfg(feature = "track")] pub use self::track::*; &self,
#[cfg(feature = "select")] mod select; #[cfg(feature = "select")] pub use self::select::*; #[cfg(feature = "track")] tracks: &[Track],
#[cfg(feature = "scene")] scenes: &[Scene],
) -> Arc<str> {
use Selection::*;
format!("{}", match self {
Mix => "Everything".to_string(),
#[cfg(feature = "scene")] Scene(s) =>
scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name)).unwrap_or_else(||"S??".into()),
#[cfg(feature = "track")] Track(t) =>
tracks.get(*t).map(|track|format!("T{t}: {}", &track.name)).unwrap_or_else(||"T??".into()),
TrackClip { track, scene } => match (tracks.get(*track), scenes.get(*scene)) {
(Some(_), Some(s)) => match s.clip(*track) {
Some(clip) => format!("T{track} S{scene} C{}", &clip.read().unwrap().name),
None => format!("T{track} S{scene}: Empty")
},
_ => format!("T{track} S{scene}: Empty"),
},
_ => todo!()
}).into()
}
#[cfg(feature = "scene")] pub fn scene (&self) -> Option<usize> {
use Selection::*;
match self { Scene(scene) | TrackClip { scene, .. } => Some(*scene), _ => None }
}
#[cfg(feature = "scene")] pub fn select_scene (&self, scene_count: usize) -> Self {
use Selection::*;
match self {
Mix | Track(_) => Scene(0),
Scene(s) => Scene((s + 1) % scene_count),
TrackClip { scene, .. } => Track(*scene),
_ => todo!(),
}
}
#[cfg(feature = "scene")] pub fn select_scene_next (&self, len: usize) -> Self {
use Selection::*;
match self {
Mix => Scene(0),
Track(t) => TrackClip { track: *t, scene: 0 },
Scene(s) => if s + 1 < len { Scene(s + 1) } else { Mix },
TrackClip { track, scene } => if scene + 1 < len { TrackClip { track: *track, scene: scene + 1 } } else { Track(*track) },
_ => todo!()
}
}
#[cfg(feature = "scene")] pub fn select_scene_prev (&self) -> Self {
use Selection::*;
match self {
Mix | Scene(0) => Mix,
Scene(s) => Scene(s - 1),
Track(t) => Track(*t),
TrackClip { track, scene: 0 } => Track(*track),
TrackClip { track, scene } => TrackClip { track: *track, scene: scene - 1 },
_ => todo!()
}
}
#[cfg(feature = "track")] pub fn track (&self) -> Option<usize> {
use Selection::*;
if let Track(track)|TrackClip{track,..}|TrackInput{track,..}|TrackOutput{track,..}|TrackDevice{track,..} = self {
Some(*track)
} else {
None
}
}
#[cfg(feature = "track")] pub fn select_track (&self, track_count: usize) -> Self {
use Selection::*;
match self {
Mix => Track(0),
Scene(_) => Mix,
Track(t) => Track((t + 1) % track_count),
TrackClip { track, .. } => Track(*track),
_ => todo!(),
}
}
#[cfg(feature = "track")] pub fn select_track_next (&self, len: usize) -> Self {
use Selection::*;
match self {
Mix => Track(0),
Scene(s) => TrackClip { track: 0, scene: *s },
Track(t) => if t + 1 < len { Track(t + 1) } else { Mix },
TrackClip {track, scene} => if track + 1 < len { TrackClip { track: track + 1, scene: *scene } } else { Scene(*scene) },
_ => todo!()
}
}
#[cfg(feature = "track")] pub fn select_track_prev (&self) -> Self {
use Selection::*;
match self {
Mix => Mix,
Scene(s) => Scene(*s),
Track(0) => Mix,
Track(t) => Track(t - 1),
TrackClip { track: 0, scene } => Scene(*scene),
TrackClip { track: t, scene } => TrackClip { track: t - 1, scene: *scene },
_ => todo!()
}
}
}
impl <T: AsRef<Selection>+AsMut<Selection>> HasSelection for T {}
pub trait HasSelection: AsRef<Selection> + AsMut<Selection> {
fn selection (&self) -> &Selection { self.as_ref() }
fn selection_mut (&mut self) -> &mut Selection { self.as_mut() }
/// Get the active track
#[cfg(feature = "track")]
fn selected_track (&self) -> Option<&Track> where Self: HasTracks {
let index = self.selection().track()?;
self.tracks().get(index)
}
/// Get a mutable reference to the active track
#[cfg(feature = "track")]
fn selected_track_mut (&mut self) -> Option<&mut Track> where Self: HasTracks {
let index = self.selection().track()?;
self.tracks_mut().get_mut(index)
}
/// Get the active scene
#[cfg(feature = "scene")]
fn selected_scene (&self) -> Option<&Scene> where Self: HasScenes {
let index = self.selection().scene()?;
self.scenes().get(index)
}
/// Get a mutable reference to the active scene
#[cfg(feature = "scene")]
fn selected_scene_mut (&mut self) -> Option<&mut Scene> where Self: HasScenes {
let index = self.selection().scene()?;
self.scenes_mut().get_mut(index)
}
/// Get the active clip
#[cfg(feature = "clip")]
fn selected_clip (&self) -> Option<Arc<RwLock<MidiClip>>> where Self: HasScenes + HasTracks {
self.selected_scene()?.clips.get(self.selection().track()?)?.clone()
}
}
impl HasJack<'static> for Arrangement { fn jack (&self) -> &Jack<'static> { &self.jack } }
impl_has!(Jack<'static>: |self: Arrangement| self.jack);
impl_has!(Sizer: |self: Arrangement| self.size);
impl_has!(Vec<MidiInput>: |self: Arrangement| self.midi_ins);
impl_has!(Vec<MidiOutput>: |self: Arrangement| self.midi_outs);
impl_has!(Clock: |self: Arrangement| self.clock);
impl_has!(Selection: |self: Arrangement| self.selection);
impl_as_ref_opt!(MidiEditor: |self: Arrangement| self.editor.as_ref());
impl_as_mut_opt!(MidiEditor: |self: Arrangement| self.editor.as_mut());
pub trait HasClipsSize {
fn clips_size (&self) -> &Sizer;
}
pub trait ClipsView: TracksView + ScenesView {
/// Draw clips per scene
fn view_scenes_clips <'a> (&'a self) -> impl Draw<Tui> + 'a {
view_scenes_clips(
||self.scenes_with_sizes(),
self.tracks_with_sizes(),
self.selection(),
self.editor(),
self.clips_size(),
self.is_editing(),
)
}
}
impl HasClipsSize for App {
fn clips_size (&self) -> &Sizer { &self.project.size_inner }
}
impl HasClipsSize for Arrangement {
fn clips_size (&self) -> &Sizer { &self.size_inner }
}
/// TODO: Preserve the generic passthru syntax;
/// remove this macro (only used twice) and potentially the trait.
#[macro_export] macro_rules! impl_has_clips {
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? {
fn clips <'a> (&'a $self) -> std::sync::RwLockReadGuard<'a, ClipPool> {
$cb.read().unwrap()
}
fn clips_mut <'a> (&'a $self) -> std::sync::RwLockWriteGuard<'a, ClipPool> {
$cb.write().unwrap()
}
}
}
}
#[macro_export] macro_rules! has_clip {
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? HasMidiClip for $Struct $(<$($L),*$($T),*>)? {
fn clip (&$self) -> Option<Arc<RwLock<MidiClip>>> { $cb }
}
}
}
def_command!(ClipCommand: |clip: MidiClip| {
SetColor { color: Option<ItemTheme> } => {
//(SetColor [t: usize, s: usize, c: ItemTheme]
//clip.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o)))));
//("color" [a: usize, b: usize] Some(Self::SetColor(a.unwrap(), b.unwrap(), ItemTheme::random())))
todo!()
},
SetLoop { looping: Option<bool> } => {
//(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}"))
//("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap())))
todo!()
}
});
impl Arrangement {
/// Put a clip in a slot
pub fn clip_put (
&mut self, track: usize, scene: usize, clip: Option<Arc<RwLock<MidiClip>>>
) -> Option<Arc<RwLock<MidiClip>>> {
let old = self.scenes[scene].clips[track].clone();
self.scenes[scene].clips[track] = clip;
old
}
/// Change the color of a clip, returning the previous one
pub fn clip_set_color (
&self, track: usize, scene: usize, color: ItemTheme
) -> Option<ItemTheme> {
self.scenes[scene].clips[track].as_ref().map(|clip|{
let mut clip = clip.write().unwrap();
let old = clip.color.clone();
clip.color = color.clone();
panic!("{color:?} {old:?}");
//old
})
}
/// Toggle looping for the active clip
pub fn toggle_loop (&mut self) {
if let Some(clip) = self.selected_clip() {
clip.write().unwrap().toggle_loop()
}
}
}
/// Default scene height.
pub const H_SCENE: usize = 2;
/// Default editor height.
pub const H_EDITOR: usize = 15;
/// A scene consists of a set of clips to play together.
///
/// ```
/// let scene: tek::Scene = Default::default();
/// let _ = scene.pulses();
/// let _ = scene.is_playing(&[]);
/// ```
#[derive(Debug, Default)] pub struct Scene {
/// Name of scene
pub name: Arc<str>,
/// Identifying color of scene
pub color: ItemTheme,
/// Clips in scene, one per track
pub clips: Vec<Option<Arc<RwLock<MidiClip>>>>,
}
impl Scene {
/// Returns the pulse length of the longest clip in the scene
pub fn pulses (&self) -> usize {
self.clips.iter().fold(0, |a, p|{
a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0))
})
}
/// Returns true if all clips in the scene are
/// currently playing on the given collection of tracks.
pub fn is_playing (&self, tracks: &[Track]) -> bool {
self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate()
.all(|(track_index, clip)|match clip {
Some(c) => tracks
.get(track_index)
.map(|track|{
if let Some((_, Some(clip))) = track.sequencer().play_clip() {
*clip.read().unwrap() == *c.read().unwrap()
} else {
false
}
})
.unwrap_or(false),
None => true
})
}
pub fn clip (&self, index: usize) -> Option<&Arc<RwLock<MidiClip>>> {
match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None }
}
fn _todo_opt_bool_stub_ (&self) -> Option<bool> { todo!() }
fn _todo_usize_stub_ (&self) -> usize { todo!() }
fn _todo_arc_str_stub_ (&self) -> Arc<str> { todo!() }
fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() }
}
pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + Send + Sync {
fn h_scenes (&self) -> u16;
fn w_side (&self) -> u16;
fn w_mid (&self) -> u16;
fn view_scenes_names (&self) -> impl Draw<Tui> {
view_scenes_names(
self.scenes_with_sizes(), self.selection(), self.editor(), self.is_editing()
)
}
fn scenes_with_sizes (&self) -> impl ScenesSizes<'_> {
let mut y = 0;
self.scenes().iter().enumerate().skip(self.scene_scroll()).map_while(move|(s, scene)|{
let height = if self.selection().scene() == Some(s) && self.editor().is_some() {
8
} else {
H_SCENE
};
if y + height <= self.clips_size().h() as usize {
let data = (s, scene, y, y + height);
y += height;
Some(data)
} else {
None
}
})
}
}
pub trait HasSceneScroll: HasScenes {
fn scene_scroll (&self) -> usize;
}
pub trait HasScene: AsRefOpt<Scene> + AsMutOpt<Scene> {
fn scene_mut (&mut self) -> Option<&mut Scene> { self.as_mut_opt() }
fn scene (&self) -> Option<&Scene> { self.as_ref_opt() }
}
pub trait HasScenes: AsRef<Vec<Scene>> + AsMut<Vec<Scene>> {
fn scenes (&self) -> &Vec<Scene> { self.as_ref() }
fn scenes_mut (&mut self) -> &mut Vec<Scene> { self.as_mut() }
/// Generate the default name for a new scene
fn scene_default_name (&self) -> Arc<str> { format!("s{:3>}", self.scenes().len() + 1).into() }
fn scene_longest_name (&self) -> usize { self.scenes().iter().map(|s|s.name.len()).fold(0, usize::max) }
/// Add multiple scenes
fn scenes_add (&mut self, n: usize) -> Usually<()> where Self: HasTracks {
let scene_color_1 = ItemColor::random();
let scene_color_2 = ItemColor::random();
for i in 0..n {
let _ = self.scene_add(None, Some(
scene_color_1.mix(scene_color_2, i as f32 / n as f32).into()
))?;
}
Ok(())
}
/// Add a scene
fn scene_add (&mut self, name: Option<&str>, color: Option<ItemTheme>)
-> Usually<(usize, &mut Scene)> where Self: HasTracks
{
let scene = Scene {
name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()),
clips: vec![None;self.tracks().len()],
color: color.unwrap_or_else(ItemTheme::random),
};
self.scenes_mut().push(scene);
let index = self.scenes().len() - 1;
Ok((index, &mut self.scenes_mut()[index]))
}
}
impl HasSceneScroll for Arrangement {
fn scene_scroll (&self) -> usize { self.scene_scroll }
}
impl HasSceneScroll for App {
fn scene_scroll (&self) -> usize { self.project.scene_scroll() }
}
impl ScenesView for Arrangement {
fn h_scenes (&self) -> u16 { (self.size.h() as u16).saturating_sub(20) }
fn w_side (&self) -> u16 { (self.size.w() as u16 * 2 / 10).max(20) }
fn w_mid (&self) -> u16 { (self.size.w() as u16).saturating_sub(2 * self.w_side()).max(40) }
}
pub type SceneWith<'a, T> =
(usize, &'a Scene, usize, usize, T);
def_command!(SceneCommand: |scene: Scene| {
SetSize { size: usize } => { todo!() },
SetZoom { size: usize } => { todo!() },
SetName { name: Arc<str> } =>
swap_value(&mut scene.name, name, |name|Self::SetName{name}),
SetColor { color: ItemTheme } =>
swap_value(&mut scene.color, color, |color|Self::SetColor{color}),
});
#[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: App| self.project.as_ref_opt());
#[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: App| self.project.as_mut_opt());
#[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: Arrangement| self.selected_scene());
#[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: Arrangement| self.selected_scene_mut());
impl_has!(Vec<Scene>: |self: Arrangement| self.scenes);
impl<T: TracksView+ScenesView+Send+Sync> ClipsView for T {}
impl<T: AsRef<Vec<Scene>>+AsMut<Vec<Scene>>> HasScenes for T {}
impl<T: AsRefOpt<Scene>+AsMutOpt<Scene>+Send+Sync> HasScene for T {}
impl ScenesView for App {
fn w_mid (&self) -> u16 {
(self.size.w() as u16).saturating_sub(self.w_side())
}
fn w_side (&self) -> u16 {
20
}
fn h_scenes (&self) -> u16 {
(self.size.h() as u16).saturating_sub(20)
}
}
/// A track consists of a sequencer and zero or more devices chained after it.
///
/// ```
/// let track: tek::Track = Default::default();
/// ```
#[derive(Debug, Default)] pub struct Track {
/// Name of track
pub name: Arc<str>,
/// Identifying color of track
pub color: ItemTheme,
/// Preferred width of track column
pub width: usize,
/// MIDI sequencer state
pub sequencer: Sequencer,
/// Device chain
pub devices: Vec<Device>,
}
impl Track {
/// Create a new track with only the default [Sequencer].
pub fn new (
name: &impl AsRef<str>,
color: Option<ItemTheme>,
jack: &Jack<'static>,
clock: Option<&Clock>,
clip: Option<&Arc<RwLock<MidiClip>>>,
midi_from: &[Connect],
midi_to: &[Connect],
) -> Usually<Self> {
Ok(Self {
name: name.as_ref().into(),
color: color.unwrap_or_default(),
sequencer: Sequencer::new(
format!("{}/sequencer", name.as_ref()), jack, clock, clip, midi_from, midi_to
)?,
..Default::default()
})
}
pub fn audio_ins (&self) -> &[AudioInput] {
self.devices.first().map(|x|x.audio_ins()).unwrap_or_default()
}
pub fn audio_outs (&self) -> &[AudioOutput] {
self.devices.last().map(|x|x.audio_outs()).unwrap_or_default()
}
fn _todo_opt_bool_stub_ (&self) -> Option<bool> { todo!() }
fn _todo_usize_stub_ (&self) -> usize { todo!() }
fn _todo_arc_str_stub_ (&self) -> Arc<str> { todo!() }
fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() }
pub fn per <'a, T: Draw<Tui> + 'a, U: TracksSizes<'a>> (
tracks: impl Fn() -> U + Send + Sync + 'a,
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
) -> impl Draw<Tui> + 'a {
view_track_per(tracks, callback)
}
}
#[cfg(feature = "sampler")]
impl Track {
/// Create a new track connecting the [Sequencer] to a [Sampler].
pub fn new_with_sampler (
name: &impl AsRef<str>,
color: Option<ItemTheme>,
jack: &Jack<'static>,
clock: Option<&Clock>,
clip: Option<&Arc<RwLock<MidiClip>>>,
midi_from: &[Connect],
midi_to: &[Connect],
audio_from: &[&[Connect];2],
audio_to: &[&[Connect];2],
) -> Usually<Self> {
let mut track = Self::new(name, color, jack, clock, clip, midi_from, midi_to)?;
let client_name = jack.with_client(|c|c.name().to_string());
let port_name = track.sequencer.midi_outs[0].port_name();
let connect = [Connect::exact(format!("{client_name}:{}", port_name))];
track.devices.push(Device::Sampler(Sampler::new(
jack, &format!("{}/sampler", name.as_ref()), &connect, audio_from, audio_to
)?));
Ok(track)
}
pub fn sampler (&self, mut nth: usize) -> Option<&Sampler> {
for device in self.devices.iter() {
match device {
Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; },
_ => {}
}
}
None
}
pub fn sampler_mut (&mut self, mut nth: usize) -> Option<&mut Sampler> {
for device in self.devices.iter_mut() {
match device {
Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; },
_ => {}
}
}
None
}
}
impl HasWidth for Track {
const MIN_WIDTH: usize = 9;
fn width_inc (&mut self) { self.width += 1; }
fn width_dec (&mut self) { if self.width > Track::MIN_WIDTH { self.width -= 1; } }
}
pub trait HasTracks: AsRef<Vec<Track>> + AsMut<Vec<Track>> {
fn tracks (&self) -> &Vec<Track> { self.as_ref() }
fn tracks_mut (&mut self) -> &mut Vec<Track> { self.as_mut() }
/// Run audio callbacks for every track and every device
fn process_tracks (&mut self, client: &Client, scope: &ProcessScope) -> Control {
for track in self.tracks_mut().iter_mut() {
if Control::Quit == Audio::process(&mut track.sequencer, client, scope) {
return Control::Quit
}
for device in track.devices.iter_mut() {
if Control::Quit == DeviceAudio(device).process(client, scope) {
return Control::Quit
}
}
}
Control::Continue
}
fn track_longest_name (&self) -> usize {
self.tracks().iter().map(|s|s.name.len()).fold(0, usize::max)
}
/// Stop all playing clips
fn tracks_stop_all (&mut self) {
for track in self.tracks_mut().iter_mut() {
track.sequencer.enqueue_next(None);
}
}
/// Stop all playing clips
fn tracks_launch (&mut self, clips: Option<Vec<Option<Arc<RwLock<MidiClip>>>>>) {
if let Some(clips) = clips {
for (clip, track) in clips.iter().zip(self.tracks_mut()) {
track.sequencer.enqueue_next(clip.as_ref());
}
} else {
for track in self.tracks_mut().iter_mut() {
track.sequencer.enqueue_next(None);
}
}
}
/// Spacing between tracks.
const TRACK_SPACING: usize = 0;
}
pub trait HasTrack: AsRefOpt<Track> + AsMutOpt<Track> {
fn track (&self) -> Option<&Track> { self.as_ref_opt() }
fn track_mut (&mut self) -> Option<&mut Track> { self.as_mut_opt() }
#[cfg(feature = "port")]
fn view_midi_ins_status <'a> (&'a self, theme: ItemTheme) -> impl Draw<Tui> + 'a {
view_midi_ins_status(theme, self.track())
}
#[cfg(feature = "port")]
fn view_midi_outs_status (&self, theme: ItemTheme) -> impl Draw<Tui> + '_ {
view_midi_outs_status(theme, self.track())
}
#[cfg(feature = "port")]
fn view_audio_ins_status (&self, theme: ItemTheme) -> impl Draw<Tui> {
view_audio_ins_status(theme, self.track())
}
#[cfg(feature = "port")]
fn view_audio_outs_status (&self, theme: ItemTheme) -> impl Draw<Tui> {
view_audio_outs_status(theme, self.track())
}
}
pub trait HasTrackScroll: HasTracks {
fn track_scroll (&self) -> usize;
}
pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll {
/// Draw name of each track
fn view_track_names (&self, theme: ItemTheme) -> impl Draw<Tui> {
view_track_names(
theme, self.tracks_with_sizes(), self.tracks().len(), self.scenes().len(), self.selection()
)
}
/// Draw outputs per track
fn view_track_outputs <'a> (&'a self, theme: ItemTheme, _h: u16) -> impl Draw<Tui> {
view_track_outputs(
theme, self.tracks_with_sizes(), self.midi_outs().iter()
)
}
/// Draw inputs per track
fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Draw<Tui> {
let mut h = 0u16;
for track in self.tracks().iter() {
h = h.max(track.sequencer.midi_ins.len() as u16);
}
view_track_inputs(theme, self.tracks_with_sizes(), h)
}
/// Iterate over tracks with their corresponding sizes.
fn tracks_with_sizes (&self) -> impl TracksSizes<'_> {
let _editor_width = self.editor().map(|e|e.size.w());
let _active_track = self.selection().track();
let mut x = 0;
let w = self.clips_size().w() as usize;
self.tracks().iter().enumerate().map_while(move |(index, track)|{
let width = track.width.max(8);
if x + width < w {
let data = (index, track, x, x + width);
x += width + Self::TRACK_SPACING;
Some(data)
} else {
None
}
})
}
}
impl HasTrackScroll for Arrangement {
fn track_scroll (&self) -> usize { self.track_scroll }
}
def_command!(TrackCommand: |track: Track| {
Stop => { track.sequencer.enqueue_next(None); Ok(None) },
SetRec { rec: Option<bool> } => toggle_bool(&mut track.sequencer.recording, rec, |rec|Self::SetRec { rec }),
SetMon { mon: Option<bool> } => toggle_bool(&mut track.sequencer.monitoring, mon, |mon|Self::SetMon { mon }),
SetMute { mute: Option<bool> } => todo!(),
SetSolo { solo: Option<bool> } => todo!(),
SetSize { size: usize } => todo!(),
SetZoom { zoom: usize } => todo!(),
SetName { name: Arc<str> } => swap_value(&mut track.name, name, |name|Self::SetName { name }),
SetColor { color: ItemTheme } => swap_value(&mut track.color, color, |color|Self::SetColor { color }),
});
impl<T: AsRef<Vec<Track>>+AsMut<Vec<Track>>> HasTracks for T {}
impl<T: AsRefOpt<Track>+AsMutOpt<Track>+Send+Sync> HasTrack for T {}
impl<T: ScenesView+HasMidiIns+HasMidiOuts+HasTrackScroll> TracksView for T {}
impl_has!(Vec<Track>: |self: Arrangement| self.tracks);
impl_as_ref_opt!(Track: |self: Arrangement| self.selected_track());
impl_as_mut_opt!(Track: |self: Arrangement| self.selected_track_mut());
impl_as_ref!(Vec<Track>: |self: App| self.project.as_ref());
impl_as_mut!(Vec<Track>: |self: App| self.project.as_mut());
#[cfg(feature = "select")] impl_as_ref_opt!(Track: |self: App| self.project.as_ref_opt());
#[cfg(feature = "select")] impl_as_mut_opt!(Track: |self: App| self.project.as_mut_opt());
impl Arrangement {
pub fn view_inputs (&self, _theme: ItemTheme) -> impl Draw<Tui> + '_ {
view_inputs(
self.tracks_with_sizes(), self.midi_ins().as_slice()
)
}
pub fn view_outputs (&self, theme: ItemTheme) -> impl Draw<Tui> {
view_outputs(
theme, self.tracks_with_sizes(), self.midi_outs(), self.outputs_height()
)
}
pub fn view_track_devices (&self, theme: ItemTheme) -> impl Draw<Tui> {
view_track_devices(
theme, self.tracks_with_sizes(), self.track(), self.devices_height()
)
}
fn devices_height (&self) -> u16 {
let mut h = 2;
for track in self.tracks().iter() {
h = h.max(track.devices.len() * 2);
}
h as u16
}
fn outputs_height (&self) -> u16 {
let mut h = 1;
for output in self.midi_outs().iter() {
h += 1 + output.connections.len();
}
h as u16
}
/// Add multiple tracks
pub fn tracks_add (
&mut self, count: usize, width: Option<usize>, mins: &[Connect], mouts: &[Connect],
) -> Usually<()> {
let track_color_1 = ItemColor::random();
let track_color_2 = ItemColor::random();
for i in 0..count {
let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into();
let track = self.track_add(None, Some(color), mins, mouts)?.1;
if let Some(width) = width {
track.width = width;
}
}
Ok(())
}
/// Add a track
pub fn track_add (
&mut self,
name: Option<&str>,
color: Option<ItemTheme>,
mins: &[Connect],
mouts: &[Connect],
) -> Usually<(usize, &mut Track)> {
let name: Arc<str> = name.map_or_else(
||format!("trk{:02}", self.track_last).into(),
|x|x.to_string().into()
);
self.track_last += 1;
let track = Track {
width: (name.len() + 2).max(12),
color: color.unwrap_or_else(ItemTheme::random),
sequencer: Sequencer::new(
&format!("{name}"),
self.jack(),
Some(self.clock()),
None,
mins,
mouts
)?,
name,
..Default::default()
};
self.tracks_mut().push(track);
let len = self.tracks().len();
let index = len - 1;
for scene in self.scenes_mut().iter_mut() {
while scene.clips.len() < len {
scene.clips.push(None);
}
}
Ok((index, &mut self.tracks_mut()[index]))
}
}
impl HasTrackScroll for App {
fn track_scroll (&self) -> usize {
self.project.track_scroll()
}
}
pub(crate) fn track_width (_index: usize, track: &Track) -> u16 {
track.width as u16
}

View file

@ -1,95 +0,0 @@
use crate::*;
/// TODO: Preserve the generic passthru syntax;
/// remove this macro (only used twice) and potentially the trait.
#[macro_export] macro_rules! impl_has_clips {
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? {
fn clips <'a> (&'a $self) -> std::sync::RwLockReadGuard<'a, ClipPool> {
$cb.read().unwrap()
}
fn clips_mut <'a> (&'a $self) -> std::sync::RwLockWriteGuard<'a, ClipPool> {
$cb.write().unwrap()
}
}
}
}
#[macro_export] macro_rules! has_clip {
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? HasMidiClip for $Struct $(<$($L),*$($T),*>)? {
fn clip (&$self) -> Option<Arc<RwLock<MidiClip>>> { $cb }
}
}
}
pub trait ClipsView: TracksView + ScenesView {
/// Draw clips per scene
fn view_scenes_clips <'a> (&'a self) -> impl Draw<Tui> + 'a {
view_scenes_clips(
||self.scenes_with_sizes(),
self.tracks_with_sizes(),
self.selection(),
self.editor(),
self.clips_size(),
self.is_editing(),
)
}
}
def_command!(ClipCommand: |clip: MidiClip| {
SetColor { color: Option<ItemTheme> } => {
//(SetColor [t: usize, s: usize, c: ItemTheme]
//clip.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o)))));
//("color" [a: usize, b: usize] Some(Self::SetColor(a.unwrap(), b.unwrap(), ItemTheme::random())))
todo!()
},
SetLoop { looping: Option<bool> } => {
//(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}"))
//("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap())))
todo!()
}
});
impl Arrangement {
/// Put a clip in a slot
pub fn clip_put (
&mut self, track: usize, scene: usize, clip: Option<Arc<RwLock<MidiClip>>>
) -> Option<Arc<RwLock<MidiClip>>> {
let old = self.scenes[scene].clips[track].clone();
self.scenes[scene].clips[track] = clip;
old
}
/// Change the color of a clip, returning the previous one
pub fn clip_set_color (
&self, track: usize, scene: usize, color: ItemTheme
) -> Option<ItemTheme> {
self.scenes[scene].clips[track].as_ref().map(|clip|{
let mut clip = clip.write().unwrap();
let old = clip.color.clone();
clip.color = color.clone();
panic!("{color:?} {old:?}");
//old
})
}
/// Toggle looping for the active clip
pub fn toggle_loop (&mut self) {
if let Some(clip) = self.selected_clip() {
clip.write().unwrap().toggle_loop()
}
}
}
pub trait HasClipsSize {
fn clips_size (&self) -> &Sizer;
}
impl HasClipsSize for App {
fn clips_size (&self) -> &Sizer { &self.project.size_inner }
}
impl HasClipsSize for Arrangement {
fn clips_size (&self) -> &Sizer { &self.size_inner }
}

View file

@ -1,174 +0,0 @@
use crate::*;
/// Default scene height.
pub const H_SCENE: usize = 2;
/// Default editor height.
pub const H_EDITOR: usize = 15;
/// A scene consists of a set of clips to play together.
///
/// ```
/// let scene: tek::Scene = Default::default();
/// let _ = scene.pulses();
/// let _ = scene.is_playing(&[]);
/// ```
#[derive(Debug, Default)] pub struct Scene {
/// Name of scene
pub name: Arc<str>,
/// Identifying color of scene
pub color: ItemTheme,
/// Clips in scene, one per track
pub clips: Vec<Option<Arc<RwLock<MidiClip>>>>,
}
impl Scene {
/// Returns the pulse length of the longest clip in the scene
pub fn pulses (&self) -> usize {
self.clips.iter().fold(0, |a, p|{
a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0))
})
}
/// Returns true if all clips in the scene are
/// currently playing on the given collection of tracks.
pub fn is_playing (&self, tracks: &[Track]) -> bool {
self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate()
.all(|(track_index, clip)|match clip {
Some(c) => tracks
.get(track_index)
.map(|track|{
if let Some((_, Some(clip))) = track.sequencer().play_clip() {
*clip.read().unwrap() == *c.read().unwrap()
} else {
false
}
})
.unwrap_or(false),
None => true
})
}
pub fn clip (&self, index: usize) -> Option<&Arc<RwLock<MidiClip>>> {
match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None }
}
fn _todo_opt_bool_stub_ (&self) -> Option<bool> { todo!() }
fn _todo_usize_stub_ (&self) -> usize { todo!() }
fn _todo_arc_str_stub_ (&self) -> Arc<str> { todo!() }
fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() }
}
pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + Send + Sync {
fn h_scenes (&self) -> u16;
fn w_side (&self) -> u16;
fn w_mid (&self) -> u16;
fn view_scenes_names (&self) -> impl Draw<Tui> {
view_scenes_names(
self.scenes_with_sizes(), self.selection(), self.editor(), self.is_editing()
)
}
fn scenes_with_sizes (&self) -> impl ScenesSizes<'_> {
let mut y = 0;
self.scenes().iter().enumerate().skip(self.scene_scroll()).map_while(move|(s, scene)|{
let height = if self.selection().scene() == Some(s) && self.editor().is_some() {
8
} else {
H_SCENE
};
if y + height <= self.clips_size().h() as usize {
let data = (s, scene, y, y + height);
y += height;
Some(data)
} else {
None
}
})
}
}
pub trait HasSceneScroll: HasScenes {
fn scene_scroll (&self) -> usize;
}
pub trait HasScene: AsRefOpt<Scene> + AsMutOpt<Scene> {
fn scene_mut (&mut self) -> Option<&mut Scene> { self.as_mut_opt() }
fn scene (&self) -> Option<&Scene> { self.as_ref_opt() }
}
pub trait HasScenes: AsRef<Vec<Scene>> + AsMut<Vec<Scene>> {
fn scenes (&self) -> &Vec<Scene> { self.as_ref() }
fn scenes_mut (&mut self) -> &mut Vec<Scene> { self.as_mut() }
/// Generate the default name for a new scene
fn scene_default_name (&self) -> Arc<str> { format!("s{:3>}", self.scenes().len() + 1).into() }
fn scene_longest_name (&self) -> usize { self.scenes().iter().map(|s|s.name.len()).fold(0, usize::max) }
/// Add multiple scenes
fn scenes_add (&mut self, n: usize) -> Usually<()> where Self: HasTracks {
let scene_color_1 = ItemColor::random();
let scene_color_2 = ItemColor::random();
for i in 0..n {
let _ = self.scene_add(None, Some(
scene_color_1.mix(scene_color_2, i as f32 / n as f32).into()
))?;
}
Ok(())
}
/// Add a scene
fn scene_add (&mut self, name: Option<&str>, color: Option<ItemTheme>)
-> Usually<(usize, &mut Scene)> where Self: HasTracks
{
let scene = Scene {
name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()),
clips: vec![None;self.tracks().len()],
color: color.unwrap_or_else(ItemTheme::random),
};
self.scenes_mut().push(scene);
let index = self.scenes().len() - 1;
Ok((index, &mut self.scenes_mut()[index]))
}
}
impl HasSceneScroll for Arrangement {
fn scene_scroll (&self) -> usize { self.scene_scroll }
}
impl HasSceneScroll for App {
fn scene_scroll (&self) -> usize { self.project.scene_scroll() }
}
impl ScenesView for Arrangement {
fn h_scenes (&self) -> u16 { (self.size.h() as u16).saturating_sub(20) }
fn w_side (&self) -> u16 { (self.size.w() as u16 * 2 / 10).max(20) }
fn w_mid (&self) -> u16 { (self.size.w() as u16).saturating_sub(2 * self.w_side()).max(40) }
}
pub type SceneWith<'a, T> =
(usize, &'a Scene, usize, usize, T);
def_command!(SceneCommand: |scene: Scene| {
SetSize { size: usize } => { todo!() },
SetZoom { size: usize } => { todo!() },
SetName { name: Arc<str> } =>
swap_value(&mut scene.name, name, |name|Self::SetName{name}),
SetColor { color: ItemTheme } =>
swap_value(&mut scene.color, color, |color|Self::SetColor{color}),
});
#[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: App| self.project.as_ref_opt());
#[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: App| self.project.as_mut_opt());
#[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: Arrangement| self.selected_scene());
#[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: Arrangement| self.selected_scene_mut());
impl_has!(Vec<Scene>: |self: Arrangement| self.scenes);
impl<T: TracksView+ScenesView+Send+Sync> ClipsView for T {}
impl<T: AsRef<Vec<Scene>>+AsMut<Vec<Scene>>> HasScenes for T {}
impl<T: AsRefOpt<Scene>+AsMutOpt<Scene>+Send+Sync> HasScene for T {}
impl ScenesView for App {
fn w_mid (&self) -> u16 {
(self.size.w() as u16).saturating_sub(self.w_side())
}
fn w_side (&self) -> u16 {
20
}
fn h_scenes (&self) -> u16 {
(self.size.h() as u16).saturating_sub(20)
}
}

View file

@ -1,161 +0,0 @@
use crate::*;
/// Represents the current user selection in the arranger
#[derive(PartialEq, Clone, Copy, Debug, Default)] pub enum Selection {
#[default]
/// Nothing is selected
Nothing,
/// The whole mix is selected
Mix,
/// A MIDI input is selected.
Input(usize),
/// A MIDI output is selected.
Output(usize),
/// A scene is selected.
#[cfg(feature = "scene")] Scene(usize),
/// A track is selected.
#[cfg(feature = "track")] Track(usize),
/// A clip (track × scene) is selected.
#[cfg(feature = "track")] TrackClip { track: usize, scene: usize },
/// A track's MIDI input connection is selected.
#[cfg(feature = "track")] TrackInput { track: usize, port: usize },
/// A track's MIDI output connection is selected.
#[cfg(feature = "track")] TrackOutput { track: usize, port: usize },
/// A track device slot is selected.
#[cfg(feature = "track")] TrackDevice { track: usize, device: usize },
}
impl Selection {
pub fn describe (
&self,
#[cfg(feature = "track")] tracks: &[Track],
#[cfg(feature = "scene")] scenes: &[Scene],
) -> Arc<str> {
use Selection::*;
format!("{}", match self {
Mix => "Everything".to_string(),
#[cfg(feature = "scene")] Scene(s) =>
scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name)).unwrap_or_else(||"S??".into()),
#[cfg(feature = "track")] Track(t) =>
tracks.get(*t).map(|track|format!("T{t}: {}", &track.name)).unwrap_or_else(||"T??".into()),
TrackClip { track, scene } => match (tracks.get(*track), scenes.get(*scene)) {
(Some(_), Some(s)) => match s.clip(*track) {
Some(clip) => format!("T{track} S{scene} C{}", &clip.read().unwrap().name),
None => format!("T{track} S{scene}: Empty")
},
_ => format!("T{track} S{scene}: Empty"),
},
_ => todo!()
}).into()
}
#[cfg(feature = "scene")] pub fn scene (&self) -> Option<usize> {
use Selection::*;
match self { Scene(scene) | TrackClip { scene, .. } => Some(*scene), _ => None }
}
#[cfg(feature = "scene")] pub fn select_scene (&self, scene_count: usize) -> Self {
use Selection::*;
match self {
Mix | Track(_) => Scene(0),
Scene(s) => Scene((s + 1) % scene_count),
TrackClip { scene, .. } => Track(*scene),
_ => todo!(),
}
}
#[cfg(feature = "scene")] pub fn select_scene_next (&self, len: usize) -> Self {
use Selection::*;
match self {
Mix => Scene(0),
Track(t) => TrackClip { track: *t, scene: 0 },
Scene(s) => if s + 1 < len { Scene(s + 1) } else { Mix },
TrackClip { track, scene } => if scene + 1 < len { TrackClip { track: *track, scene: scene + 1 } } else { Track(*track) },
_ => todo!()
}
}
#[cfg(feature = "scene")] pub fn select_scene_prev (&self) -> Self {
use Selection::*;
match self {
Mix | Scene(0) => Mix,
Scene(s) => Scene(s - 1),
Track(t) => Track(*t),
TrackClip { track, scene: 0 } => Track(*track),
TrackClip { track, scene } => TrackClip { track: *track, scene: scene - 1 },
_ => todo!()
}
}
#[cfg(feature = "track")] pub fn track (&self) -> Option<usize> {
use Selection::*;
if let Track(track)|TrackClip{track,..}|TrackInput{track,..}|TrackOutput{track,..}|TrackDevice{track,..} = self {
Some(*track)
} else {
None
}
}
#[cfg(feature = "track")] pub fn select_track (&self, track_count: usize) -> Self {
use Selection::*;
match self {
Mix => Track(0),
Scene(_) => Mix,
Track(t) => Track((t + 1) % track_count),
TrackClip { track, .. } => Track(*track),
_ => todo!(),
}
}
#[cfg(feature = "track")] pub fn select_track_next (&self, len: usize) -> Self {
use Selection::*;
match self {
Mix => Track(0),
Scene(s) => TrackClip { track: 0, scene: *s },
Track(t) => if t + 1 < len { Track(t + 1) } else { Mix },
TrackClip {track, scene} => if track + 1 < len { TrackClip { track: track + 1, scene: *scene } } else { Scene(*scene) },
_ => todo!()
}
}
#[cfg(feature = "track")] pub fn select_track_prev (&self) -> Self {
use Selection::*;
match self {
Mix => Mix,
Scene(s) => Scene(*s),
Track(0) => Mix,
Track(t) => Track(t - 1),
TrackClip { track: 0, scene } => Scene(*scene),
TrackClip { track: t, scene } => TrackClip { track: t - 1, scene: *scene },
_ => todo!()
}
}
}
impl <T: AsRef<Selection>+AsMut<Selection>> HasSelection for T {}
pub trait HasSelection: AsRef<Selection> + AsMut<Selection> {
fn selection (&self) -> &Selection { self.as_ref() }
fn selection_mut (&mut self) -> &mut Selection { self.as_mut() }
/// Get the active track
#[cfg(feature = "track")]
fn selected_track (&self) -> Option<&Track> where Self: HasTracks {
let index = self.selection().track()?;
self.tracks().get(index)
}
/// Get a mutable reference to the active track
#[cfg(feature = "track")]
fn selected_track_mut (&mut self) -> Option<&mut Track> where Self: HasTracks {
let index = self.selection().track()?;
self.tracks_mut().get_mut(index)
}
/// Get the active scene
#[cfg(feature = "scene")]
fn selected_scene (&self) -> Option<&Scene> where Self: HasScenes {
let index = self.selection().scene()?;
self.scenes().get(index)
}
/// Get a mutable reference to the active scene
#[cfg(feature = "scene")]
fn selected_scene_mut (&mut self) -> Option<&mut Scene> where Self: HasScenes {
let index = self.selection().scene()?;
self.scenes_mut().get_mut(index)
}
/// Get the active clip
#[cfg(feature = "clip")]
fn selected_clip (&self) -> Option<Arc<RwLock<MidiClip>>> where Self: HasScenes + HasTracks {
self.selected_scene()?.clips.get(self.selection().track()?)?.clone()
}
}

View file

@ -1,350 +0,0 @@
use crate::*;
/// A track consists of a sequencer and zero or more devices chained after it.
///
/// ```
/// let track: tek::Track = Default::default();
/// ```
#[derive(Debug, Default)] pub struct Track {
/// Name of track
pub name: Arc<str>,
/// Identifying color of track
pub color: ItemTheme,
/// Preferred width of track column
pub width: usize,
/// MIDI sequencer state
pub sequencer: Sequencer,
/// Device chain
pub devices: Vec<Device>,
}
impl Track {
/// Create a new track with only the default [Sequencer].
pub fn new (
name: &impl AsRef<str>,
color: Option<ItemTheme>,
jack: &Jack<'static>,
clock: Option<&Clock>,
clip: Option<&Arc<RwLock<MidiClip>>>,
midi_from: &[Connect],
midi_to: &[Connect],
) -> Usually<Self> {
Ok(Self {
name: name.as_ref().into(),
color: color.unwrap_or_default(),
sequencer: Sequencer::new(
format!("{}/sequencer", name.as_ref()), jack, clock, clip, midi_from, midi_to
)?,
..Default::default()
})
}
pub fn audio_ins (&self) -> &[AudioInput] {
self.devices.first().map(|x|x.audio_ins()).unwrap_or_default()
}
pub fn audio_outs (&self) -> &[AudioOutput] {
self.devices.last().map(|x|x.audio_outs()).unwrap_or_default()
}
fn _todo_opt_bool_stub_ (&self) -> Option<bool> { todo!() }
fn _todo_usize_stub_ (&self) -> usize { todo!() }
fn _todo_arc_str_stub_ (&self) -> Arc<str> { todo!() }
fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() }
pub fn per <'a, T: Draw<Tui> + 'a, U: TracksSizes<'a>> (
tracks: impl Fn() -> U + Send + Sync + 'a,
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
) -> impl Draw<Tui> + 'a {
view_track_per(tracks, callback)
}
}
#[cfg(feature = "sampler")]
impl Track {
/// Create a new track connecting the [Sequencer] to a [Sampler].
pub fn new_with_sampler (
name: &impl AsRef<str>,
color: Option<ItemTheme>,
jack: &Jack<'static>,
clock: Option<&Clock>,
clip: Option<&Arc<RwLock<MidiClip>>>,
midi_from: &[Connect],
midi_to: &[Connect],
audio_from: &[&[Connect];2],
audio_to: &[&[Connect];2],
) -> Usually<Self> {
let mut track = Self::new(name, color, jack, clock, clip, midi_from, midi_to)?;
let client_name = jack.with_client(|c|c.name().to_string());
let port_name = track.sequencer.midi_outs[0].port_name();
let connect = [Connect::exact(format!("{client_name}:{}", port_name))];
track.devices.push(Device::Sampler(Sampler::new(
jack, &format!("{}/sampler", name.as_ref()), &connect, audio_from, audio_to
)?));
Ok(track)
}
pub fn sampler (&self, mut nth: usize) -> Option<&Sampler> {
for device in self.devices.iter() {
match device {
Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; },
_ => {}
}
}
None
}
pub fn sampler_mut (&mut self, mut nth: usize) -> Option<&mut Sampler> {
for device in self.devices.iter_mut() {
match device {
Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; },
_ => {}
}
}
None
}
}
impl HasWidth for Track {
const MIN_WIDTH: usize = 9;
fn width_inc (&mut self) { self.width += 1; }
fn width_dec (&mut self) { if self.width > Track::MIN_WIDTH { self.width -= 1; } }
}
pub trait HasTracks: AsRef<Vec<Track>> + AsMut<Vec<Track>> {
fn tracks (&self) -> &Vec<Track> { self.as_ref() }
fn tracks_mut (&mut self) -> &mut Vec<Track> { self.as_mut() }
/// Run audio callbacks for every track and every device
fn process_tracks (&mut self, client: &Client, scope: &ProcessScope) -> Control {
for track in self.tracks_mut().iter_mut() {
if Control::Quit == Audio::process(&mut track.sequencer, client, scope) {
return Control::Quit
}
for device in track.devices.iter_mut() {
if Control::Quit == DeviceAudio(device).process(client, scope) {
return Control::Quit
}
}
}
Control::Continue
}
fn track_longest_name (&self) -> usize {
self.tracks().iter().map(|s|s.name.len()).fold(0, usize::max)
}
/// Stop all playing clips
fn tracks_stop_all (&mut self) {
for track in self.tracks_mut().iter_mut() {
track.sequencer.enqueue_next(None);
}
}
/// Stop all playing clips
fn tracks_launch (&mut self, clips: Option<Vec<Option<Arc<RwLock<MidiClip>>>>>) {
if let Some(clips) = clips {
for (clip, track) in clips.iter().zip(self.tracks_mut()) {
track.sequencer.enqueue_next(clip.as_ref());
}
} else {
for track in self.tracks_mut().iter_mut() {
track.sequencer.enqueue_next(None);
}
}
}
/// Spacing between tracks.
const TRACK_SPACING: usize = 0;
}
pub trait HasTrack: AsRefOpt<Track> + AsMutOpt<Track> {
fn track (&self) -> Option<&Track> { self.as_ref_opt() }
fn track_mut (&mut self) -> Option<&mut Track> { self.as_mut_opt() }
#[cfg(feature = "port")]
fn view_midi_ins_status <'a> (&'a self, theme: ItemTheme) -> impl Draw<Tui> + 'a {
view_midi_ins_status(theme, self.track())
}
#[cfg(feature = "port")]
fn view_midi_outs_status (&self, theme: ItemTheme) -> impl Draw<Tui> + '_ {
view_midi_outs_status(theme, self.track())
}
#[cfg(feature = "port")]
fn view_audio_ins_status (&self, theme: ItemTheme) -> impl Draw<Tui> {
view_audio_ins_status(theme, self.track())
}
#[cfg(feature = "port")]
fn view_audio_outs_status (&self, theme: ItemTheme) -> impl Draw<Tui> {
view_audio_outs_status(theme, self.track())
}
}
pub trait HasTrackScroll: HasTracks {
fn track_scroll (&self) -> usize;
}
pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll {
/// Draw name of each track
fn view_track_names (&self, theme: ItemTheme) -> impl Draw<Tui> {
view_track_names(
theme, self.tracks_with_sizes(), self.tracks().len(), self.scenes().len(), self.selection()
)
}
/// Draw outputs per track
fn view_track_outputs <'a> (&'a self, theme: ItemTheme, _h: u16) -> impl Draw<Tui> {
view_track_outputs(
theme, self.tracks_with_sizes(), self.midi_outs().iter()
)
}
/// Draw inputs per track
fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Draw<Tui> {
let mut h = 0u16;
for track in self.tracks().iter() {
h = h.max(track.sequencer.midi_ins.len() as u16);
}
view_track_inputs(theme, self.tracks_with_sizes(), h)
}
/// Iterate over tracks with their corresponding sizes.
fn tracks_with_sizes (&self) -> impl TracksSizes<'_> {
let _editor_width = self.editor().map(|e|e.size.w());
let _active_track = self.selection().track();
let mut x = 0;
let w = self.clips_size().w() as usize;
self.tracks().iter().enumerate().map_while(move |(index, track)|{
let width = track.width.max(8);
if x + width < w {
let data = (index, track, x, x + width);
x += width + Self::TRACK_SPACING;
Some(data)
} else {
None
}
})
}
}
impl HasTrackScroll for Arrangement {
fn track_scroll (&self) -> usize { self.track_scroll }
}
def_command!(TrackCommand: |track: Track| {
Stop => { track.sequencer.enqueue_next(None); Ok(None) },
SetRec { rec: Option<bool> } => toggle_bool(&mut track.sequencer.recording, rec, |rec|Self::SetRec { rec }),
SetMon { mon: Option<bool> } => toggle_bool(&mut track.sequencer.monitoring, mon, |mon|Self::SetMon { mon }),
SetMute { mute: Option<bool> } => todo!(),
SetSolo { solo: Option<bool> } => todo!(),
SetSize { size: usize } => todo!(),
SetZoom { zoom: usize } => todo!(),
SetName { name: Arc<str> } => swap_value(&mut track.name, name, |name|Self::SetName { name }),
SetColor { color: ItemTheme } => swap_value(&mut track.color, color, |color|Self::SetColor { color }),
});
impl<T: AsRef<Vec<Track>>+AsMut<Vec<Track>>> HasTracks for T {}
impl<T: AsRefOpt<Track>+AsMutOpt<Track>+Send+Sync> HasTrack for T {}
impl<T: ScenesView+HasMidiIns+HasMidiOuts+HasTrackScroll> TracksView for T {}
impl_has!(Vec<Track>: |self: Arrangement| self.tracks);
impl_as_ref_opt!(Track: |self: Arrangement| self.selected_track());
impl_as_mut_opt!(Track: |self: Arrangement| self.selected_track_mut());
impl_as_ref!(Vec<Track>: |self: App| self.project.as_ref());
impl_as_mut!(Vec<Track>: |self: App| self.project.as_mut());
#[cfg(feature = "select")] impl_as_ref_opt!(Track: |self: App| self.project.as_ref_opt());
#[cfg(feature = "select")] impl_as_mut_opt!(Track: |self: App| self.project.as_mut_opt());
impl Arrangement {
pub fn view_inputs (&self, _theme: ItemTheme) -> impl Draw<Tui> + '_ {
view_inputs(
self.tracks_with_sizes(), self.midi_ins().as_slice()
)
}
pub fn view_outputs (&self, theme: ItemTheme) -> impl Draw<Tui> {
view_outputs(
theme, self.tracks_with_sizes(), self.midi_outs(), self.outputs_height()
)
}
pub fn view_track_devices (&self, theme: ItemTheme) -> impl Draw<Tui> {
view_track_devices(
theme, self.tracks_with_sizes(), self.track(), self.devices_height()
)
}
fn devices_height (&self) -> u16 {
let mut h = 2;
for track in self.tracks().iter() {
h = h.max(track.devices.len() * 2);
}
h as u16
}
fn outputs_height (&self) -> u16 {
let mut h = 1;
for output in self.midi_outs().iter() {
h += 1 + output.connections.len();
}
h as u16
}
/// Add multiple tracks
pub fn tracks_add (
&mut self, count: usize, width: Option<usize>, mins: &[Connect], mouts: &[Connect],
) -> Usually<()> {
let track_color_1 = ItemColor::random();
let track_color_2 = ItemColor::random();
for i in 0..count {
let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into();
let track = self.track_add(None, Some(color), mins, mouts)?.1;
if let Some(width) = width {
track.width = width;
}
}
Ok(())
}
/// Add a track
pub fn track_add (
&mut self,
name: Option<&str>,
color: Option<ItemTheme>,
mins: &[Connect],
mouts: &[Connect],
) -> Usually<(usize, &mut Track)> {
let name: Arc<str> = name.map_or_else(
||format!("trk{:02}", self.track_last).into(),
|x|x.to_string().into()
);
self.track_last += 1;
let track = Track {
width: (name.len() + 2).max(12),
color: color.unwrap_or_else(ItemTheme::random),
sequencer: Sequencer::new(
&format!("{name}"),
self.jack(),
Some(self.clock()),
None,
mins,
mouts
)?,
name,
..Default::default()
};
self.tracks_mut().push(track);
let len = self.tracks().len();
let index = len - 1;
for scene in self.scenes_mut().iter_mut() {
while scene.clips.len() < len {
scene.clips.push(None);
}
}
Ok((index, &mut self.tracks_mut()[index]))
}
}
impl HasTrackScroll for App {
fn track_scroll (&self) -> usize {
self.project.track_scroll()
}
}
pub(crate) fn track_width (_index: usize, track: &Track) -> u16 {
track.width as u16
}

View file

@ -14,38 +14,56 @@ use crate::{*, browse::*, device::*, menu::*};
Browse(BrowseTarget, Arc<Browse>), Browse(BrowseTarget, Arc<Browse>),
Options, Options,
} }
namespace!(App: Dialog { symbol = |app| {
":dialog/none" => Dialog::None, impl<'n> Namespace<'n, Dialog> for App {
":dialog/options" => Dialog::Options, fn namespace (&self, src: impl Language) -> Perhaps<Dialog> {
":dialog/device" => Dialog::Device(0), src.word()?.map(|word|Ok(match word {
":dialog/device/prev" => Dialog::Device(0), ":dialog/none" => Dialog::None,
":dialog/device/next" => Dialog::Device(0), ":dialog/options" => Dialog::Options,
":dialog/help" => Dialog::Help(0), ":dialog/device" => Dialog::Device(0),
":dialog/save" => Dialog::Browse(BrowseTarget::SaveProject, ":dialog/device/prev" => Dialog::Device(0),
Browse::new(None).unwrap().into()), ":dialog/device/next" => Dialog::Device(0),
":dialog/load" => Dialog::Browse(BrowseTarget::LoadProject, ":dialog/help" => Dialog::Help(0),
Browse::new(None).unwrap().into()), ":dialog/save" => Dialog::Browse(
":dialog/import/clip" => Dialog::Browse(BrowseTarget::ImportClip(Default::default()), BrowseTarget::SaveProject,
Browse::new(None).unwrap().into()), Browse::new(None).unwrap().into()),
":dialog/export/clip" => Dialog::Browse(BrowseTarget::ExportClip(Default::default()), ":dialog/load" => Dialog::Browse(
Browse::new(None).unwrap().into()), BrowseTarget::LoadProject,
":dialog/import/sample" => Dialog::Browse(BrowseTarget::ImportSample(Default::default()), Browse::new(None).unwrap().into()),
Browse::new(None).unwrap().into()), ":dialog/import/clip" => Dialog::Browse(
":dialog/export/sample" => Dialog::Browse(BrowseTarget::ExportSample(Default::default()), BrowseTarget::ImportClip(Default::default()),
Browse::new(None).unwrap().into()), Browse::new(None).unwrap().into()),
}; }); ":dialog/export/clip" => Dialog::Browse(
BrowseTarget::ExportClip(Default::default()),
Browse::new(None).unwrap().into()),
":dialog/import/sample" => Dialog::Browse(
BrowseTarget::ImportSample(Default::default()),
Browse::new(None).unwrap().into()),
":dialog/export/sample" => Dialog::Browse(
BrowseTarget::ExportSample(Default::default()),
Browse::new(None).unwrap().into()),
_ => return Err(
format!("not a dialog: {}", src.src()?.unwrap_or("<???>")).into()
)
})).transpose()
}
}
impl Dialog { impl Dialog {
/// ``` /// ```
/// let _ = tek::Dialog::welcome(); /// let _ = tek::Dialog::welcome();
/// ``` /// ```
pub fn welcome () -> Self { pub fn welcome () -> Self {
Self::Menu(1, MenuItems([ Self::Menu(1, MenuItems([
MenuItem("Resume session".into(), Arc::new(Box::new(|_|Ok(())))), MenuItem("Resume session".into(), Arc::new(Box::new(|_|Ok(())))),
MenuItem("Create new session".into(), Arc::new(Box::new(|app|Ok({ MenuItem("New session".into(), Arc::new(Box::new(|app|Ok({
app.dialog = Dialog::None; app.dialog = Dialog::None;
app.mode = app.config.modes.get(":arranger").unwrap(); app.mode = app.config.modes.get(":arranger").unwrap();
})))), })))),
MenuItem("Load old session".into(), Arc::new(Box::new(|_|Ok(())))), MenuItem("Load session".into(), Arc::new(Box::new(|_|Ok(())))),
MenuItem("Exit".into(), Arc::new(Box::new(|_|Ok({
()
})))),
].into())) ].into()))
} }
/// FIXME: generalize /// FIXME: generalize

View file

View file

@ -1,136 +0,0 @@
use crate::*;
use ConnectName::*;
use ConnectScope::*;
use ConnectStatus::*;
mod audio; pub use self::audio::*;
mod midi; pub use self::midi::*;
mod connect; pub use self::connect::*;
mod register; pub use self::register::*;
pub trait JackPort: HasJack<'static> {
type Port: PortSpec + Default;
type Pair: PortSpec + Default;
fn new (jack: &Jack<'static>, name: &impl AsRef<str>, connect: &[Connect])
-> Usually<Self> where Self: Sized;
fn register (jack: &Jack<'static>, name: &impl AsRef<str>) -> Usually<Port<Self::Port>> {
jack.with_client(|c|c.register_port::<Self::Port>(name.as_ref(), Default::default()))
.map_err(|e|e.into())
}
fn port_name (&self) -> &Arc<str>;
fn connections (&self) -> &[Connect];
fn port (&self) -> &Port<Self::Port>;
fn port_mut (&mut self) -> &mut Port<Self::Port>;
fn into_port (self) -> Port<Self::Port> where Self: Sized;
fn close (self) -> Usually<()> where Self: Sized {
let jack = self.jack().clone();
Ok(jack.with_client(|c|c.unregister_port(self.into_port()))?)
}
fn ports (&self, re_name: Option<&str>, re_type: Option<&str>, flags: PortFlags) -> Vec<String> {
self.with_client(|c|c.ports(re_name, re_type, flags))
}
fn port_by_id (&self, id: u32) -> Option<Port<Unowned>> {
self.with_client(|c|c.port_by_id(id))
}
fn port_by_name (&self, name: impl AsRef<str>) -> Option<Port<Unowned>> {
self.with_client(|c|c.port_by_name(name.as_ref()))
}
fn connect_to_matching <'k> (&'k self) -> Usually<()> {
for connect in self.connections().iter() {
match &connect.name {
Some(Exact(name)) => {
*connect.status.write().unwrap() = self.connect_exact(name)?;
},
Some(RegExp(re)) => {
*connect.status.write().unwrap() = self.connect_regexp(re, connect.scope)?;
},
_ => {},
};
}
Ok(())
}
fn connect_exact <'k> (&'k self, name: &str) ->
Usually<Vec<(Port<Unowned>, Arc<str>, ConnectStatus)>>
{
self.with_client(move|c|{
let mut status = vec![];
for port in c.ports(None, None, PortFlags::empty()).iter() {
if port.as_str() == &*name {
if let Some(port) = c.port_by_name(port.as_str()) {
let port_status = self.connect_to_unowned(&port)?;
let name = port.name()?.into();
status.push((port, name, port_status));
if port_status == Connected {
break
}
}
}
}
Ok(status)
})
}
fn connect_regexp <'k> (
&'k self, re: &str, scope: Option<ConnectScope>
) -> Usually<Vec<(Port<Unowned>, Arc<str>, ConnectStatus)>> {
self.with_client(move|c|{
let mut status = vec![];
let ports = c.ports(Some(&re), None, PortFlags::empty());
for port in ports.iter() {
if let Some(port) = c.port_by_name(port.as_str()) {
let port_status = self.connect_to_unowned(&port)?;
let name = port.name()?.into();
status.push((port, name, port_status));
if port_status == Connected && scope == Some(One) {
break
}
}
}
Ok(status)
})
}
/** Connect to a matching port by name. */
fn connect_to_name (&self, name: impl AsRef<str>) -> Usually<ConnectStatus> {
self.with_client(|c|if let Some(ref port) = c.port_by_name(name.as_ref()) {
self.connect_to_unowned(port)
} else {
Ok(Missing)
})
}
/** Connect to a matching port by reference. */
fn connect_to_unowned (&self, port: &Port<Unowned>) -> Usually<ConnectStatus> {
self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) {
Connected
} else if let Ok(_) = c.connect_ports(port, self.port()) {
Connected
} else {
Mismatch
}))
}
/** Connect to an owned matching port by reference. */
fn connect_to_owned (&self, port: &Port<Self::Pair>) -> Usually<ConnectStatus> {
self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) {
Connected
} else if let Ok(_) = c.connect_ports(port, self.port()) {
Connected
} else {
Mismatch
}))
}
}

View file

@ -1,103 +0,0 @@
use crate::*;
def_command!(AudioInputCommand: |port: AudioInput| {
Close => todo!(),
Connect { audio_out: Arc<str> } => todo!(),
});
def_command!(AudioOutputCommand: |port: AudioOutput| {
Close => todo!(),
Connect { audio_in: Arc<str> } => todo!(),
});
impl HasJack<'static> for AudioInput { fn jack (&self) -> &Jack<'static> { &self.jack } }
impl HasJack<'static> for AudioOutput { fn jack (&self) -> &Jack<'static> { &self.jack } }
/// Audio input port.
#[derive(Debug)] pub struct AudioInput {
/// Handle to JACK client, for receiving reconnect events.
pub jack: Jack<'static>,
/// Port name
pub name: Arc<str>,
/// Port handle.
pub port: Port<AudioIn>,
/// List of ports to connect to.
pub connections: Vec<Connect>,
}
/// Audio output port.
#[derive(Debug)] pub struct AudioOutput {
/// Handle to JACK client, for receiving reconnect events.
pub jack: Jack<'static>,
/// Port name
pub name: Arc<str>,
/// Port handle.
pub port: Port<AudioOut>,
/// List of ports to connect to.
pub connections: Vec<Connect>,
}
impl JackPort for AudioInput {
type Port = AudioIn;
type Pair = AudioOut;
fn port_name (&self) -> &Arc<str> {
&self.name
}
fn port (&self) -> &Port<Self::Port> {
&self.port
}
fn port_mut (&mut self) -> &mut Port<Self::Port> {
&mut self.port
}
fn into_port (self) -> Port<Self::Port> {
self.port
}
fn connections (&self) -> &[Connect] {
self.connections.as_slice()
}
fn new (jack: &Jack<'static>, name: &impl AsRef<str>, connect: &[Connect])
-> Usually<Self> where Self: Sized
{
let port = Self {
port: Self::register(jack, name)?,
jack: jack.clone(),
name: name.as_ref().into(),
connections: connect.to_vec()
};
port.connect_to_matching()?;
Ok(port)
}
}
impl JackPort for AudioOutput {
type Port = AudioOut;
type Pair = AudioIn;
fn port_name (&self) -> &Arc<str> {
&self.name
}
fn port (&self) -> &Port<Self::Port> {
&self.port
}
fn port_mut (&mut self) -> &mut Port<Self::Port> {
&mut self.port
}
fn into_port (self) -> Port<Self::Port> {
self.port
}
fn connections (&self) -> &[Connect] {
self.connections.as_slice()
}
fn new (jack: &Jack<'static>, name: &impl AsRef<str>, connect: &[Connect])
-> Usually<Self> where Self: Sized
{
let port = Self {
port: Self::register(jack, name)?,
jack: jack.clone(),
name: name.as_ref().into(),
connections: connect.to_vec()
};
port.connect_to_matching()?;
Ok(port)
}
}

View file

@ -1,83 +0,0 @@
use crate::*;
use ConnectName::*;
use ConnectScope::*;
use ConnectStatus::*;
#[derive(Clone, Debug, PartialEq)] pub enum ConnectName {
/** Exact match */
Exact(Arc<str>),
/** Match regular expression */
RegExp(Arc<str>),
}
#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectScope {
One,
All
}
#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectStatus {
Missing,
Disconnected,
Connected,
Mismatch,
}
/// Port connection manager.
///
/// ```
/// let connect = tek::Connect::default();
/// ```
#[derive(Clone, Debug, Default)] pub struct Connect {
pub name: Option<ConnectName>,
pub scope: Option<ConnectScope>,
pub status: Arc<RwLock<Vec<(Port<Unowned>, Arc<str>, ConnectStatus)>>>,
pub info: Arc<str>,
}
impl Connect {
pub fn collect (exact: &[impl AsRef<str>], re: &[impl AsRef<str>], re_all: &[impl AsRef<str>])
-> Vec<Self>
{
let mut connections = vec![];
for port in exact.iter() { connections.push(Self::exact(port)) }
for port in re.iter() { connections.push(Self::regexp(port)) }
for port in re_all.iter() { connections.push(Self::regexp_all(port)) }
connections
}
/// Connect to this exact port
pub fn exact (name: impl AsRef<str>) -> Self {
let info = format!("=:{}", name.as_ref()).into();
let name = Some(Exact(name.as_ref().into()));
Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info }
}
pub fn regexp (name: impl AsRef<str>) -> Self {
let info = format!("~:{}", name.as_ref()).into();
let name = Some(RegExp(name.as_ref().into()));
Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info }
}
pub fn regexp_all (name: impl AsRef<str>) -> Self {
let info = format!("+:{}", name.as_ref()).into();
let name = Some(RegExp(name.as_ref().into()));
Self { name, scope: Some(All), status: Arc::new(RwLock::new(vec![])), info }
}
pub fn info (&self) -> Arc<str> {
format!(" ({}) {} {}", {
let status = self.status.read().unwrap();
let mut ok = 0;
for (_, _, state) in status.iter() {
if *state == Connected {
ok += 1
}
}
format!("{ok}/{}", status.len())
}, match self.scope {
None => "x",
Some(One) => " ",
Some(All) => "*",
}, match &self.name {
None => format!("x"),
Some(Exact(name)) => format!("= {name}"),
Some(RegExp(name)) => format!("~ {name}"),
}).into()
}
}

View file

@ -1,256 +0,0 @@
use crate::*;
impl HasJack<'static> for MidiInput { fn jack (&self) -> &Jack<'static> { &self.jack } }
impl HasJack<'static> for MidiOutput { fn jack (&self) -> &Jack<'static> { &self.jack } }
/// MIDI input port.
#[derive(Debug)] pub struct MidiInput {
/// Handle to JACK client, for receiving reconnect events.
pub jack: Jack<'static>,
/// Port name
pub name: Arc<str>,
/// Port handle.
pub port: Port<MidiIn>,
/// List of currently held notes.
pub held: Arc<RwLock<[bool;128]>>,
/// List of ports to connect to.
pub connections: Vec<Connect>,
}
/// MIDI output port.
#[derive(Debug)] pub struct MidiOutput {
/// Handle to JACK client, for receiving reconnect events.
pub jack: Jack<'static>,
/// Port name
pub name: Arc<str>,
/// Port handle.
pub port: Port<MidiOut>,
/// List of ports to connect to.
pub connections: Vec<Connect>,
/// List of currently held notes.
pub held: Arc<RwLock<[bool;128]>>,
/// Buffer
pub note_buffer: Vec<u8>,
/// Buffer
pub output_buffer: Vec<Vec<Vec<u8>>>,
}
def_command!(MidiInputCommand: |port: MidiInput| {
Close => todo!(),
Connect { midi_out: Arc<str> } => todo!(),
});
def_command!(MidiOutputCommand: |port: MidiOutput| {
Close => todo!(),
Connect { midi_in: Arc<str> } => todo!(),
});
/// Trait for thing that may receive MIDI.
pub trait HasMidiIns {
fn midi_ins (&self) -> &Vec<MidiInput>;
fn midi_ins_mut (&mut self) -> &mut Vec<MidiInput>;
/// Collect MIDI input from app ports (TODO preallocate large buffers)
fn midi_input_collect <'a> (&'a self, scope: &'a ProcessScope) -> CollectedMidiInput<'a> {
self.midi_ins().iter()
.map(|port|port.port().iter(scope)
.map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes)))
.collect::<Vec<_>>())
.collect::<Vec<_>>()
}
fn midi_ins_with_sizes <'a> (&'a self) ->
impl Iterator<Item=(usize, &'a Arc<str>, &'a [Connect], usize, usize)> + Send + Sync + 'a
{
let mut y = 0;
self.midi_ins().iter().enumerate().map(move|(i, input)|{
let height = 1 + input.connections().len();
let data = (i, input.port_name(), input.connections(), y, y + height);
y += height;
data
})
}
}
/// Trait for thing that may output MIDI.
pub trait HasMidiOuts {
fn midi_outs (&self) -> &Vec<MidiOutput>;
fn midi_outs_mut (&mut self) -> &mut Vec<MidiOutput>;
fn midi_outs_with_sizes <'a> (&'a self) ->
impl Iterator<Item=(usize, &'a Arc<str>, &'a [Connect], usize, usize)> + Send + Sync + 'a
{
let mut y = 0;
self.midi_outs().iter().enumerate().map(move|(i, output)|{
let height = 1 + output.connections().len();
let data = (i, output.port_name(), output.connections(), y, y + height);
y += height;
data
})
}
fn midi_outs_emit (&mut self, scope: &ProcessScope) {
for port in self.midi_outs_mut().iter_mut() {
port.buffer_emit(scope)
}
}
}
impl JackPort for MidiInput {
type Port = MidiIn;
type Pair = MidiOut;
fn port_name (&self) -> &Arc<str> {
&self.name
}
fn port (&self) -> &Port<Self::Port> {
&self.port
}
fn port_mut (&mut self) -> &mut Port<Self::Port> {
&mut self.port
}
fn into_port (self) -> Port<Self::Port> {
self.port
}
fn connections (&self) -> &[Connect] {
self.connections.as_slice()
}
fn new (jack: &Jack<'static>, name: &impl AsRef<str>, connect: &[Connect])
-> Usually<Self> where Self: Sized
{
let port = Self {
port: Self::register(jack, name)?,
jack: jack.clone(),
name: name.as_ref().into(),
connections: connect.to_vec(),
held: Arc::new(RwLock::new([false;128]))
};
port.connect_to_matching()?;
Ok(port)
}
}
impl JackPort for MidiOutput {
type Port = MidiOut;
type Pair = MidiIn;
fn port_name (&self) -> &Arc<str> {
&self.name
}
fn port (&self) -> &Port<Self::Port> {
&self.port
}
fn port_mut (&mut self) -> &mut Port<Self::Port> {
&mut self.port
}
fn into_port (self) -> Port<Self::Port> {
self.port
}
fn connections (&self) -> &[Connect] {
self.connections.as_slice()
}
fn new (jack: &Jack<'static>, name: &impl AsRef<str>, connect: &[Connect])
-> Usually<Self> where Self: Sized
{
let port = Self::register(jack, name)?;
let jack = jack.clone();
let name = name.as_ref().into();
let connections = connect.to_vec();
let port = Self {
jack,
port,
name,
connections,
held: Arc::new([false;128].into()),
note_buffer: vec![0;8],
output_buffer: vec![vec![];65536],
};
port.connect_to_matching()?;
Ok(port)
}
}
impl View<Tui> for MidiInput {
fn view (&self) -> impl Draw<Tui> {
"TODO: MIDI IN"
}
}
impl View<Tui> for MidiOutput {
fn view (&self) -> impl Draw<Tui> {
"TODO: MIDI OUT"
}
}
impl MidiOutput {
/// Clear the section of the output buffer that we will be using,
/// emitting "all notes off" at start of buffer if requested.
pub fn buffer_clear (&mut self, scope: &ProcessScope, reset: bool) {
let n_frames = (scope.n_frames() as usize).min(self.output_buffer.len());
for frame in &mut self.output_buffer[0..n_frames] {
frame.clear();
}
if reset {
all_notes_off(&mut self.output_buffer);
}
}
/// Write a note to the output buffer
pub fn buffer_write <'a> (
&'a mut self,
sample: usize,
event: LiveEvent,
) {
self.note_buffer.fill(0);
event.write(&mut self.note_buffer).expect("failed to serialize MIDI event");
self.output_buffer[sample].push(self.note_buffer.clone());
// Update the list of currently held notes.
if let LiveEvent::Midi { ref message, .. } = event {
update_keys(&mut*self.held.write().unwrap(), message);
}
}
/// Write a chunk of MIDI data from the output buffer to the output port.
pub fn buffer_emit (&mut self, scope: &ProcessScope) {
let samples = scope.n_frames() as usize;
let mut writer = self.port.writer(scope);
for (time, events) in self.output_buffer.iter().enumerate().take(samples) {
for bytes in events.iter() {
writer.write(&RawMidi { time: time as u32, bytes }).unwrap_or_else(|_|{
panic!("Failed to write MIDI data: {bytes:?}");
});
}
}
}
}
impl MidiInput {
pub fn parsed <'a> (&'a self, scope: &'a ProcessScope) -> impl Iterator<Item=(usize, LiveEvent<'a>, &'a [u8])> {
parse_midi_input(self.port().iter(scope))
}
}
impl<T: AsRef<Vec<MidiInput>> + AsMut<Vec<MidiInput>>> HasMidiIns for T {
fn midi_ins (&self) -> &Vec<MidiInput> { self.as_ref() }
fn midi_ins_mut (&mut self) -> &mut Vec<MidiInput> { self.as_mut() }
}
impl<T: AsRef<Vec<MidiOutput>> + AsMut<Vec<MidiOutput>>> HasMidiOuts for T {
fn midi_outs (&self) -> &Vec<MidiOutput> { self.as_ref() }
fn midi_outs_mut (&mut self) -> &mut Vec<MidiOutput> { self.as_mut() }
}
impl<T: HasMidiIns + HasJack<'static>> AddMidiIn for T {
fn midi_in_add (&mut self) -> Usually<()> {
let index = self.midi_ins().len();
let port = MidiInput::new(self.jack(), &format!("M/{index}"), &[])?;
self.midi_ins_mut().push(port);
Ok(())
}
}
/// Trail for thing that may gain new MIDI ports.
impl<T: HasMidiOuts + HasJack<'static>> AddMidiOut for T {
fn midi_out_add (&mut self) -> Usually<()> {
let index = self.midi_outs().len();
let port = MidiOutput::new(self.jack(), &format!("{index}/M"), &[])?;
self.midi_outs_mut().push(port);
Ok(())
}
}
/// May create new MIDI input ports.
pub trait AddMidiIn {
fn midi_in_add (&mut self) -> Usually<()>;
}
/// May create new MIDI output ports.
pub trait AddMidiOut {
fn midi_out_add (&mut self) -> Usually<()>;
}

View file

@ -1,27 +0,0 @@
use crate::*;
pub trait RegisterPorts: HasJack<'static> {
/// Register a MIDI input port.
fn midi_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiInput>;
/// Register a MIDI output port.
fn midi_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiOutput>;
/// Register an audio input port.
fn audio_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioInput>;
/// Register an audio output port.
fn audio_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioOutput>;
}
impl<J: HasJack<'static>> RegisterPorts for J {
fn midi_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiInput> {
MidiInput::new(self.jack(), name, connect)
}
fn midi_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiOutput> {
MidiOutput::new(self.jack(), name, connect)
}
fn audio_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioInput> {
AudioInput::new(self.jack(), name, connect)
}
fn audio_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioOutput> {
AudioOutput::new(self.jack(), name, connect)
}
}

View file

@ -1,5 +1,6 @@
use crate::{*, clock::*, device::*}; use crate::{*, clock::*, device::*};
impl <T: AsRef<Sequencer>+AsMut<Sequencer>> HasSequencer for T {} impl <T: AsRef<Sequencer>+AsMut<Sequencer>> HasSequencer for T {}
/// A MIDI sequence. /// A MIDI sequence.
@ -229,7 +230,6 @@ pub trait MidiRecord: MidiMonitor + HasClock + HasPlayClip {
pub type MidiData = Vec<Vec<MidiMessage>>; pub type MidiData = Vec<Vec<MidiMessage>>;
pub type ClipPool = Vec<Arc<RwLock<MidiClip>>>; pub type ClipPool = Vec<Arc<RwLock<MidiClip>>>;
pub type CollectedMidiInput<'a> = Vec<Vec<(u32, Result<LiveEvent<'a>, MidiError>)>>;
pub trait HasClips { pub trait HasClips {
fn clips <'a> (&'a self) -> std::sync::RwLockReadGuard<'a, ClipPool>; fn clips <'a> (&'a self) -> std::sync::RwLockReadGuard<'a, ClipPool>;
@ -592,35 +592,6 @@ pub(crate) fn note_y_iter (note_lo: usize, note_hi: usize, y0: u16)
(note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n)) (note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n))
} }
/// Return boxed iterator of MIDI events
pub fn parse_midi_input <'a> (input: ::tengri::jack::MidiIter<'a>)
-> Box<dyn Iterator<Item=(usize, LiveEvent<'a>, &'a [u8])> + 'a>
{
Box::new(input.map(|::tengri::jack::RawMidi { time, bytes }|(
time as usize,
LiveEvent::parse(bytes).unwrap(),
bytes
)))
}
/// Add "all notes off" to the start of a buffer.
pub fn all_notes_off (output: &mut [Vec<Vec<u8>>]) {
let mut buf = vec![];
let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() };
let evt = LiveEvent::Midi { channel: 0.into(), message: msg };
evt.write(&mut buf).unwrap();
output[0].push(buf);
}
/// Update notes_in array
pub fn update_keys (keys: &mut[bool;128], message: &MidiMessage) {
match message {
MidiMessage::NoteOn { key, .. } => { keys[key.as_int() as usize] = true; }
MidiMessage::NoteOff { key, .. } => { keys[key.as_int() as usize] = false; },
_ => {}
}
}
/// Returns the next shorter length /// Returns the next shorter length
pub fn note_duration_prev (pulses: usize) -> usize { pub fn note_duration_prev (pulses: usize) -> usize {
for (length, _) in NOTE_DURATIONS.iter().rev() { if *length < pulses { return *length } } for (length, _) in NOTE_DURATIONS.iter().rev() { if *length < pulses { return *length } }

View file

@ -1,3 +1,76 @@
(view :logo (bsp/s (exact/y 1 (text ~~ ~~~~~~~~~~~~))
(bsp/s (exact/y 1 (text ~~~~ ~ ~< ~~ v0.3.0 ~~))
(bsp/s (exact/y 1 (text ~~~~ ~ ~~~~~~~~~~~~)) (text dig?)))))
(view :browse (bsp/s (padding 3 1 :browse-title) (enclose (fg (g 96)) browser)))
(mode :transport (name Transport) (info JACK transport controller.) (keys :clock :global)
:transport)
(mode :menu (name Menu) (info Mode selector.) (keys :axis/y :confirm)
(view (bg (g 0)
(bsp/s (max/y 2 :transport
(bsp/s (max/y 3 (bg (g 80) :ports/out))
(bsp/n (max/y 3 (bg (g 80) :ports/in))
(bg (g 30) (bsp/s (max/h 6 (bg (g 70) :logo) :dialog/menu))))))))))
(mode :sequencer (name Sequencer) (info MIDI sequencer.)
(keys :editor :clock :global)
(mode browse (keys :browse))
(mode rename (keys :pool/rename))
(mode length (keys :pool/length))
(bsp/s (fixed/y 1 :transport)
(bsp/n (fixed/y 1 :status)
(fill (bsp/a (fill/xy (align/e :pool)) :editor)))))
(mode :sampler (name Sampler) (info Sample player.)
(keys :sampler/directions :sampler/record :sampler/play)
(bsp/s (fixed/y 1 :transport)
(bsp/n (fixed/y 1 :status)
(fill :samples/grid))))
(view :ports/out (fill/x (bsp/s (fill/x (align/w (text L-AUDIO-OUT)))
(bsp/e (text MIDI-OUT)
(fill/x (align/e (text AUDIO-OUT-R)))))))
(view :ports/in (fill/x (bsp/s (fill/x (align/w (text L-AUDIO-IN)))
(bsp/e (text MIDI-IN)
(fill/x (align/e (text AUDIO-IN-R)))))))
(mode :groovebox (name Groovebox) (info Sequencer with sampler.)
(keys :clock :editor :sampler :global)
(mode browse (keys :browse))
(mode rename (keys :pool-rename))
(mode length (keys :pool-length))
(bsp/w :meters/output (bsp/e :meters/input (bsp/w :groove/meta :groove/editor))))
(view :groove/meta (fill/y (align/n (stack/s :midi-ins/status :midi-outs/status :audio-ins/status :audio-outs/status :pool))))
(view :groove/editor (bsp/n :groove/sample :groove/sequence))
(view :groove/sample (fixed/y :h-sample-detail (bsp/e (fill/y (fixed/x 20 (align/nw :sample-status))) :sample-viewer)))
(view :groove/sequence (bsp/e (fill/y (align/n (bsp/s :status/v :editor-status))) (bsp/e :samples/keys :editor)))
(mode :arranger (name Arranger) (info Launch grid.)
(keys (see :clock :color :launch :scenes :tracks :global)
(@tab project/edit) (@shift/I project/input/add) (@shift/O project/output/add)
(@shift/D dialog/show :dialog/device))
(mode :editor (keys :editor))
(mode :dialog (keys :dialog))
(mode :message (keys :message))
(mode :add-device (keys :add-device))
(mode :browse (keys :browse))
(mode :rename (keys :input))
(mode :length (keys :rename))
(mode :clip (keys :clip))
(mode :track (keys :track))
(mode :scene (keys :scene))
(mode :mix (keys :mix))
(bsp/n :status (bsp/w :meters/output (bsp/e :meters/input (bsp/n :tracks/inputs
(bsp/s :tracks/outputs (bsp/s :tracks/names (bsp/s :tracks/devices
(fill (either :mode/editor (bsp/e :scenes/names :editor) :scenes))))))))))
(keys :back (@escape back)) (keys :back (@escape back))
(keys :confirm (@enter confirm)) (keys :confirm (@enter confirm))
(keys :axis/x (@left x/dec) (@right x/inc)) (keys :axis/x (@left x/dec) (@right x/inc))
@ -10,113 +83,67 @@
(keys :axis/i2 (@lt i2/dec) (@gt z2/inc)) (keys :axis/i2 (@lt i2/dec) (@gt z2/inc))
(keys :axis/w (@openbracket w/dec) (@closebracket w/inc)) (keys :axis/w (@openbracket w/dec) (@closebracket w/inc))
(keys :axis/w2 (@openbrace w2/dec) (@closebrace w2/inc)) (keys :axis/w2 (@openbrace w2/dec) (@closebrace w2/inc))
(mode :menu
(name Menu)
(info Mode selector.)
(keys :axis/y :confirm)
(view (bg (g 0) (bsp/s
:ports/out
(bsp/n
:ports/in
(bg (g 30) (bsp/s
(fixed/y 7 :logo)
(fill :dialog/menu))))))))
(view :ports/out (fill/x (fixed/y 3 (bsp/a
(fill/x (align/w (text L-AUDIO-OUT)))
(bsp/a (text MIDI-OUT) (fill/x (align/e (text AUDIO-OUT-R))))))))
(view :ports/in (fill/x (fixed/y 3 (bsp/a
(fill/x (align/w (text L-AUDIO-IN)))
(bsp/a (text MIDI-IN) (fill/x (align/e (text AUDIO-IN-R))))))))
(mode :transport (name Transport) (info JACK transport controller.)
(keys :clock :global)
:transport)
(mode :sequencer (name Sequencer) (info MIDI sequencer.)
(mode browse (keys :browse))
(mode rename (keys :pool/rename))
(mode length (keys :pool/length))
(keys :editor :clock :global)
(bsp/s (fixed/y 1 :transport)
(bsp/n (fixed/y 1 :status)
(fill (bsp/a (fill/xy (align/e :pool)) :editor)))))
(keys :editor (see :axis/i :axis/i2 :axis/y :page :editor/view :editor/add :editor/del))
(keys :editor/view (see :axis/x :axis/x2 :axis/z :axis/z2) (@z toggle :lock))
(keys :editor/add (@a editor/append :true) (@enter editor/append :false))
(keys :editor/del (@del editor/delete/note) (@shift/del editor/delete/note))
(mode :sampler (name Sampler) (info Sample player.)
(keys :sampler/directions :sampler/record :sampler/play)
(bsp/s (fixed/y 1 :transport) (bsp/n (fixed/y 1 :status) (fill :samples/grid))))
(keys :sampler/record
(@r sampler/record/toggle :sample/selected)
(@shift/R sampler/record/back))
(keys :sampler/play
(@p sampler/play/sample :sample/selected)
(@P sampler/stop/sample :sample/selected))
(keys :sampler/import-export
(@shift/f6 dialog :dialog/export/sample)
(@shift/f9 dialog :dialog/import/sample))
(keys :sampler/directions
(@up sampler/select :sample/above)
(@down sampler/select :sample/below)
(@left sampler/select :sample/to/left)
(@right sampler/select :sample/to/right))
(mode :groovebox (name Groovebox) (info Sequencer with sampler.)
(mode browse (keys :browse))
(mode rename (keys :pool-rename))
(mode length (keys :pool-length))
(keys :clock :editor :sampler :global)
(bsp/w :meters/output (bsp/e :meters/input (bsp/w :groove/meta :groove/editor))))
(view :groove/meta (fill/y (align/n (stack/s :midi-ins/status :midi-outs/status :audio-ins/status :audio-outs/status :pool))))
(view :groove/editor (bsp/n :groove/sample :groove/sequence))
(view :groove/sample (fixed/y :h-sample-detail (bsp/e (fill/y (fixed/x 20 (align/nw :sample-status))) :sample-viewer)))
(view :groove/sequence (bsp/e (fill/y (align/n (bsp/s :status/v :editor-status))) (bsp/e :samples/keys :editor)))
(mode :arranger (name Arranger) (info Launch grid.)
(mode :editor (keys :editor))
(mode :dialog (keys :dialog))
(mode :message (keys :message))
(mode :add-device (keys :add-device))
(mode :browse (keys :browse))
(mode :rename (keys :input))
(mode :length (keys :rename))
(mode :clip (keys :clip))
(mode :track (keys :track))
(mode :scene (keys :scene))
(mode :mix (keys :mix))
(keys (see :clock :color :launch :scenes :tracks :global)
(@tab project/edit)
(@shift/I project/input/add) (@shift/O project/output/add)
(@shift/D dialog/show :dialog/device))
(bsp/n :status (bsp/w :meters/output (bsp/e :meters/input (bsp/n :tracks/inputs
(bsp/s :tracks/outputs (bsp/s :tracks/names (bsp/s :tracks/devices
(fill (either :mode/editor (bsp/e :scenes/names :editor) :scenes))))))))))
(keys :tracks (@t select :select/track) (@shift/T project/track/add)
(@left select :select/track/dec) (@right select :select/track/inc))
(keys :track (see :color :launch :axis/z :axis/z2 :delete)
(@r toggle :rec) (@m toggle :mon) (@p toggle :play) (@P toggle :solo))
(keys :scenes (@s select :select/scene) (@shift/S project/scene/add)
(@up select :select/scene/dec) (@down select :select/scene/inc))
(keys :scene (see :color :launch :axis/z :axis/z2 :delete))
(keys :focus) (keys :focus)
(keys :help (@f1 dialog :help)) (keys :editor (see :axis/i :axis/i2 :axis/y :page :editor/view :editor/add :editor/del))
(keys :page (@pgup page/up) (@pgdn page/down)) (keys :editor/view (see :axis/x :axis/x2 :axis/z :axis/z2)
(keys :delete (@delete delete) (@backspace delete/back)) (@z toggle :lock))
(keys :input (see :axis/x :delete) (:char input)) (keys :editor/add (@a editor/append :true)
(keys :list (see :axis/y :confirm)) (@enter editor/append :false))
(keys :length (see :axis/x :axis/y :confirm)) (keys :editor/del (@del editor/delete/note)
(keys :browse (see :list :input :focus)) (@shift/del editor/delete/note))
(keys :history (@u undo 1) (@r redo 1)) (keys :sampler/record (@r sampler/record/toggle :sample/selected)
(keys :saveload (@f6 dialog :save) (@f9 dialog :load)) (@shift/R sampler/record/back))
(keys :color (@c color)) (keys :sampler/play (@p sampler/play/sample :sample/selected)
(keys :launch (@q launch)) (@P sampler/stop/sample :sample/selected))
(keys :clock (@space clock/toggle 0) (@shift/space clock/toggle 0)) (keys :sampler/import-export (@shift/f6 dialog :dialog/export/sample)
(keys :global (see :history :saveload) (@f8 dialog :options) (@f10 dialog :quit)) (@shift/f9 dialog :dialog/import/sample))
(keys :clip (see :color :launch :axis/z :axis/z2 :delete) (@l toggle :loop)) (keys :sampler/directions (@up sampler/select :sample/above)
(keys :sequencer (see :color :launch) (@shift/I input/add) (@shift/O output/add)) (@down sampler/select :sample/below)
(keys :pool (see :axis-y :axis-w :axis/z2 :color :delete) (@left sampler/select :sample/to/left)
(@n rename/begin) (@t length/begin) (@m import/begin) (@x export/begin) (@right sampler/select :sample/to/right))
(@shift/A clip/add :after :new/clip) (@shift/D clip/add :after :cloned/clip)) (keys :tracks (@t select :select/track)
(view :browse (bsp/s (@shift/T project/track/add)
(padding 3 1 :browse-title) (@left select :select/track/dec)
(enclose (fg (g 96)) browser))) (@right select :select/track/inc))
(keys :track (see :color :launch :axis/z :axis/z2 :delete)
(@r toggle :rec)
(@m toggle :mon)
(@p toggle :play)
(@P toggle :solo))
(keys :scenes (@s select :select/scene)
(@shift/S project/scene/add)
(@up select :select/scene/dec)
(@down select :select/scene/inc))
(keys :scene (see :color :launch :axis/z :axis/z2 :delete))
(keys :help (@f1 dialog :help))
(keys :page (@pgup page/up)
(@pgdn page/down))
(keys :delete (@delete delete)
(@backspace delete/back))
(keys :input (see :axis/x :delete) (:char input))
(keys :list (see :axis/y :confirm))
(keys :length (see :axis/x :axis/y :confirm))
(keys :browse (see :list :input :focus))
(keys :history (@u undo 1)
(@r redo 1))
(keys :saveload (@f6 dialog :save)
(@f9 dialog :load))
(keys :color (@c color))
(keys :launch (@q launch))
(keys :clock (@space clock/toggle 0)
(@shift/space clock/toggle 0))
(keys :global (see :history :saveload)
(@f8 dialog :options)
(@f10 dialog :quit))
(keys :clip (see :color :launch :axis/z :axis/z2 :delete)
(@l toggle :loop))
(keys :sequencer (see :color :launch)
(@shift/I input/add)
(@shift/O output/add))
(keys :pool (see :axis-y :axis-w :axis/z2 :color :delete)
(@n rename/begin)
(@t length/begin)
(@m import/begin)
(@x export/begin)
(@shift/A clip/add :after :new/clip)
(@shift/D clip/add :after :cloned/clip))

View file

@ -6,33 +6,295 @@
trait_alias, trait_alias,
type_changing_struct_update type_changing_struct_update
)] )]
pub extern crate atomic_float; pub extern crate atomic_float;
pub extern crate xdg; pub extern crate xdg;
pub extern crate midly;
pub extern crate tengri; pub extern crate tengri;
/// Command-line entrypoint. /// Command-line entrypoint.
#[cfg(feature = "cli")] pub fn main () -> Usually<()> { #[cfg(feature = "cli")] pub fn main () -> Usually<()> {
tui_setup_panic(); tengri::Tui::setup_panic();
Config::watch(|config|{ let name = "tek";
tui_run_main(Jack::new_run("tek", move|jack|{ let mode = ":menu";
let clock = Clock::new(&jack, Some(51.))?; let title = "untitled!";
let project = Arrangement::new(&jack, None, clock, vec![], vec![], vec![], vec![]); let bpm = 74.;
let app = App::new(&jack, project, config, ":menu"); Config::watch(|config|tengri::Tui::run_main(Jack::new_run(name, move|jack|{
Ok(app) let clock = Clock::new(&jack, Some(bpm))?;
})?) let tracks = [];
}).map(|_|()) let scenes = [];
let project = Arrangement::new(
&jack, title.into(), clock,
tracks.into_iter(),
scenes.into_iter(),
Connect::midi_ins(&jack, &"M", &[], None)?.into_iter(),
Connect::midi_outs(&jack, &"M", &[], None)?.into_iter(),
[].into_iter()
.chain(Connect::audio_ins(&jack, &"L", &[], None)?.into_iter())
.chain(Connect::audio_ins(&jack, &"R", &[], None)?.into_iter()),
[].into_iter()
.chain(Connect::audio_outs(&jack, &"L", &[], None)?.into_iter())
.chain(Connect::audio_outs(&jack, &"R", &[], None)?.into_iter()),
);
Ok(App::new(&jack, project, config, mode))
})?)).map(|_|())
} }
pub mod deps;
pub mod deps; pub use self::deps::*; pub(crate) use self::deps::*;
pub mod device; pub use self::device::*; pub mod device;
pub mod app; pub use self::app::*; pub use self::device::*;
pub mod app;
pub use self::app::*;
/// CLI banner. /// CLI banner.
pub(crate) const HEADER: &'static str = r#" pub(crate) const HEADER: &'static str = r#"
~ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~ ~ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~
~ v0.4.0, 2026 heatwave edition ~ ~ v0.4.0, 2026 heatwave edition ~
~ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ "#; ~ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ "#;
#[cfg(feature = "cli")]
mod cli {
use crate::*;
/// The command-line interface descriptor.
///
/// ```
/// let cli: tek::Cli = Default::default();
///
/// use clap::CommandFactory;
/// tek::Cli::command().debug_assert();
/// ```
#[derive(Parser)]
#[command(name = "tek", version, about = Some(HEADER), long_about = Some(HEADER))]
#[derive(Debug, Default)] pub struct Cli {
/// Pre-defined configuration modes.
///
/// TODO: Replace these with scripted configurations.
#[command(subcommand)] pub action: Action,
}
/// Application modes that can be passed to the mommand line interface.
///
/// ```
/// let action: tek::Action = Default::default();
/// ```
#[derive(Debug, Clone, Subcommand, Default)] pub enum Action {
/// Continue where you left off
#[default] Resume,
/// Run headlessly in current session.
Headless,
/// Show status of current session.
Status,
/// List known sessions.
List,
/// Continue work in a copy of the current session.
Fork,
/// Create a new empty session.
New {
/// Name of JACK client
#[arg(short='n', long)] name: Option<String>,
/// Whether to attempt to become transport master
#[arg(short='Y', long, default_value_t = false)] sync_lead: bool,
/// Whether to sync to external transport master
#[arg(short='y', long, default_value_t = true)] sync_follow: bool,
/// Initial tempo in beats per minute
#[arg(short='b', long, default_value = None)] bpm: Option<f64>,
/// Whether to include a transport toolbar (default: true)
#[arg(short='c', long, default_value_t = true)] show_clock: bool,
/// MIDI outs to connect to (multiple instances accepted)
#[arg(short='I', long)] midi_from: Vec<String>,
/// MIDI outs to connect to (multiple instances accepted)
#[arg(short='i', long)] midi_from_re: Vec<String>,
/// MIDI ins to connect to (multiple instances accepted)
#[arg(short='O', long)] midi_to: Vec<String>,
/// MIDI ins to connect to (multiple instances accepted)
#[arg(short='o', long)] midi_to_re: Vec<String>,
/// Audio outs to connect to left input
#[arg(short='l', long)] left_from: Vec<String>,
/// Audio outs to connect to right input
#[arg(short='r', long)] right_from: Vec<String>,
/// Audio ins to connect from left output
#[arg(short='L', long)] left_to: Vec<String>,
/// Audio ins to connect from right output
#[arg(short='R', long)] right_to: Vec<String>,
/// Tracks to creat
#[arg(short='t', long)] tracks: Option<usize>,
/// Scenes to create
#[arg(short='s', long)] scenes: Option<usize>,
},
/// Import media as new session.
Import,
/// Show configuration.
Config,
/// Show version.
Version,
}
/// Command-line configuration.
#[cfg(feature = "cli")]
impl Cli {
pub fn run (&self) -> Usually<()> {
self.action.run(Config::init_new(None)?)
}
}
#[cfg(feature = "cli")]
impl Action {
fn run (&self, config: Config) -> Usually<()> {
use Action::*;
match self {
Version => show_version(),
Config => print_config(&config),
List => todo!("list sessions"),
Resume => todo!("resume session"),
New {
name, bpm, tracks, scenes,
sync_lead, sync_follow,
midi_from: mf, midi_from_re: mfr, midi_to: mt, midi_to_re: mtr,
left_from: lf, right_from: rf, left_to: lt, right_to: rt, ..
} => {
let name = name.as_ref().map_or("tek", |x|x.as_str());
let jack = Jack::new(&name)?;
let mut proj = Arrangement::new(
&jack,
name.into(),
Clock::new(&jack, None)?,
[].into_iter(),
[].into_iter(),
Connect::midi_ins(&jack, &"M", &[], None)?.into_iter(),
Connect::midi_outs(&jack, &"M", &[], None)?.into_iter(),
[].into_iter()
.chain(Connect::audio_ins(&jack, &"L", &[], None)?.into_iter())
.chain(Connect::audio_ins(&jack, &"R", &[], None)?.into_iter()),
[].into_iter()
.chain(Connect::audio_outs(&jack, &"L", &[], None)?.into_iter())
.chain(Connect::audio_outs(&jack, &"R", &[], None)?.into_iter()),
);
//&jack, Clock::new(&jack, *bpm)?, &lf, &lt, &rf, &rt, &mf, &mt, &mfr, &mtr)?;
proj.tracks_add(tracks.unwrap_or(0), None, &[], &[])?;
proj.scenes_add(scenes.unwrap_or(0))?;
//if matches!(self, Action::Status) {
//// Show status and exit
//tek_print_status(&proj);
//return Ok(())
//}
// Initialize the app state
let app = Arc::new(RwLock::new(App::new(&jack, proj, config, ":menu")));
//if matches!(self, Action::Headless) {
//// TODO: Headless mode (daemon + client over IPC, then over network...)
//println!("todo headless");
//return Ok(())
//}
let (_keyboard, _terminal) = Exit::run(|exited|Tui::io(
exited.as_ref(),
&app,
Duration::from_millis(100),
Duration::from_millis(10),
std::io::stdout()
))?;
},
_ => todo!()
}
Ok(())
}
}
//pub fn tui (
//app: Arc<RwLock<App>>,
//jack: Jack,
//sync_lead: &bool,
//sync_follow: &bool,
//) -> Usually<()> {
//// Run the [Tui] and [Jack] threads with the [App] state.
//Tui::run_main(&jack.run(move|jack|{
//// Between jack init and app's first cycle:
////jack.sync_lead(*sync_lead, |mut state|{
////let clock = app.write().unwrap().clock();
////clock.playhead.update_from_sample(state.position.frame() as f64);
////state.position.bbt = Some(clock.bbt());
////state.position
////})?;
////jack.sync_follow(*sync_follow)?;
//// FIXME: They don't work properly.
//Ok(app)
//})?)?
//}
pub fn show_version () {
println!("todo version");
}
pub fn print_config (config: &Config) {
use ::ansi_term::Color::*;
println!("{:?}", config.dirs);
for (k, v) in config.views.read().unwrap().iter() {
println!("{} {} {v}", Green.paint("VIEW"), Green.bold().paint(format!("{k:<16}")));
}
for (k, v) in config.binds.read().unwrap().iter() {
println!("{} {}", Green.paint("BIND"), Green.bold().paint(format!("{k:<16}")));
for (k, v) in v.0.iter() {
print!("{} ", &Yellow.paint(match &k.0 {
Event::Key(KeyEvent { modifiers, .. }) =>
format!("{:>16}", format!("{modifiers}")),
_ => unimplemented!()
}));
print!("{}", &Yellow.bold().paint(match &k.0 {
Event::Key(KeyEvent { code, .. }) =>
format!("{:<10}", format!("{code}")),
_ => unimplemented!()
}));
for v in v.iter() {
print!(" => {:?}", v.commands);
print!(" {}", v.condition.as_ref().map(|x|format!("{x:?}")).unwrap_or_default());
println!(" {}", v.description.as_ref().map(|x|x.as_ref()).unwrap_or_default());
//println!(" {:?}", v.source);
}
}
}
config.modes.for_each(|k, v|{
println!();
for v in v.name.iter() { print!("{}", Green.bold().paint(format!("{v} "))); }
for v in v.info.iter() { print!("\n{}", Green.paint(format!("{v}"))); }
print!("\n{} {}", Blue.paint("TOOL"), Green.bold().paint(format!("{k:<16}")));
print!("\n{}", Blue.paint("KEYS"));
for v in v.keys.iter() { print!("{}", Green.paint(format!(" {v}"))); }
println!();
v.modes.for_each(|k, v|{
print!("{} {} {:?}", Blue.paint("MODE"), Green.bold().paint(format!("{k:<16}")), v.name);
print!( " INFO={:?}", v.info);
print!( " VIEW={:?}", v.view);
println!(" KEYS={:?}", v.keys);
});
print!("{}", Blue.paint("VIEW"));
for v in v.view.iter() { print!("{}", Green.paint(format!(" {v}"))); }
println!();
});
}
pub fn print_status (project: &Arrangement) {
println!("Name: {:?}", &project.name);
println!("JACK: {:?}", &project.jack);
println!("Buffer: {:?}", &project.clock.chunk);
println!("Sample rate: {:?}", &project.clock.timebase.sr);
println!("MIDI PPQ: {:?}", &project.clock.timebase.ppq);
println!("Tempo: {:?}", &project.clock.timebase.bpm);
println!("Quantize: {:?}", &project.clock.quant);
println!("Launch: {:?}", &project.clock.sync);
println!("Playhead: {:?}us", &project.clock.playhead.usec);
println!("Playhead: {:?}s", &project.clock.playhead.sample);
println!("Playhead: {:?}p", &project.clock.playhead.pulse);
println!("Started: {:?}", &project.clock.started);
println!("Tracks:");
for (i, t) in project.tracks.iter().enumerate() {
println!(" Track {i}: {} {} {:?} {:?}", t.name, t.width,
&t.sequencer.play_clip, &t.sequencer.next_clip);
}
println!("Scenes:");
for (i, t) in project.scenes.iter().enumerate() {
println!(" Scene {i}: {} {:?}", &t.name, &t.clips);
}
println!("MIDI Ins: {:?}", &project.midi_ins);
println!("MIDI Outs: {:?}", &project.midi_outs);
println!("Audio Ins: {:?}", &project.audio_ins);
println!("Audio Outs: {:?}", &project.audio_outs);
// TODO git integration
// TODO dawvert integration
}
}

2
tengri

@ -1 +1 @@
Subproject commit 482b6241690f7b116b07d06fb4950754f6fdf584 Subproject commit c0d6d0174e8858108ec053f4ac486cf6229980e1