mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-02-21 08:19:03 +01:00
1333 lines
48 KiB
Rust
1333 lines
48 KiB
Rust
#![allow(clippy::unit_arg)]
|
||
#![feature(adt_const_params,
|
||
associated_type_defaults,
|
||
closure_lifetime_binder,
|
||
if_let_guard,
|
||
impl_trait_in_assoc_type,
|
||
trait_alias,
|
||
type_alias_impl_trait,
|
||
type_changing_struct_update)]
|
||
|
||
#[allow(unused)] pub(crate) use ::{
|
||
std::{
|
||
cmp::Ord,
|
||
collections::BTreeMap,
|
||
error::Error,
|
||
ffi::OsString,
|
||
fmt::{Write, Debug, Formatter},
|
||
fs::File,
|
||
ops::{Add, Sub, Mul, Div, Rem},
|
||
path::{Path, PathBuf},
|
||
sync::{Arc, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}},
|
||
thread::{spawn, JoinHandle},
|
||
},
|
||
};
|
||
extern crate xdg;
|
||
pub(crate) use ::xdg::BaseDirectories;
|
||
pub extern crate atomic_float;
|
||
pub(crate) use atomic_float::AtomicF64;
|
||
pub extern crate jack;
|
||
pub(crate) use ::jack::{*, contrib::{*, ClosureProcessHandler}};
|
||
pub extern crate midly;
|
||
pub(crate) use ::midly::{Smf, TrackEventKind, MidiMessage, Error as MidiError, num::*, live::*};
|
||
pub extern crate tengri;
|
||
pub(crate) use tengri::{
|
||
*,
|
||
ratatui::{
|
||
self,
|
||
prelude::{Rect, Style, Stylize, Buffer, Modifier, buffer::Cell, Color::{self, *}},
|
||
widgets::{Widget, canvas::{Canvas, Line}},
|
||
},
|
||
crossterm::{
|
||
self,
|
||
event::{Event, KeyEvent, KeyCode::{self, *}},
|
||
},
|
||
};
|
||
#[cfg(feature = "sampler")] pub(crate) use symphonia::{
|
||
core::{
|
||
formats::Packet,
|
||
codecs::{Decoder, CODEC_TYPE_NULL},
|
||
//errors::Error as SymphoniaError,
|
||
io::MediaSourceStream,
|
||
probe::Hint,
|
||
audio::SampleBuffer,
|
||
},
|
||
default::get_codecs,
|
||
};
|
||
#[cfg(feature = "lv2_gui")] use ::winit::{
|
||
application::ApplicationHandler,
|
||
event::WindowEvent,
|
||
event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
|
||
window::{Window, WindowId},
|
||
platform::x11::EventLoopBuilderExtX11
|
||
};
|
||
|
||
#[cfg(test)] mod tek_test;
|
||
|
||
mod tek_struct;
|
||
pub use self::tek_struct::*;
|
||
|
||
mod tek_trait;
|
||
pub use self::tek_trait::*;
|
||
|
||
mod tek_type;
|
||
pub use self::tek_type::*;
|
||
|
||
mod tek_impls;
|
||
|
||
pub(crate) use ConnectName::*;
|
||
pub(crate) use ConnectScope::*;
|
||
pub(crate) use ConnectStatus::*;
|
||
pub(crate) use JackState::*;
|
||
|
||
/// Command-line entrypoint.
|
||
#[cfg(feature = "cli")] pub fn main () -> Usually<()> {
|
||
use clap::Parser;
|
||
Cli::parse().run()
|
||
}
|
||
|
||
/// Create a new application from a backend, project, config, and mode
|
||
///
|
||
/// ```
|
||
/// let jack = tek::Jack::new(&"test_tek").expect("failed to connect to jack");
|
||
/// let proj = Default::default();
|
||
/// let conf = Default::default();
|
||
/// let tek = tek::tek(&jack, proj, conf, "");
|
||
/// ```
|
||
pub fn tek (
|
||
jack: &Jack<'static>, project: Arrangement, config: Config, mode: impl AsRef<str>
|
||
) -> App {
|
||
App {
|
||
color: ItemTheme::random(),
|
||
dialog: Dialog::welcome(),
|
||
jack: jack.clone(),
|
||
mode: config.get_mode(mode).expect("failed to find mode"),
|
||
config,
|
||
project,
|
||
..Default::default()
|
||
}
|
||
}
|
||
|
||
|
||
fn tek_confirm (state: &mut App) -> Perhaps<AppCommand> {
|
||
Ok(match &state.dialog {
|
||
Dialog::Menu(index, items) => {
|
||
let callback = items.0[*index].1.clone();
|
||
callback(state)?;
|
||
None
|
||
},
|
||
_ => todo!(),
|
||
})
|
||
}
|
||
|
||
fn tek_inc (state: &mut App, axis: &ControlAxis) -> Perhaps<AppCommand> {
|
||
Ok(match (&state.dialog, axis) {
|
||
(Dialog::None, _) => todo!(),
|
||
(Dialog::Menu(_, _), ControlAxis::Y) => AppCommand::SetDialog { dialog: state.dialog.menu_next() }
|
||
.execute(state)?,
|
||
_ => todo!()
|
||
})
|
||
}
|
||
|
||
fn tek_dec (state: &mut App, axis: &ControlAxis) -> Perhaps<AppCommand> {
|
||
Ok(match (&state.dialog, axis) {
|
||
(Dialog::None, _) => None,
|
||
(Dialog::Menu(_, _), ControlAxis::Y) => AppCommand::SetDialog { dialog: state.dialog.menu_prev() }
|
||
.execute(state)?,
|
||
_ => todo!()
|
||
})
|
||
}
|
||
|
||
pub fn load_view (views: &Views, name: &impl AsRef<str>, body: &impl Language) -> Usually<()> {
|
||
views.write().unwrap().insert(name.as_ref().into(), body.src()?.unwrap_or_default().into());
|
||
Ok(())
|
||
}
|
||
|
||
pub fn load_mode (modes: &Modes, name: &impl AsRef<str>, body: &impl Language) -> Usually<()> {
|
||
let mut mode = Mode::default();
|
||
body.each(|item|mode.add(item))?;
|
||
modes.write().unwrap().insert(name.as_ref().into(), Arc::new(mode));
|
||
Ok(())
|
||
}
|
||
|
||
pub fn load_bind (binds: &Binds, name: &impl AsRef<str>, body: &impl Language) -> Usually<()> {
|
||
let mut map = Bind::new();
|
||
body.each(|item|if item.expr().head() == Ok(Some("see")) {
|
||
// TODO
|
||
Ok(())
|
||
} else if let Ok(Some(_word)) = item.expr().head().word() {
|
||
if let Some(key) = TuiEvent::from_dsl(item.expr()?.head()?)? {
|
||
map.add(key, Binding {
|
||
commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(),
|
||
condition: None,
|
||
description: None,
|
||
source: None
|
||
});
|
||
Ok(())
|
||
} else if Some(":char") == item.expr()?.head()? {
|
||
// TODO
|
||
return Ok(())
|
||
} else {
|
||
return Err(format!("Config::load_bind: invalid key: {:?}", item.expr()?.head()?).into())
|
||
}
|
||
} else {
|
||
return Err(format!("Config::load_bind: unexpected: {item:?}").into())
|
||
})?;
|
||
binds.write().unwrap().insert(name.as_ref().into(), map);
|
||
Ok(())
|
||
}
|
||
|
||
/// CLI banner.
|
||
pub(crate) const HEADER: &'static str = r#"
|
||
|
||
~ █▀█▀█ █▀▀█ █ █ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~
|
||
█ █▀ █▀▀▄ ~ v0.4.0, 2026 winter (or is it) ~
|
||
~ ▀ █▀▀█ ▀ ▀ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ "#;
|
||
|
||
fn collect_commands (
|
||
app: &App, input: &TuiIn
|
||
) -> Usually<Vec<AppCommand>> {
|
||
let mut commands = vec![];
|
||
for id in app.mode.keys.iter() {
|
||
if let Some(event_map) = app.config.binds.clone().read().unwrap().get(id.as_ref())
|
||
&& let Some(bindings) = event_map.query(input.event()) {
|
||
for binding in bindings {
|
||
for command in binding.commands.iter() {
|
||
if let Some(command) = app.namespace(command)? as Option<AppCommand> {
|
||
commands.push(command)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
Ok(commands)
|
||
}
|
||
fn execute_commands (
|
||
app: &mut App, commands: Vec<AppCommand>
|
||
) -> Usually<Vec<(AppCommand, Option<AppCommand>)>> {
|
||
let mut history = vec![];
|
||
for command in commands.into_iter() {
|
||
let result = command.execute(app);
|
||
match result { Err(err) => { history.push((command, None)); return Err(err) }
|
||
Ok(undo) => { history.push((command, undo)); } };
|
||
}
|
||
Ok(history)
|
||
}
|
||
pub fn tek_jack_process (app: &mut App, client: &Client, scope: &ProcessScope) -> Control {
|
||
let t0 = app.perf.get_t0();
|
||
app.clock().update_from_scope(scope).unwrap();
|
||
let midi_in = app.project.midi_input_collect(scope);
|
||
if let Some(editor) = &app.editor() {
|
||
let mut pitch: Option<u7> = None;
|
||
for port in midi_in.iter() {
|
||
for event in port.iter() {
|
||
if let (_, Ok(LiveEvent::Midi {message: MidiMessage::NoteOn {key, ..}, ..}))
|
||
= event
|
||
{
|
||
pitch = Some(key.clone());
|
||
}
|
||
}
|
||
}
|
||
if let Some(pitch) = pitch {
|
||
editor.set_note_pos(pitch.as_int() as usize);
|
||
}
|
||
}
|
||
let result = app.project.process_tracks(client, scope);
|
||
app.perf.update_from_jack_scope(t0, scope);
|
||
result
|
||
}
|
||
pub fn tek_jack_event (app: &mut App, event: JackEvent) {
|
||
use JackEvent::*;
|
||
match event {
|
||
SampleRate(sr) => { app.clock().timebase.sr.set(sr as f64); },
|
||
PortRegistration(_id, true) => {
|
||
//let port = app.jack().port_by_id(id);
|
||
//println!("\rport add: {id} {port:?}");
|
||
//println!("\rport add: {id}");
|
||
},
|
||
PortRegistration(_id, false) => {
|
||
/*println!("\rport del: {id}")*/
|
||
},
|
||
PortsConnected(_a, _b, true) => { /*println!("\rport conn: {a} {b}")*/ },
|
||
PortsConnected(_a, _b, false) => { /*println!("\rport disc: {a} {b}")*/ },
|
||
ClientRegistration(_id, true) => {},
|
||
ClientRegistration(_id, false) => {},
|
||
ThreadInit => {},
|
||
XRun => {},
|
||
GraphReorder => {},
|
||
_ => { panic!("{event:?}"); }
|
||
}
|
||
}
|
||
pub fn 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);
|
||
}
|
||
}
|
||
}
|
||
for (k, v) in config.modes.read().unwrap().iter() {
|
||
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!();
|
||
for (k, v) in v.modes.read().unwrap().iter() {
|
||
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
|
||
}
|
||
|
||
pub const DEFAULT_PPQ: f64 = 96.0;
|
||
|
||
/// FIXME: remove this and use PPQ from timebase everywhere:
|
||
pub const PPQ: usize = 96;
|
||
|
||
/// (pulses, name), assuming 96 PPQ
|
||
pub const NOTE_DURATIONS: [(usize, &str);26] = [
|
||
(1, "1/384"), (2, "1/192"),
|
||
(3, "1/128"), (4, "1/96"),
|
||
(6, "1/64"), (8, "1/48"),
|
||
(12, "1/32"), (16, "1/24"),
|
||
(24, "1/16"), (32, "1/12"),
|
||
(48, "1/8"), (64, "1/6"),
|
||
(96, "1/4"), (128, "1/3"),
|
||
(192, "1/2"), (256, "2/3"),
|
||
(384, "1/1"), (512, "4/3"),
|
||
(576, "3/2"), (768, "2/1"),
|
||
(1152, "3/1"), (1536, "4/1"),
|
||
(2304, "6/1"), (3072, "8/1"),
|
||
(3456, "9/1"), (6144, "16/1"),
|
||
];
|
||
|
||
pub const NOTE_NAMES: [&str; 128] = [
|
||
"C0", "C#0", "D0", "D#0", "E0", "F0", "F#0", "G0", "G#0", "A0", "A#0", "B0",
|
||
"C1", "C#1", "D1", "D#1", "E1", "F1", "F#1", "G1", "G#1", "A1", "A#1", "B1",
|
||
"C2", "C#2", "D2", "D#2", "E2", "F2", "F#2", "G2", "G#2", "A2", "A#2", "B2",
|
||
"C3", "C#3", "D3", "D#3", "E3", "F3", "F#3", "G3", "G#3", "A3", "A#3", "B3",
|
||
"C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4",
|
||
"C5", "C#5", "D5", "D#5", "E5", "F5", "F#5", "G5", "G#5", "A5", "A#5", "B5",
|
||
"C6", "C#6", "D6", "D#6", "E6", "F6", "F#6", "G6", "G#6", "A6", "A#6", "B6",
|
||
"C7", "C#7", "D7", "D#7", "E7", "F7", "F#7", "G7", "G#7", "A7", "A#7", "B7",
|
||
"C8", "C#8", "D8", "D#8", "E8", "F8", "F#8", "G8", "G#8", "A8", "A#8", "B8",
|
||
"C9", "C#9", "D9", "D#9", "E9", "F9", "F#9", "G9", "G#9", "A9", "A#9", "B9",
|
||
"C10", "C#10", "D10", "D#10", "E10", "F10", "F#10", "G10",
|
||
];
|
||
|
||
/// Return boxed iterator of MIDI events
|
||
pub fn parse_midi_input <'a> (input: ::jack::MidiIter<'a>)
|
||
-> Box<dyn Iterator<Item=(usize, LiveEvent<'a>, &'a [u8])> + 'a>
|
||
{
|
||
Box::new(input.map(|::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 } }
|
||
pulses
|
||
}
|
||
|
||
/// Returns the next longer length
|
||
pub fn note_duration_next (pulses: usize) -> usize {
|
||
for (length, _) in NOTE_DURATIONS.iter() { if *length > pulses { return *length } }
|
||
pulses
|
||
}
|
||
|
||
pub fn note_duration_to_name (pulses: usize) -> &'static str {
|
||
for (length, name) in NOTE_DURATIONS.iter() { if *length == pulses { return name } }
|
||
""
|
||
}
|
||
|
||
pub fn note_pitch_to_name (n: usize) -> &'static str {
|
||
if n > 127 {
|
||
panic!("to_note_name({n}): must be 0-127");
|
||
}
|
||
NOTE_NAMES[n]
|
||
}
|
||
|
||
|
||
//macro_rules! impl_port {
|
||
//($Name:ident : $Spec:ident -> $Pair:ident |$jack:ident, $name:ident|$port:expr) => {
|
||
//#[derive(Debug)] pub struct $Name {
|
||
///// Handle to JACK client, for receiving reconnect events.
|
||
//jack: Jack<'static>,
|
||
///// Port name
|
||
//name: Arc<str>,
|
||
///// Port handle.
|
||
//port: Port<$Spec>,
|
||
///// List of ports to connect to.
|
||
//conn: Vec<PortConnect>
|
||
//}
|
||
//impl AsRef<Port<$Spec>> for $Name {
|
||
//fn as_ref (&self) -> &Port<$Spec> { &self.port }
|
||
//}
|
||
//impl $Name {
|
||
//pub fn new ($jack: &Jack, name: impl AsRef<str>, connect: &[PortConnect])
|
||
//-> Usually<Self>
|
||
//{
|
||
//let $name = name.as_ref();
|
||
//let jack = $jack.clone();
|
||
//let port = $port?;
|
||
//let name = $name.into();
|
||
//let conn = connect.to_vec();
|
||
//let port = Self { jack, port, name, conn };
|
||
//port.connect_to_matching()?;
|
||
//Ok(port)
|
||
//}
|
||
//pub fn name (&self) -> &Arc<str> { &self.name }
|
||
//pub fn port (&self) -> &Port<$Spec> { &self.port }
|
||
//pub fn port_mut (&mut self) -> &mut Port<$Spec> { &mut self.port }
|
||
//pub fn into_port (self) -> Port<$Spec> { self.port }
|
||
//pub fn close (self) -> Usually<()> {
|
||
//let Self { jack, port, .. } = self;
|
||
//Ok(jack.with_client(|client|client.unregister_port(port))?)
|
||
//}
|
||
//}
|
||
//impl HasJack<'static> for $Name {
|
||
//fn jack (&self) -> &'static Jack<'static> { &self.jack }
|
||
//}
|
||
//impl JackPort<'static> for $Name {
|
||
//type Port = $Spec;
|
||
//type Pair = $Pair;
|
||
//fn port (&self) -> &Port<$Spec> { &self.port }
|
||
//}
|
||
//impl ConnectTo<'static, &str> for $Name {
|
||
//fn connect_to (&self, to: &str) -> Usually<PortConnectStatus> {
|
||
//self.with_client(|c|if let Some(ref port) = c.port_by_name(to.as_ref()) {
|
||
//self.connect_to(port)
|
||
//} else {
|
||
//Ok(Missing)
|
||
//})
|
||
//}
|
||
//}
|
||
//impl ConnectTo<'static, &Port<Unowned>> for $Name {
|
||
//fn connect_to (&self, port: &Port<Unowned>) -> Usually<PortConnectStatus> {
|
||
//self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(&self.port, port) {
|
||
//Connected
|
||
//} else if let Ok(_) = c.connect_ports(port, &self.port) {
|
||
//Connected
|
||
//} else {
|
||
//Mismatch
|
||
//}))
|
||
//}
|
||
//}
|
||
//impl ConnectTo<'static, &Port<$Pair>> for $Name {
|
||
//fn connect_to (&self, port: &Port<$Pair>) -> Usually<PortConnectStatus> {
|
||
//self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(&self.port, port) {
|
||
//Connected
|
||
//} else if let Ok(_) = c.connect_ports(port, &self.port) {
|
||
//Connected
|
||
//} else {
|
||
//Mismatch
|
||
//}))
|
||
//}
|
||
//}
|
||
//impl ConnectAuto<'static> for $Name {
|
||
//fn connections (&self) -> &[PortConnect] {
|
||
//&self.conn
|
||
//}
|
||
//}
|
||
//};
|
||
//}
|
||
|
||
|
||
|
||
|
||
pub fn swap_value <T: Clone + PartialEq, U> (
|
||
target: &mut T, value: &T, returned: impl Fn(T)->U
|
||
) -> Perhaps<U> {
|
||
if *target == *value {
|
||
Ok(None)
|
||
} else {
|
||
let mut value = value.clone();
|
||
std::mem::swap(target, &mut value);
|
||
Ok(Some(returned(value)))
|
||
}
|
||
}
|
||
|
||
pub fn toggle_bool <U> (
|
||
target: &mut bool, value: &Option<bool>, returned: impl Fn(Option<bool>)->U
|
||
) -> Perhaps<U> {
|
||
let mut value = value.unwrap_or(!*target);
|
||
if value == *target {
|
||
Ok(None)
|
||
} else {
|
||
std::mem::swap(target, &mut value);
|
||
Ok(Some(returned(Some(value))))
|
||
}
|
||
}
|
||
|
||
pub fn device_kinds () -> &'static [&'static str] {
|
||
&[
|
||
#[cfg(feature = "sampler")] "Sampler",
|
||
#[cfg(feature = "lv2")] "Plugin (LV2)",
|
||
]
|
||
}
|
||
|
||
impl<T: Has<Vec<Device>>> HasDevices for T {
|
||
fn devices (&self) -> &Vec<Device> {
|
||
self.get()
|
||
}
|
||
fn devices_mut (&mut self) -> &mut Vec<Device> {
|
||
self.get_mut()
|
||
}
|
||
}
|
||
|
||
impl Device {
|
||
pub fn name (&self) -> &str {
|
||
match self {
|
||
Self::Sampler(sampler) => sampler.name.as_ref(),
|
||
_ => todo!(),
|
||
}
|
||
}
|
||
pub fn midi_ins (&self) -> &[MidiInput] {
|
||
match self {
|
||
//Self::Sampler(Sampler { midi_in, .. }) => &[midi_in],
|
||
_ => todo!()
|
||
}
|
||
}
|
||
pub fn midi_outs (&self) -> &[MidiOutput] {
|
||
match self {
|
||
Self::Sampler(_) => &[],
|
||
_ => todo!()
|
||
}
|
||
}
|
||
pub fn audio_ins (&self) -> &[AudioInput] {
|
||
match self {
|
||
Self::Sampler(Sampler { audio_ins, .. }) => audio_ins.as_slice(),
|
||
_ => todo!()
|
||
}
|
||
}
|
||
pub fn audio_outs (&self) -> &[AudioOutput] {
|
||
match self {
|
||
Self::Sampler(Sampler { audio_outs, .. }) => audio_outs.as_slice(),
|
||
_ => todo!()
|
||
}
|
||
}
|
||
}
|
||
|
||
audio!(|self: DeviceAudio<'a>, client, scope|{
|
||
use Device::*;
|
||
match self.0 {
|
||
Mute => { Control::Continue },
|
||
Bypass => { /*TODO*/ Control::Continue },
|
||
#[cfg(feature = "sampler")] Sampler(sampler) => sampler.process(client, scope),
|
||
#[cfg(feature = "lv2")] Lv2(lv2) => lv2.process(client, scope),
|
||
#[cfg(feature = "vst2")] Vst2 => { todo!() }, // TODO
|
||
#[cfg(feature = "vst3")] Vst3 => { todo!() }, // TODO
|
||
#[cfg(feature = "clap")] Clap => { todo!() }, // TODO
|
||
#[cfg(feature = "sf2")] Sf2 => { todo!() }, // TODO
|
||
}
|
||
});
|
||
|
||
//take!(DeviceCommand|state: Arrangement, iter|state.selected_device().as_ref()
|
||
//.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten()));
|
||
|
||
|
||
#[macro_export] macro_rules! has_clip {
|
||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||
impl $(<$($L),*$($T $(: $U)?),*>)? HasMidiClip for $Struct $(<$($L),*$($T),*>)? {
|
||
fn clip (&$self) -> Option<Arc<RwLock<MidiClip>>> { $cb }
|
||
}
|
||
}
|
||
}
|
||
|
||
fn to_key (note: usize) -> &'static str {
|
||
match note % 12 {
|
||
11 | 9 | 7 | 5 | 4 | 2 | 0 => "████▌",
|
||
10 | 8 | 6 | 3 | 1 => " ",
|
||
_ => unreachable!(),
|
||
}
|
||
}
|
||
|
||
pub(crate) fn note_y_iter (note_lo: usize, note_hi: usize, y0: u16)
|
||
-> impl Iterator<Item=(usize, u16, usize)>
|
||
{
|
||
(note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n))
|
||
}
|
||
|
||
|
||
#[cfg(feature = "lv2_gui")]
|
||
pub fn run_lv2_ui (mut ui: LV2PluginUI) -> Usually<JoinHandle<()>> {
|
||
Ok(spawn(move||{
|
||
let event_loop = EventLoop::builder().with_x11().with_any_thread(true).build().unwrap();
|
||
event_loop.set_control_flow(ControlFlow::Wait);
|
||
event_loop.run_app(&mut ui).unwrap()
|
||
}))
|
||
}
|
||
|
||
#[cfg(feature = "lv2_gui")]
|
||
fn lv2_ui_instantiate (kind: &str) {
|
||
//let host = Suil
|
||
}
|
||
|
||
pub fn mix_summing <const N: usize> (
|
||
buffer: &mut [Vec<f32>], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>,
|
||
) -> bool {
|
||
let channels = buffer.len();
|
||
for index in 0..frames {
|
||
if let Some(frame) = next() {
|
||
for (channel, sample) in frame.iter().enumerate() {
|
||
let channel = channel % channels;
|
||
buffer[channel][index] += sample * gain;
|
||
}
|
||
} else {
|
||
return false
|
||
}
|
||
}
|
||
true
|
||
}
|
||
|
||
pub fn mix_average <const N: usize> (
|
||
buffer: &mut [Vec<f32>], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>,
|
||
) -> bool {
|
||
let channels = buffer.len();
|
||
for index in 0..frames {
|
||
if let Some(frame) = next() {
|
||
for (channel, sample) in frame.iter().enumerate() {
|
||
let channel = channel % channels;
|
||
let value = buffer[channel][index];
|
||
buffer[channel][index] = (value + sample * gain) / 2.0;
|
||
}
|
||
} else {
|
||
return false
|
||
}
|
||
}
|
||
true
|
||
}
|
||
|
||
pub fn to_log10 (samples: &[f32]) -> f32 {
|
||
let total: f32 = samples.iter().map(|x|x.abs()).sum();
|
||
let count = samples.len() as f32;
|
||
10. * (total / count).log10()
|
||
}
|
||
|
||
|
||
pub fn to_rms (samples: &[f32]) -> f32 {
|
||
let sum = samples.iter()
|
||
.map(|s|*s)
|
||
.reduce(|sum, sample|sum + sample.abs())
|
||
.unwrap_or(0.0);
|
||
(sum / samples.len() as f32).sqrt()
|
||
}
|
||
|
||
|
||
fn read_sample_data (_: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
|
||
todo!();
|
||
}
|
||
|
||
fn scan (dir: &PathBuf) -> Usually<(Vec<OsString>, Vec<OsString>)> {
|
||
let (mut subdirs, mut files) = std::fs::read_dir(dir)?
|
||
.fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{
|
||
let entry = entry.expect("failed to read drectory entry");
|
||
let meta = entry.metadata().expect("failed to read entry metadata");
|
||
if meta.is_file() {
|
||
files.push(entry.file_name());
|
||
} else if meta.is_dir() {
|
||
subdirs.push(entry.file_name());
|
||
}
|
||
(subdirs, files)
|
||
});
|
||
subdirs.sort();
|
||
files.sort();
|
||
Ok((subdirs, files))
|
||
}
|
||
|
||
|
||
|
||
pub(crate) fn track_width (_index: usize, track: &Track) -> u16 {
|
||
track.width as u16
|
||
}
|
||
|
||
|
||
#[cfg(feature = "vst2")] fn set_vst_plugin <E: Engine> (
|
||
host: &Arc<Mutex<Plugin<E>>>, _path: &str
|
||
) -> Usually<PluginKind> {
|
||
let mut loader = ::vst::host::PluginLoader::load(
|
||
&std::path::Path::new("/nix/store/ij3sz7nqg5l7v2dygdvzy3w6cj62bd6r-helm-0.9.0/lib/lxvst/helm.so"),
|
||
host.clone()
|
||
)?;
|
||
Ok(PluginKind::VST2 {
|
||
instance: loader.instance()?
|
||
})
|
||
}
|
||
|
||
def_command!(AppCommand: |app: App| {
|
||
Nop => Ok(None),
|
||
Confirm => tek_confirm(app),
|
||
Cancel => todo!(), // TODO delegate:
|
||
Inc { axis: ControlAxis } => tek_inc(app, axis),
|
||
Dec { axis: ControlAxis } => tek_dec(app, axis),
|
||
SetDialog { dialog: Dialog } => {
|
||
swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog })
|
||
},
|
||
});
|
||
|
||
def_command!(ClockCommand: |clock: Clock| {
|
||
SeekUsec { usec: f64 } => {
|
||
clock.playhead.update_from_usec(*usec); Ok(None) },
|
||
SeekSample { sample: f64 } => {
|
||
clock.playhead.update_from_sample(*sample); Ok(None) },
|
||
SeekPulse { pulse: f64 } => {
|
||
clock.playhead.update_from_pulse(*pulse); Ok(None) },
|
||
SetBpm { bpm: f64 } => Ok(Some(
|
||
Self::SetBpm { bpm: clock.timebase().bpm.set(*bpm) })),
|
||
SetQuant { quant: f64 } => Ok(Some(
|
||
Self::SetQuant { quant: clock.quant.set(*quant) })),
|
||
SetSync { sync: f64 } => Ok(Some(
|
||
Self::SetSync { sync: clock.sync.set(*sync) })),
|
||
|
||
Play { position: Option<u32> } => {
|
||
clock.play_from(*position)?; Ok(None) /* TODO Some(Pause(previousPosition)) */ },
|
||
Pause { position: Option<u32> } => {
|
||
clock.pause_at(*position)?; Ok(None) },
|
||
|
||
TogglePlayback { position: u32 } => Ok(if clock.is_rolling() {
|
||
clock.pause_at(Some(*position))?; None
|
||
} else {
|
||
clock.play_from(Some(*position))?; None
|
||
}),
|
||
});
|
||
|
||
def_command!(DeviceCommand: |device: Device| {});
|
||
|
||
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!()
|
||
}
|
||
});
|
||
|
||
def_command!(BrowseCommand: |browse: Browse| {
|
||
SetVisible => Ok(None),
|
||
SetPath { address: PathBuf } => Ok(None),
|
||
SetSearch { filter: Arc<str> } => Ok(None),
|
||
SetCursor { cursor: usize } => Ok(None),
|
||
});
|
||
|
||
def_command!(MidiEditCommand: |editor: MidiEditor| {
|
||
Show { clip: Option<Arc<RwLock<MidiClip>>> } => {
|
||
editor.set_clip(clip.as_ref()); editor.redraw(); Ok(None) },
|
||
DeleteNote => {
|
||
editor.redraw(); todo!() },
|
||
AppendNote { advance: bool } => {
|
||
editor.put_note(*advance); editor.redraw(); Ok(None) },
|
||
SetNotePos { pos: usize } => {
|
||
editor.set_note_pos((*pos).min(127)); editor.redraw(); Ok(None) },
|
||
SetNoteLen { len: usize } => {
|
||
editor.set_note_len(*len); editor.redraw(); Ok(None) },
|
||
SetNoteScroll { scroll: usize } => {
|
||
editor.set_note_lo((*scroll).min(127)); editor.redraw(); Ok(None) },
|
||
SetTimePos { pos: usize } => {
|
||
editor.set_time_pos(*pos); editor.redraw(); Ok(None) },
|
||
SetTimeScroll { scroll: usize } => {
|
||
editor.set_time_start(*scroll); editor.redraw(); Ok(None) },
|
||
SetTimeZoom { zoom: usize } => {
|
||
editor.set_time_zoom(*zoom); editor.redraw(); Ok(None) },
|
||
SetTimeLock { lock: bool } => {
|
||
editor.set_time_lock(*lock); editor.redraw(); Ok(None) },
|
||
// TODO: 1-9 seek markers that by default start every 8th of the clip
|
||
});
|
||
|
||
def_command!(PoolCommand: |pool: Pool| {
|
||
// Toggle visibility of pool
|
||
Show { visible: bool } => { pool.visible = *visible; Ok(Some(Self::Show { visible: !visible })) },
|
||
// Select a clip from the clip pool
|
||
Select { index: usize } => { pool.set_clip_index(*index); Ok(None) },
|
||
// Update the contents of the clip pool
|
||
Clip { command: PoolClipCommand } => Ok(command.execute(pool)?.map(|command|Self::Clip{command})),
|
||
// Rename a clip
|
||
Rename { command: RenameCommand } => Ok(command.delegate(pool, |command|Self::Rename{command})?),
|
||
// Change the length of a clip
|
||
Length { command: CropCommand } => Ok(command.delegate(pool, |command|Self::Length{command})?),
|
||
// Import from file
|
||
Import { command: BrowseCommand } => Ok(if let Some(browse) = pool.browse.as_mut() {
|
||
command.delegate(browse, |command|Self::Import{command})?
|
||
} else {
|
||
None
|
||
}),
|
||
// Export to file
|
||
Export { command: BrowseCommand } => Ok(if let Some(browse) = pool.browse.as_mut() {
|
||
command.delegate(browse, |command|Self::Export{command})?
|
||
} else {
|
||
None
|
||
}),
|
||
});
|
||
|
||
def_command!(PoolClipCommand: |pool: Pool| {
|
||
Delete { index: usize } => {
|
||
let index = *index;
|
||
let clip = pool.clips_mut().remove(index).read().unwrap().clone();
|
||
Ok(Some(Self::Add { index, clip }))
|
||
},
|
||
Swap { index: usize, other: usize } => {
|
||
let index = *index;
|
||
let other = *other;
|
||
pool.clips_mut().swap(index, other);
|
||
Ok(Some(Self::Swap { index, other }))
|
||
},
|
||
Export { index: usize, path: PathBuf } => {
|
||
todo!("export clip to midi file");
|
||
},
|
||
Add { index: usize, clip: MidiClip } => {
|
||
let index = *index;
|
||
let mut index = index;
|
||
let clip = Arc::new(RwLock::new(clip.clone()));
|
||
let mut clips = pool.clips_mut();
|
||
if index >= clips.len() {
|
||
index = clips.len();
|
||
clips.push(clip)
|
||
} else {
|
||
clips.insert(index, clip);
|
||
}
|
||
Ok(Some(Self::Delete { index }))
|
||
},
|
||
Import { index: usize, path: PathBuf } => {
|
||
let index = *index;
|
||
let bytes = std::fs::read(&path)?;
|
||
let smf = Smf::parse(bytes.as_slice())?;
|
||
let mut t = 0u32;
|
||
let mut events = vec![];
|
||
for track in smf.tracks.iter() {
|
||
for event in track.iter() {
|
||
t += event.delta.as_int();
|
||
if let TrackEventKind::Midi { channel, message } = event.kind {
|
||
events.push((t, channel.as_int(), message));
|
||
}
|
||
}
|
||
}
|
||
let mut clip = MidiClip::new("imported", true, t as usize + 1, None, None);
|
||
for event in events.iter() {
|
||
clip.notes[event.0 as usize].push(event.2);
|
||
}
|
||
Ok(Self::Add { index, clip }.execute(pool)?)
|
||
},
|
||
SetName { index: usize, name: Arc<str> } => {
|
||
let index = *index;
|
||
let clip = &mut pool.clips_mut()[index];
|
||
let old_name = clip.read().unwrap().name.clone();
|
||
clip.write().unwrap().name = name.clone();
|
||
Ok(Some(Self::SetName { index, name: old_name }))
|
||
},
|
||
SetLength { index: usize, length: usize } => {
|
||
let index = *index;
|
||
let clip = &mut pool.clips_mut()[index];
|
||
let old_len = clip.read().unwrap().length;
|
||
clip.write().unwrap().length = *length;
|
||
Ok(Some(Self::SetLength { index, length: old_len }))
|
||
},
|
||
SetColor { index: usize, color: ItemColor } => {
|
||
let index = *index;
|
||
let mut color = ItemTheme::from(*color);
|
||
std::mem::swap(&mut color, &mut pool.clips()[index].write().unwrap().color);
|
||
Ok(Some(Self::SetColor { index, color: color.base }))
|
||
},
|
||
});
|
||
|
||
def_command!(RenameCommand: |pool: Pool| {
|
||
Begin => unreachable!(),
|
||
Cancel => {
|
||
if let Some(PoolMode::Rename(clip, ref mut old_name)) = pool.mode_mut().clone() {
|
||
pool.clips()[clip].write().unwrap().name = old_name.clone().into();
|
||
}
|
||
Ok(None)
|
||
},
|
||
Confirm => {
|
||
if let Some(PoolMode::Rename(_clip, ref mut old_name)) = pool.mode_mut().clone() {
|
||
let old_name = old_name.clone(); *pool.mode_mut() = None; return Ok(Some(Self::Set { value: old_name }))
|
||
}
|
||
Ok(None)
|
||
},
|
||
Set { value: Arc<str> } => {
|
||
if let Some(PoolMode::Rename(clip, ref mut _old_name)) = pool.mode_mut().clone() {
|
||
pool.clips()[clip].write().unwrap().name = value.clone();
|
||
}
|
||
Ok(None)
|
||
},
|
||
});
|
||
|
||
def_command!(CropCommand: |pool: Pool| {
|
||
Begin => unreachable!(),
|
||
Cancel => { if let Some(PoolMode::Length(..)) = pool.mode_mut().clone() { *pool.mode_mut() = None; } Ok(None) },
|
||
Set { length: usize } => {
|
||
if let Some(PoolMode::Length(clip, ref mut length, ref mut _focus))
|
||
= pool.mode_mut().clone()
|
||
{
|
||
let old_length;
|
||
{
|
||
let clip = pool.clips()[clip].clone();//.write().unwrap();
|
||
old_length = Some(clip.read().unwrap().length);
|
||
clip.write().unwrap().length = *length;
|
||
}
|
||
*pool.mode_mut() = None;
|
||
return Ok(old_length.map(|length|Self::Set { length }))
|
||
}
|
||
Ok(None)
|
||
},
|
||
Next => {
|
||
if let Some(PoolMode::Length(_clip, ref mut _length, ref mut focus)) = pool.mode_mut().clone() { focus.next() }; Ok(None)
|
||
},
|
||
Prev => {
|
||
if let Some(PoolMode::Length(_clip, ref mut _length, ref mut focus)) = pool.mode_mut().clone() { focus.prev() }; Ok(None)
|
||
},
|
||
Inc => {
|
||
if let Some(PoolMode::Length(_clip, ref mut length, ref mut focus)) = pool.mode_mut().clone() {
|
||
match focus {
|
||
ClipLengthFocus::Bar => { *length += 4 * PPQ },
|
||
ClipLengthFocus::Beat => { *length += PPQ },
|
||
ClipLengthFocus::Tick => { *length += 1 },
|
||
}
|
||
}
|
||
Ok(None)
|
||
},
|
||
Dec => {
|
||
if let Some(PoolMode::Length(_clip, ref mut length, ref mut focus)) = pool.mode_mut().clone() {
|
||
match focus {
|
||
ClipLengthFocus::Bar => { *length = length.saturating_sub(4 * PPQ) },
|
||
ClipLengthFocus::Beat => { *length = length.saturating_sub(PPQ) },
|
||
ClipLengthFocus::Tick => { *length = length.saturating_sub(1) },
|
||
}
|
||
}
|
||
Ok(None)
|
||
}
|
||
});
|
||
|
||
def_command!(MidiInputCommand: |port: MidiInput| {
|
||
Close => todo!(),
|
||
Connect { midi_out: Arc<str> } => todo!(),
|
||
});
|
||
|
||
def_command!(MidiOutputCommand: |port: MidiOutput| {
|
||
Close => todo!(),
|
||
Connect { midi_in: Arc<str> } => todo!(),
|
||
});
|
||
|
||
def_command!(AudioInputCommand: |port: AudioInput| {
|
||
Close => todo!(),
|
||
Connect { audio_out: Arc<str> } => todo!(),
|
||
});
|
||
|
||
def_command!(AudioOutputCommand: |port: AudioOutput| {
|
||
Close => todo!(),
|
||
Connect { audio_in: Arc<str> } => todo!(),
|
||
});
|
||
|
||
def_command!(SamplerCommand: |sampler: Sampler| {
|
||
RecordToggle { slot: usize } => {
|
||
let slot = *slot;
|
||
let recording = sampler.recording.as_ref().map(|x|x.0);
|
||
let _ = Self::RecordFinish.execute(sampler)?;
|
||
// autoslice: continue recording at next slot
|
||
if recording != Some(slot) {
|
||
Self::RecordBegin { slot }.execute(sampler)
|
||
} else {
|
||
Ok(None)
|
||
}
|
||
},
|
||
RecordBegin { slot: usize } => {
|
||
let slot = *slot;
|
||
sampler.recording = Some((
|
||
slot,
|
||
Some(Arc::new(RwLock::new(Sample::new(
|
||
"Sample", 0, 0, vec![vec![];sampler.audio_ins.len()]
|
||
))))
|
||
));
|
||
Ok(None)
|
||
},
|
||
RecordFinish => {
|
||
let _prev_sample = sampler.recording.as_mut().map(|(index, sample)|{
|
||
std::mem::swap(sample, &mut sampler.samples.0[*index]);
|
||
sample
|
||
}); // TODO: undo
|
||
Ok(None)
|
||
},
|
||
RecordCancel => {
|
||
sampler.recording = None;
|
||
Ok(None)
|
||
},
|
||
PlaySample { slot: usize } => {
|
||
let slot = *slot;
|
||
if let Some(ref sample) = sampler.samples.0[slot] {
|
||
sampler.voices.write().unwrap().push(Sample::play(sample, 0, &u7::from(128)));
|
||
}
|
||
Ok(None)
|
||
},
|
||
StopSample { slot: usize } => {
|
||
let slot = *slot;
|
||
todo!();
|
||
Ok(None)
|
||
},
|
||
});
|
||
|
||
def_command!(FileBrowserCommand: |sampler: Sampler|{
|
||
//("begin" [] Some(Self::Begin))
|
||
//("cancel" [] Some(Self::Cancel))
|
||
//("confirm" [] Some(Self::Confirm))
|
||
//("select" [i: usize] Some(Self::Select(i.expect("no index"))))
|
||
//("chdir" [p: PathBuf] Some(Self::Chdir(p.expect("no path"))))
|
||
//("filter" [f: Arc<str>] Some(Self::Filter(f.expect("no filter")))))
|
||
});
|
||
|
||
def_command!(SceneCommand: |scene: Scene| {
|
||
SetSize { size: usize } => { todo!() },
|
||
SetZoom { size: usize } => { todo!() },
|
||
SetName { name: Arc<str> } =>
|
||
swap_value(&mut scene.name, name, |name|Self::SetName{name}),
|
||
SetColor { color: ItemTheme } =>
|
||
swap_value(&mut scene.color, color, |color|Self::SetColor{color}),
|
||
});
|
||
|
||
def_command!(TrackCommand: |track: Track| {
|
||
Stop => { track.sequencer.enqueue_next(None); Ok(None) },
|
||
SetMute { mute: Option<bool> } => todo!(),
|
||
SetSolo { solo: Option<bool> } => todo!(),
|
||
SetSize { size: usize } => todo!(),
|
||
SetZoom { zoom: usize } => todo!(),
|
||
SetName { name: Arc<str> } =>
|
||
swap_value(&mut track.name, name, |name|Self::SetName { name }),
|
||
SetColor { color: ItemTheme } =>
|
||
swap_value(&mut track.color, color, |color|Self::SetColor { color }),
|
||
SetRec { rec: Option<bool> } =>
|
||
toggle_bool(&mut track.sequencer.recording, rec, |rec|Self::SetRec { rec }),
|
||
SetMon { mon: Option<bool> } =>
|
||
toggle_bool(&mut track.sequencer.monitoring, mon, |mon|Self::SetMon { mon }),
|
||
});
|
||
|
||
pub(crate) use self::size::*;
|
||
mod size {
|
||
use crate::*;
|
||
/// Define a type alias for iterators of sized items (columns).
|
||
macro_rules! def_sizes_iter {
|
||
($Type:ident => $($Item:ty),+) => {
|
||
pub trait $Type<'a> =
|
||
Iterator<Item=(usize, $(&'a $Item,)+ usize, usize)> + Send + Sync + 'a;
|
||
}
|
||
}
|
||
def_sizes_iter!(InputsSizes => MidiInput);
|
||
def_sizes_iter!(OutputsSizes => MidiOutput);
|
||
def_sizes_iter!(PortsSizes => Arc<str>, [Connect]);
|
||
def_sizes_iter!(ScenesSizes => Scene);
|
||
def_sizes_iter!(TracksSizes => Track);
|
||
}
|
||
|
||
pub(crate) use self::view::*;
|
||
mod view {
|
||
use crate::*;
|
||
|
||
pub fn view_logo () -> impl Content<TuiOut> {
|
||
Fixed::XY(32, 7, Tui::bold(true, Tui::fg(Rgb(240,200,180), col!{
|
||
Fixed::Y(1, ""),
|
||
Fixed::Y(1, ""),
|
||
Fixed::Y(1, "~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~"),
|
||
Fixed::Y(1, Bsp::e("~~~~ ║ ~ ╟─╌ ~╟─< ~~ ", Bsp::e(Tui::fg(Rgb(230,100,40), "v0.3.0"), " ~~"))),
|
||
Fixed::Y(1, "~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~"),
|
||
})))
|
||
}
|
||
|
||
pub fn view_transport (
|
||
play: bool,
|
||
bpm: Arc<RwLock<String>>,
|
||
beat: Arc<RwLock<String>>,
|
||
time: Arc<RwLock<String>>,
|
||
) -> impl Content<TuiOut> {
|
||
let theme = ItemTheme::G[96];
|
||
Tui::bg(Black, row!(Bsp::a(
|
||
Fill::XY(Align::w(button_play_pause(play))),
|
||
Fill::XY(Align::e(row!(
|
||
FieldH(theme, "BPM", bpm),
|
||
FieldH(theme, "Beat", beat),
|
||
FieldH(theme, "Time", time),
|
||
)))
|
||
)))
|
||
}
|
||
|
||
pub fn view_status (
|
||
sel: Option<Arc<str>>,
|
||
sr: Arc<RwLock<String>>,
|
||
buf: Arc<RwLock<String>>,
|
||
lat: Arc<RwLock<String>>,
|
||
) -> impl Content<TuiOut> {
|
||
let theme = ItemTheme::G[96];
|
||
Tui::bg(Black, row!(Bsp::a(
|
||
Fill::XY(Align::w(sel.map(|sel|FieldH(theme, "Selected", sel)))),
|
||
Fill::XY(Align::e(row!(
|
||
FieldH(theme, "SR", sr),
|
||
FieldH(theme, "Buf", buf),
|
||
FieldH(theme, "Lat", lat),
|
||
)))
|
||
)))
|
||
}
|
||
|
||
pub fn button_play_pause (playing: bool) -> impl Content<TuiOut> {
|
||
let compact = true;//self.is_editing();
|
||
Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) },
|
||
Either::new(compact,
|
||
Thunk::new(move|to: &mut TuiOut|to.place(&Fixed::X(9, Either::new(playing,
|
||
Tui::fg(Rgb(0, 255, 0), " PLAYING "),
|
||
Tui::fg(Rgb(255, 128, 0), " STOPPED ")))
|
||
)),
|
||
Thunk::new(move|to: &mut TuiOut|to.place(&Fixed::X(5, Either::new(playing,
|
||
Tui::fg(Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)),
|
||
Tui::fg(Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",))))
|
||
))
|
||
)
|
||
)
|
||
}
|
||
|
||
#[cfg(feature = "track")] pub fn view_track_row_section (
|
||
_theme: ItemTheme,
|
||
button: impl Content<TuiOut>,
|
||
button_add: impl Content<TuiOut>,
|
||
content: impl Content<TuiOut>,
|
||
) -> impl Content<TuiOut> {
|
||
Bsp::w(Fill::Y(Fixed::X(4, Align::nw(button_add))),
|
||
Bsp::e(Fixed::X(20, Fill::Y(Align::nw(button))), Fill::XY(Align::c(content))))
|
||
}
|
||
|
||
pub fn wrap (bg: Color, fg: Color, content: impl Content<TuiOut>) -> impl Content<TuiOut> {
|
||
let left = Tui::fg_bg(bg, Reset, Fixed::X(1, Repeat::Y("▐")));
|
||
let right = Tui::fg_bg(bg, Reset, Fixed::X(1, Repeat::Y("▌")));
|
||
Bsp::e(left, Bsp::w(right, Tui::fg_bg(fg, bg, content)))
|
||
}
|
||
|
||
pub fn view_meter <'a> (label: &'a str, value: f32) -> impl Content<TuiOut> + 'a {
|
||
col!(
|
||
FieldH(ItemTheme::G[128], label, format!("{:>+9.3}", value)),
|
||
Fixed::XY(if value >= 0.0 { 13 }
|
||
else if value >= -1.0 { 12 }
|
||
else if value >= -2.0 { 11 }
|
||
else if value >= -3.0 { 10 }
|
||
else if value >= -4.0 { 9 }
|
||
else if value >= -6.0 { 8 }
|
||
else if value >= -9.0 { 7 }
|
||
else if value >= -12.0 { 6 }
|
||
else if value >= -15.0 { 5 }
|
||
else if value >= -20.0 { 4 }
|
||
else if value >= -25.0 { 3 }
|
||
else if value >= -30.0 { 2 }
|
||
else if value >= -40.0 { 1 }
|
||
else { 0 }, 1, Tui::bg(if value >= 0.0 { Red }
|
||
else if value >= -3.0 { Yellow }
|
||
else { Green }, ())))
|
||
}
|
||
|
||
pub fn view_meters (values: &[f32;2]) -> impl Content<TuiOut> + use<'_> {
|
||
let left = format!("L/{:>+9.3}", values[0]);
|
||
let right = format!("R/{:>+9.3}", values[1]);
|
||
Bsp::s(left, right)
|
||
}
|
||
|
||
pub fn draw_info (sample: Option<&Arc<RwLock<Sample>>>) -> impl Content<TuiOut> + use<'_> {
|
||
When::new(sample.is_some(), Thunk::new(move|to: &mut TuiOut|{
|
||
let sample = sample.unwrap().read().unwrap();
|
||
let theme = sample.color;
|
||
to.place(&row!(
|
||
FieldH(theme, "Name", format!("{:<10}", sample.name.clone())),
|
||
FieldH(theme, "Length", format!("{:<8}", sample.channels[0].len())),
|
||
FieldH(theme, "Start", format!("{:<8}", sample.start)),
|
||
FieldH(theme, "End", format!("{:<8}", sample.end)),
|
||
FieldH(theme, "Trans", "0"),
|
||
FieldH(theme, "Gain", format!("{}", sample.gain)),
|
||
))
|
||
}))
|
||
}
|
||
|
||
pub fn draw_info_v (sample: Option<&Arc<RwLock<Sample>>>) -> impl Content<TuiOut> + use<'_> {
|
||
Either::new(sample.is_some(), Thunk::new(move|to: &mut TuiOut|{
|
||
let sample = sample.unwrap().read().unwrap();
|
||
let theme = sample.color;
|
||
to.place(&Fixed::X(20, col!(
|
||
Fill::X(Align::w(FieldH(theme, "Name ", format!("{:<10}", sample.name.clone())))),
|
||
Fill::X(Align::w(FieldH(theme, "Length", format!("{:<8}", sample.channels[0].len())))),
|
||
Fill::X(Align::w(FieldH(theme, "Start ", format!("{:<8}", sample.start)))),
|
||
Fill::X(Align::w(FieldH(theme, "End ", format!("{:<8}", sample.end)))),
|
||
Fill::X(Align::w(FieldH(theme, "Trans ", "0"))),
|
||
Fill::X(Align::w(FieldH(theme, "Gain ", format!("{}", sample.gain)))),
|
||
)))
|
||
}), Thunk::new(|to: &mut TuiOut|to.place(&Tui::fg(Red, col!(
|
||
Tui::bold(true, "× No sample."),
|
||
"[r] record",
|
||
"[Shift-F9] import",
|
||
)))))
|
||
}
|
||
|
||
pub fn draw_status (sample: Option<&Arc<RwLock<Sample>>>) -> impl Content<TuiOut> {
|
||
Tui::bold(true, Tui::fg(Tui::g(224), sample
|
||
.map(|sample|{
|
||
let sample = sample.read().unwrap();
|
||
format!("Sample {}-{}", sample.start, sample.end)
|
||
})
|
||
.unwrap_or_else(||"No sample".to_string())))
|
||
}
|
||
|
||
pub fn view_track_header (theme: ItemTheme, content: impl Content<TuiOut>) -> impl Content<TuiOut> {
|
||
Fixed::X(12, Tui::bg(theme.darker.rgb, Fill::X(Align::e(content))))
|
||
}
|
||
|
||
pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, ports: &'a [T])
|
||
-> impl Content<TuiOut> + use<'a, T>
|
||
{
|
||
let ins = ports.len() as u16;
|
||
let frame = Outer(true, Style::default().fg(Tui::g(96)));
|
||
let iter = move||ports.iter();
|
||
let names = Map::south(1, iter, move|port, index|Fill::Y(Align::w(format!(" {index} {}", port.port_name()))));
|
||
let field = FieldV(theme, title, names);
|
||
Fixed::XY(20, 1 + ins, frame.enclose(Fixed::XY(20, 1 + ins, field)))
|
||
}
|
||
|
||
pub fn per_track_top <'a, T: Content<TuiOut> + 'a, U: TracksSizes<'a>> (
|
||
tracks: impl Fn() -> U + Send + Sync + 'a,
|
||
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
|
||
) -> impl Content<TuiOut> + 'a {
|
||
Align::x(Tui::bg(Reset, Map::new(tracks,
|
||
move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{
|
||
let width = (x2 - x1) as u16;
|
||
map_east(x1 as u16, width, Fixed::X(width, Tui::fg_bg(
|
||
track.color.lightest.rgb,
|
||
track.color.base.rgb,
|
||
callback(index, track))))})))
|
||
}
|
||
|
||
pub fn per_track <'a, T: Content<TuiOut> + 'a, U: TracksSizes<'a>> (
|
||
tracks: impl Fn() -> U + Send + Sync + 'a,
|
||
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
|
||
) -> impl Content<TuiOut> + 'a {
|
||
per_track_top(tracks, move|index, track|Fill::Y(Align::y(callback(index, track))))
|
||
}
|
||
|
||
pub fn io_ports <'a, T: PortsSizes<'a>> (
|
||
fg: Color, bg: Color, iter: impl Fn()->T + Send + Sync + 'a
|
||
) -> impl Content<TuiOut> + 'a {
|
||
Map::new(iter, move|(
|
||
_index, name, connections, y, y2
|
||
): (usize, &'a Arc<str>, &'a [Connect], usize, usize), _|
|
||
map_south(y as u16, (y2-y) as u16, Bsp::s(
|
||
Fill::Y(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(Bsp::e(&" ", name))))),
|
||
Map::new(||connections.iter(), move|connect: &'a Connect, index|map_south(index as u16, 1,
|
||
Fill::Y(Align::w(Tui::bold(false, Tui::fg_bg(fg, bg,
|
||
&connect.info)))))))))
|
||
}
|
||
|
||
pub fn draw_header (state: &Lv2, to: &mut TuiOut, x: u16, y: u16, w: u16) {
|
||
let style = Style::default().gray();
|
||
let label1 = format!(" {}", state.name);
|
||
to.blit(&label1, x + 1, y, Some(style.white().bold()));
|
||
if let Some(ref path) = state.path {
|
||
let label2 = format!("{}…", &path[..((w as usize - 10).min(path.len()))]);
|
||
to.blit(&label2, x + 2 + label1.len() as u16, y, Some(style.not_dim()));
|
||
}
|
||
//Ok(Rect { x, y, width: w, height: 1 })
|
||
}
|
||
|
||
pub fn draw_sample (
|
||
to: &mut TuiOut, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool
|
||
) -> Usually<usize> {
|
||
let style = if focus { Style::default().green() } else { Style::default() };
|
||
if focus {
|
||
to.blit(&"🬴", x+1, y, Some(style.bold()));
|
||
}
|
||
let label1 = format!("{:3} {:12}",
|
||
note.map(|n|n.to_string()).unwrap_or(String::default()),
|
||
sample.name);
|
||
let label2 = format!("{:>6} {:>6} +0.0",
|
||
sample.start,
|
||
sample.end);
|
||
to.blit(&label1, x+2, y, Some(style.bold()));
|
||
to.blit(&label2, x+3+label1.len()as u16, y, Some(style));
|
||
Ok(label1.len() + label2.len() + 4)
|
||
}
|
||
}
|