Compare commits

..

No commits in common. "07f229001702a095db13b112be7708dc9ac3d720" and "34bd2eeacf6060ea0a187e2e29e668a1562c0634" have entirely different histories.

28 changed files with 2582 additions and 2101 deletions

1130
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

@ -1,25 +1,11 @@
use crate::*;
pub mod audio; #[allow(unused)] pub use self::audio::*;
pub mod bind; pub use self::bind::*;
pub mod cli; pub use self::cli::*;
pub mod config; pub use self::config::*;
pub mod draw; pub use self::draw::*;
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.
///
/// ```
@ -43,21 +29,7 @@ impl_has_clips!( |self: App|self.pool.clips);
/// //let _ = app.project.h_outputs();
/// let _ = app.project.h_scenes();
/// ```
#[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 {
#[derive(Default, Debug)] pub struct App {
/// Base color.
pub color: ItemTheme,
/// Must not be dropped for the duration of the process
@ -81,7 +53,9 @@ pub struct App {
/// Error, if any
pub error: Arc<RwLock<Option<Arc<str>>>>
}
impl App {
/// Create a new application instance from a backend, project, config, and mode
///
/// ```
@ -105,6 +79,36 @@ impl App {
..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.
/// ```
/// tek::App::default().update_clock();
@ -223,139 +227,99 @@ impl App {
}
}
}
fn get_arc_str (&self, src: impl Language) -> Perhaps<Arc<str>> {
Ok(src.src()?.map(|x|x.into()))
}
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!(),
})
}
}
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
}
}; });
pub fn swap_value <T: Clone + PartialEq, U> (
target: &mut T, value: &T, returned: impl Fn(T)->U
) -> Perhaps<U> {
@ -396,3 +360,4 @@ pub fn scan (dir: &PathBuf) -> Usually<(Vec<OsString>, Vec<OsString>)> {
files.sort();
Ok((subdirs, files))
}

View file

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

259
src/app/cli.rs Normal file
View file

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

View file

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

View file

@ -1,3 +1,20 @@
#[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)]
pub(crate) use ::{
std::{
@ -13,20 +30,4 @@ pub(crate) use ::{
time::Duration,
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,7 +100,6 @@ pub trait HasDevices: AsRef<Vec<Device>> + AsMut<Vec<Device>> {
}
}
pub use ::tengri::sing::*;
pub mod arrange; pub use self::arrange::*;
pub mod browse; pub use self::browse::*;
pub mod clock; pub use self::clock::*;
@ -109,36 +108,9 @@ pub mod editor; pub use self::editor::*;
pub mod menu; pub use self::menu::*;
pub mod meter; pub use self::meter::*;
pub mod mix; pub use self::mix::*;
pub mod port; pub use self::port::*;
pub mod sampler; pub use self::sampler::*;
pub mod sequence; pub use self::sequence::*;
#[cfg(feature = "plugin")] pub mod 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,8 +5,7 @@ use crate::*;
/// ```
/// let arranger = tek::Arrangement::default();
/// ```
#[derive(Default, Debug)]
pub struct Arrangement {
#[derive(Default, Debug)] pub struct Arrangement {
/// Project name.
pub name: Arc<str>,
/// Base color.
@ -20,8 +19,6 @@ pub struct Arrangement {
pub size: Sizer,
/// Display size of clips area
pub size_inner: Sizer,
/// Selected UI element
pub selection: Selection,
/// Source of time
#[cfg(feature = "clock")] pub clock: Clock,
/// Allows one MIDI clip to be edited
@ -34,6 +31,8 @@ pub struct Arrangement {
#[cfg(feature = "port")] pub audio_ins: Vec<AudioInput>,
/// List of global audio outputs
#[cfg(feature = "port")] pub audio_outs: Vec<AudioOutput>,
/// Selected UI element
#[cfg(feature = "select")] pub selection: Selection,
/// Last track number (to avoid duplicate port names)
#[cfg(feature = "track")] pub track_last: usize,
/// List of tracks
@ -46,58 +45,34 @@ pub struct Arrangement {
#[cfg(feature = "scene")] pub scene_scroll: usize,
}
/// 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 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());
impl Arrangement {
/// Create a new arrangement.
pub fn new (
jack: &Jack<'static>,
name: Arc<str>,
clock: Clock,
tracks: impl Iterator<Item = Track>,
scenes: impl Iterator<Item = Scene>,
midi_ins: impl Iterator<Item = MidiInput>,
midi_outs: impl Iterator<Item = MidiOutput>,
audio_ins: impl Iterator<Item = AudioInput>,
audio_outs: impl Iterator<Item = AudioOutput>,
jack: &Jack<'static>,
name: Option<Arc<str>>,
clock: Clock,
tracks: Vec<Track>,
scenes: Vec<Scene>,
midi_ins: Vec<MidiInput>,
midi_outs: Vec<MidiOutput>,
) -> Self {
Self {
jack: jack.clone(),
color: ItemTheme::random(),
selection: Selection::TrackClip { track: 0, scene: 0 },
tracks: tracks.collect(),
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,
clock, tracks, scenes, midi_ins, midi_outs,
jack: jack.clone(),
name: name.unwrap_or_default(),
color: ItemTheme::random(),
selection: Selection::TrackClip { track: 0, scene: 0 },
..Default::default()
}
}
@ -142,759 +117,7 @@ impl Arrangement {
}
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()
}
}
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
}
#[cfg(feature = "clip")] mod clip; #[cfg(feature = "clip")] pub use self::clip::*;
#[cfg(feature = "scene")] mod scene; #[cfg(feature = "scene")] pub use self::scene::*;
#[cfg(feature = "track")] mod track; #[cfg(feature = "track")] pub use self::track::*;
#[cfg(feature = "select")] mod select; #[cfg(feature = "select")] pub use self::select::*;

View file

@ -0,0 +1,95 @@
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 }
}

174
src/device/arrange/scene.rs Normal file
View file

@ -0,0 +1,174 @@
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

@ -0,0 +1,161 @@
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()
}
}

