mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-02-21 08:19:03 +01:00
Some checks are pending
/ build (push) Waiting to run
might join app + device + engine again?
820 lines
29 KiB
Rust
820 lines
29 KiB
Rust
#![feature(trait_alias)]
|
||
|
||
pub use tek_engine;
|
||
pub(crate) use ConnectName::*;
|
||
pub(crate) use ConnectScope::*;
|
||
pub(crate) use ConnectStatus::*;
|
||
|
||
mod device_struct; pub use self::device_struct::*;
|
||
mod device_trait; pub use self::device_trait::*;
|
||
mod device_type; pub use self::device_type::*;
|
||
mod device_impl;
|
||
|
||
pub(crate) use ::{
|
||
tek_engine::*,
|
||
tek_engine::tengri::{
|
||
Usually, Perhaps, Has, MaybeHas, has, maybe_has, from,
|
||
input::*,
|
||
output::*,
|
||
tui::*,
|
||
tui::ratatui,
|
||
tui::ratatui::widgets::{Widget, canvas::{Canvas, Line}},
|
||
tui::ratatui::prelude::{Rect, Style, Stylize, Buffer, Color::{self, *}},
|
||
},
|
||
std::{
|
||
cmp::Ord,
|
||
ffi::OsString,
|
||
fmt::{Debug, Formatter},
|
||
fs::File,
|
||
path::PathBuf,
|
||
sync::{Arc, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}},
|
||
}
|
||
};
|
||
|
||
#[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")] use std::thread::{spawn, JoinHandle};
|
||
|
||
#[cfg(feature = "lv2_gui")] use ::winit::{
|
||
application::ApplicationHandler,
|
||
event::WindowEvent,
|
||
event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
|
||
window::{Window, WindowId},
|
||
platform::x11::EventLoopBuilderExtX11
|
||
};
|
||
|
||
/// 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;
|
||
}
|
||
}
|
||
|
||
|
||
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(" ▗▄▖ ", " ▝▀▘ ",))))
|
||
))
|
||
)
|
||
)
|
||
}
|
||
|
||
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
|
||
}
|
||
});
|
||
|
||
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),
|
||
});
|
||
//take!(DeviceCommand|state: Arrangement, iter|state.selected_device().as_ref()
|
||
//.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten()));
|
||
|
||
#[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)))
|
||
}
|
||
|
||
#[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!(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
|
||
});
|
||
|
||
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))
|
||
}
|
||
|
||
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 })
|
||
}
|
||
|
||
|
||
#[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()
|
||
}
|
||
|
||
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)
|
||
}
|
||
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_sizes_iter!(InputsSizes => MidiInput);
|
||
def_sizes_iter!(OutputsSizes => MidiOutput);
|
||
def_sizes_iter!(PortsSizes => Arc<str>, [Connect]);
|
||
def_sizes_iter!(ScenesSizes => Scene);
|
||
|
||
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)),
|
||
))
|
||
}))
|
||
}
|
||
|
||
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",
|
||
)))))
|
||
}
|
||
|
||
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())))
|
||
}
|
||
|
||
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)
|
||
}
|
||
|
||
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))
|
||
}
|
||
|
||
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.mapped[*index]);
|
||
sample
|
||
}); // TODO: undo
|
||
Ok(None)
|
||
},
|
||
RecordCancel => {
|
||
sampler.recording = None;
|
||
Ok(None)
|
||
},
|
||
PlaySample { slot: usize } => {
|
||
let slot = *slot;
|
||
if let Some(ref sample) = sampler.mapped[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_sizes_iter!(TracksSizes => Track);
|
||
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) fn track_width (_index: usize, track: &Track) -> u16 {
|
||
track.width as u16
|
||
}
|
||
|
||
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))))
|
||
}
|
||
|
||
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(crate) 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(crate) 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(crate) 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)))))))))
|
||
}
|
||
|
||
#[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()?
|
||
})
|
||
}
|