350
src/device/arrange/track.rs Normal file
View file

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

0
src/device/midi.rs Normal file
View file

136
src/device/port.rs Normal file
View file

@ -0,0 +1,136 @@
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
}))
}
}

103
src/device/port/audio.rs Normal file
View file

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

View file

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

256
src/device/port/midi.rs Normal file
View file

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

View file

@ -0,0 +1,27 @@
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,6 +1,5 @@
use crate::{*, clock::*, device::*};
impl <T: AsRef<Sequencer>+AsMut<Sequencer>> HasSequencer for T {}
/// A MIDI sequence.
@ -230,6 +229,7 @@ pub trait MidiRecord: MidiMonitor + HasClock + HasPlayClip {
pub type MidiData = Vec<Vec<MidiMessage>>;
pub type ClipPool = Vec<Arc<RwLock<MidiClip>>>;
pub type CollectedMidiInput<'a> = Vec<Vec<(u32, Result<LiveEvent<'a>, MidiError>)>>;
pub trait HasClips {
fn clips <'a> (&'a self) -> std::sync::RwLockReadGuard<'a, ClipPool>;
@ -592,6 +592,35 @@ 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))
}
/// 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
pub fn note_duration_prev (pulses: usize) -> usize {
for (length, _) in NOTE_DURATIONS.iter().rev() { if *length < pulses { return *length } }

View file

@ -1,76 +1,3 @@
(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 :confirm (@enter confirm))
(keys :axis/x (@left x/dec) (@right x/inc))
@ -83,67 +10,113 @@
(keys :axis/i2 (@lt i2/dec) (@gt z2/inc))
(keys :axis/w (@openbracket w/dec) (@closebracket w/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 :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))
(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))
(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 :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))
(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 :browse (bsp/s
(padding 3 1 :browse-title)
(enclose (fg (g 96)) browser)))

View file

@ -6,295 +6,33 @@
trait_alias,
type_changing_struct_update
)]
pub extern crate atomic_float;
pub extern crate xdg;
pub extern crate midly;
pub extern crate tengri;
/// Command-line entrypoint.
#[cfg(feature = "cli")] pub fn main () -> Usually<()> {
tengri::Tui::setup_panic();
let name = "tek";
let mode = ":menu";
let title = "untitled!";
let bpm = 74.;
Config::watch(|config|tengri::Tui::run_main(Jack::new_run(name, move|jack|{
let clock = Clock::new(&jack, Some(bpm))?;
let tracks = [];
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(|_|())
tui_setup_panic();
Config::watch(|config|{
tui_run_main(Jack::new_run("tek", move|jack|{
let clock = Clock::new(&jack, Some(51.))?;
let project = Arrangement::new(&jack, None, clock, vec![], vec![], vec![], vec![]);
let app = App::new(&jack, project, config, ":menu");
Ok(app)
})?)
}).map(|_|())
}
pub mod deps;
pub(crate) use self::deps::*;
pub mod device;
pub use self::device::*;
pub mod app;
pub use self::app::*;
pub mod deps; pub use self::deps::*;
pub mod device; pub use self::device::*;
pub mod app; pub use self::app::*;
/// CLI banner.
pub(crate) const HEADER: &'static str = r#"
~ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~
~ 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 c0d6d0174e8858108ec053f4ac486cf6229980e1
Subproject commit 482b6241690f7b116b07d06fb4950754f6fdf584