wip: remudolarize 4

This commit is contained in:
🪞👃🪞 2025-01-08 19:56:31 +01:00
parent 0d9a4d4830
commit e8430c373f
46 changed files with 175 additions and 63 deletions

View file

@ -1,135 +0,0 @@
use crate::*;
mod arranger_command; pub(crate) use self::arranger_command::*;
mod arranger_scene; pub(crate) use self::arranger_scene::*;
mod arranger_select; pub(crate) use self::arranger_select::*;
mod arranger_track; pub(crate) use self::arranger_track::*;
mod arranger_tui; pub(crate) use self::arranger_tui::*;
mod arranger_mode; pub(crate) use self::arranger_mode::*;
mod arranger_h;
/// Root view for standalone `tek_arranger`
pub struct Arranger {
jack: Arc<RwLock<JackConnection>>,
pub clock: Clock,
pub pool: PoolModel,
pub tracks: Vec<ArrangerTrack>,
pub scenes: Vec<ArrangerScene>,
pub splits: [u16;2],
pub selected: ArrangerSelection,
pub mode: ArrangerMode,
pub color: ItemPalette,
pub size: Measure<TuiOut>,
pub note_buf: Vec<u8>,
pub midi_buf: Vec<Vec<Vec<u8>>>,
pub editor: MidiEditor,
pub perf: PerfModel,
pub compact: bool,
}
audio!(|self: Arranger, client, scope|{
// Start profiling cycle
let t0 = self.perf.get_t0();
// Update transport clock
//if Control::Quit == ClockAudio(self).process(client, scope) {
//return Control::Quit
//}
//// Update MIDI sequencers
//let tracks = &mut self.tracks;
//let note_buf = &mut self.note_buf;
//let midi_buf = &mut self.midi_buf;
//if Control::Quit == TracksAudio(tracks, note_buf, midi_buf).process(client, scope) {
//return Control::Quit
//}
// FIXME: one of these per playing track
//self.now.set(0.);
//if let ArrangerSelection::Clip(t, s) = self.selected {
//let phrase = self.scenes.get(s).map(|scene|scene.clips.get(t));
//if let Some(Some(Some(phrase))) = phrase {
//if let Some(track) = self.tracks().get(t) {
//if let Some((ref started_at, Some(ref playing))) = track.player.play_phrase {
//let phrase = phrase.read().unwrap();
//if *playing.read().unwrap() == *phrase {
//let pulse = self.current().pulse.get();
//let start = started_at.pulse.get();
//let now = (pulse - start) % phrase.length as f64;
//self.now.set(now);
//}
//}
//}
//}
//}
// End profiling cycle
self.perf.update(t0, scope);
return Control::Continue
});
has_clock!(|self: Arranger|&self.clock);
has_phrases!(|self: Arranger|self.pool.phrases);
has_editor!(|self: Arranger|self.editor);
handle!(TuiIn: |self: Arranger, input|ArrangerCommand::execute_with_state(self, input.event()));
impl Arranger {
pub fn new (jack: &Arc<RwLock<JackConnection>>) -> Self {
let clock = Clock::from(jack);
let phrase = Arc::new(RwLock::new(MidiClip::new(
"Clip", true, 4 * clock.timebase.ppq.get() as usize,
None, Some(ItemColor::random().into())
)));
Self {
clock,
pool: (&phrase).into(),
editor: (&phrase).into(),
selected: ArrangerSelection::Clip(0, 0),
scenes: vec![],
tracks: vec![],
color: ItemPalette::random(),
mode: ArrangerMode::V(1),
size: Measure::new(),
splits: [12, 20],
midi_buf: vec![vec![];65536],
note_buf: vec![],
perf: PerfModel::default(),
jack: jack.clone(),
compact: true,
}
}
pub fn selected (&self) -> ArrangerSelection {
self.selected
}
pub fn selected_mut (&mut self) -> &mut ArrangerSelection {
&mut self.selected
}
pub fn activate (&mut self) -> Usually<()> {
if let ArrangerSelection::Scene(s) = self.selected {
for (t, track) in self.tracks.iter_mut().enumerate() {
let phrase = self.scenes[s].clips[t].clone();
if track.player.play_phrase.is_some() || phrase.is_some() {
track.player.enqueue_next(phrase.as_ref());
}
}
if self.clock().is_stopped() {
self.clock().play_from(Some(0))?;
}
} else if let ArrangerSelection::Clip(t, s) = self.selected {
let phrase = self.scenes[s].clips[t].clone();
self.tracks[t].player.enqueue_next(phrase.as_ref());
};
Ok(())
}
pub fn selected_phrase (&self) -> Option<Arc<RwLock<MidiClip>>> {
self.selected_scene()?.clips.get(self.selected.track()?)?.clone()
}
pub fn toggle_loop (&mut self) {
if let Some(phrase) = self.selected_phrase() {
phrase.write().unwrap().toggle_loop()
}
}
pub fn randomize_color (&mut self) {
match self.selected {
ArrangerSelection::Mix => { self.color = ItemPalette::random() },
ArrangerSelection::Track(t) => { self.tracks[t].color = ItemPalette::random() },
ArrangerSelection::Scene(s) => { self.scenes[s].color = ItemPalette::random() },
ArrangerSelection::Clip(t, s) => if let Some(phrase) = &self.scenes[s].clips[t] {
phrase.write().unwrap().color = ItemPalette::random();
}
}
}
}

View file

@ -1,247 +0,0 @@
use crate::*;
use ClockCommand::{Play, Pause};
use ArrangerCommand as Cmd;
#[derive(Clone, Debug)] pub enum ArrangerCommand {
History(isize),
Color(ItemPalette),
Clock(ClockCommand),
Scene(ArrangerSceneCommand),
Track(ArrangerTrackCommand),
Clip(ArrangerClipCommand),
Select(ArrangerSelection),
Zoom(usize),
Phrases(PoolCommand),
Editor(MidiEditCommand),
StopAll,
Clear,
}
#[derive(Clone, Debug)]
pub enum ArrangerTrackCommand {
Add,
Delete(usize),
Stop(usize),
Swap(usize, usize),
SetSize(usize),
SetZoom(usize),
SetColor(usize, ItemPalette),
}
#[derive(Clone, Debug)]
pub enum ArrangerSceneCommand {
Enqueue(usize),
Add,
Delete(usize),
Swap(usize, usize),
SetSize(usize),
SetZoom(usize),
SetColor(usize, ItemPalette),
}
#[derive(Clone, Debug)]
pub enum ArrangerClipCommand {
Get(usize, usize),
Put(usize, usize, Option<Arc<RwLock<MidiClip>>>),
Enqueue(usize, usize),
Edit(Option<Arc<RwLock<MidiClip>>>),
SetLoop(usize, usize, bool),
SetColor(usize, usize, ItemPalette),
}
//handle!(TuiIn: |self: Arranger, input|ArrangerCommand::execute_with_state(self, input.event()));
//input_to_command!(ArrangerCommand: |state: Arranger, input: Event|{KEYS_ARRANGER.handle(state, input)?});
keymap!(KEYS_ARRANGER = |state: Arranger, input: Event| ArrangerCommand {
key(Char('u')) => Cmd::History(-1),
key(Char('U')) => Cmd::History(1),
// TODO: k: toggle on-screen keyboard
ctrl(key(Char('k'))) => { todo!("keyboard") },
// Transport: Play/pause
key(Char(' ')) => Cmd::Clock(if state.clock().is_stopped() { Play(None) } else { Pause(None) }),
// Transport: Play from start or rewind to start
shift(key(Char(' '))) => Cmd::Clock(if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }),
key(Char('e')) => Cmd::Editor(MidiEditCommand::Show(Some(state.pool.phrase().clone()))),
ctrl(key(Char('a'))) => Cmd::Scene(ArrangerSceneCommand::Add),
ctrl(key(Char('t'))) => Cmd::Track(ArrangerTrackCommand::Add),
// Tab: Toggle visibility of phrase pool column
key(Tab) => Cmd::Phrases(PoolCommand::Show(!state.pool.visible)),
}, {
use ArrangerSelection as Selected;
use ArrangerSceneCommand as Scene;
use ArrangerTrackCommand as Track;
use ArrangerClipCommand as Clip;
let t_len = state.tracks.len();
let s_len = state.scenes.len();
match state.selected() {
Selected::Clip(t, s) => match input {
kpat!(Char('g')) => Some(Cmd::Phrases(PoolCommand::Select(0))),
kpat!(Char('q')) => Some(Cmd::Clip(Clip::Enqueue(t, s))),
kpat!(Char(',')) => Some(Cmd::Clip(Clip::Put(t, s, None))),
kpat!(Char('.')) => Some(Cmd::Clip(Clip::Put(t, s, None))),
kpat!(Char('<')) => Some(Cmd::Clip(Clip::Put(t, s, None))),
kpat!(Char('>')) => Some(Cmd::Clip(Clip::Put(t, s, None))),
kpat!(Char('p')) => Some(Cmd::Clip(Clip::Put(t, s, Some(state.pool.phrase().clone())))),
kpat!(Char('l')) => Some(Cmd::Clip(ArrangerClipCommand::SetLoop(t, s, false))),
kpat!(Delete) => Some(Cmd::Clip(Clip::Put(t, s, None))),
kpat!(Up) => Some(Cmd::Select(
if s > 0 { Selected::Clip(t, s - 1) } else { Selected::Track(t) })),
kpat!(Down) => Some(Cmd::Select(
Selected::Clip(t, (s + 1).min(s_len.saturating_sub(1))))),
kpat!(Left) => Some(Cmd::Select(
if t > 0 { Selected::Clip(t - 1, s) } else { Selected::Scene(s) })),
kpat!(Right) => Some(Cmd::Select(
Selected::Clip((t + 1).min(t_len.saturating_sub(1)), s))),
_ => None
},
Selected::Scene(s) => match input {
kpat!(Char(',')) => Some(Cmd::Scene(Scene::Swap(s, s - 1))),
kpat!(Char('.')) => Some(Cmd::Scene(Scene::Swap(s, s + 1))),
kpat!(Char('<')) => Some(Cmd::Scene(Scene::Swap(s, s - 1))),
kpat!(Char('>')) => Some(Cmd::Scene(Scene::Swap(s, s + 1))),
kpat!(Char('q')) => Some(Cmd::Scene(Scene::Enqueue(s))),
kpat!(Delete) => Some(Cmd::Scene(Scene::Delete(s))),
kpat!(Char('c')) => Some(Cmd::Scene(Scene::SetColor(s, ItemPalette::random()))),
kpat!(Up) => Some(
Cmd::Select(if s > 0 { Selected::Scene(s - 1) } else { Selected::Mix })),
kpat!(Down) => Some(
Cmd::Select(Selected::Scene((s + 1).min(s_len.saturating_sub(1))))),
kpat!(Left) =>
return None,
kpat!(Right) => Some(
Cmd::Select(Selected::Clip(0, s))),
_ => None
},
Selected::Track(t) => match input {
kpat!(Char(',')) => Some(Cmd::Track(Track::Swap(t, t - 1))),
kpat!(Char('.')) => Some(Cmd::Track(Track::Swap(t, t + 1))),
kpat!(Char('<')) => Some(Cmd::Track(Track::Swap(t, t - 1))),
kpat!(Char('>')) => Some(Cmd::Track(Track::Swap(t, t + 1))),
kpat!(Delete) => Some(Cmd::Track(Track::Delete(t))),
kpat!(Char('c')) => Some(Cmd::Track(Track::SetColor(t, ItemPalette::random()))),
kpat!(Up) =>
return None,
kpat!(Down) => Some(
Cmd::Select(Selected::Clip(t, 0))),
kpat!(Left) => Some(
Cmd::Select(if t > 0 { Selected::Track(t - 1) } else { Selected::Mix })),
kpat!(Right) => Some(
Cmd::Select(Selected::Track((t + 1).min(t_len.saturating_sub(1))))),
_ => None
},
Selected::Mix => match input {
kpat!(Delete) => Some(Cmd::Clear),
kpat!(Char('0')) => Some(Cmd::StopAll),
kpat!(Char('c')) => Some(Cmd::Color(ItemPalette::random())),
kpat!(Up) =>
return None,
kpat!(Down) => Some(
Cmd::Select(Selected::Scene(0))),
kpat!(Left) =>
return None,
kpat!(Right) => Some(
Cmd::Select(Selected::Track(0))),
_ => None
},
}
}.or_else(||if let Some(command) = MidiEditCommand::input_to_command(&state.editor, input) {
Some(Cmd::Editor(command))
} else if let Some(command) = PoolCommand::input_to_command(&state.pool, input) {
Some(Cmd::Phrases(command))
} else {
None
})?);
command!(|self: ArrangerCommand, state: Arranger|match self {
Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?,
Self::Clip(cmd) => cmd.delegate(state, Self::Clip)?,
Self::Scene(cmd) => cmd.delegate(state, Self::Scene)?,
Self::Track(cmd) => cmd.delegate(state, Self::Track)?,
Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?,
Self::Zoom(_) => { todo!(); },
Self::Select(selected) => {
*state.selected_mut() = selected;
None
},
Self::Color(palette) => {
let old = state.color;
state.color = palette;
Some(Self::Color(old))
},
Self::Phrases(cmd) => {
match cmd {
// autoselect: automatically load selected phrase in editor
PoolCommand::Select(_) => {
let undo = cmd.delegate(&mut state.pool, Self::Phrases)?;
state.editor.set_phrase(Some(state.pool.phrase()));
undo
},
// reload phrase in editor to update color
PoolCommand::Phrase(MidiPoolCommand::SetColor(index, _)) => {
let undo = cmd.delegate(&mut state.pool, Self::Phrases)?;
state.editor.set_phrase(Some(state.pool.phrase()));
undo
},
_ => cmd.delegate(&mut state.pool, Self::Phrases)?
}
},
Self::History(_) => { todo!() },
Self::StopAll => {
for track in 0..state.tracks.len() {
state.tracks[track].player.enqueue_next(None);
}
None
},
Self::Clear => { todo!() },
});
command!(|self: ArrangerTrackCommand, state: Arranger|match self {
Self::SetColor(index, color) => {
let old = state.tracks[index].color;
state.tracks[index].color = color;
Some(Self::SetColor(index, old))
},
Self::Stop(track) => {
state.tracks[track].player.enqueue_next(None);
None
},
_ => None
});
command!(|self: ArrangerSceneCommand, state: Arranger|match self {
Self::Delete(index) => {
state.scene_del(index);
None
},
Self::SetColor(index, color) => {
let old = state.scenes[index].color;
state.scenes[index].color = color;
Some(Self::SetColor(index, old))
},
Self::Enqueue(scene) => {
for track in 0..state.tracks.len() {
state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref());
}
None
},
_ => None
});
command!(|self: ArrangerClipCommand, state: Arranger|match self {
Self::Get(track, scene) => { todo!() },
Self::Put(track, scene, phrase) => {
let old = state.scenes[scene].clips[track].clone();
state.scenes[scene].clips[track] = phrase;
Some(Self::Put(track, scene, old))
},
Self::Enqueue(track, scene) => {
state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref());
None
},
_ => None
});

View file

@ -1 +0,0 @@
// TODO

View file

@ -1,40 +0,0 @@
(def keys
(u :undo 1)
(shift-u :redo 1)
(ctrl-k :todo "keyboard")
(space :play-toggle)
(shift-space :play-start-toggle)
(e :editor-show :pool-phrase)
(ctrl-a :scene-add)
(ctrl-t :track-add)
(tab :pool-toggle))
(def keys-clip
(q :clip-launch)
(c :clip-color)
(g :clip-get)
(p :clip-put)
(del :clip-del)
(, :clip-prev)
(. :clip-next)
(< :clip-swap-prev)
(> :clip-swap-next)
(l :clip-loop-toggle))
(def keys-scene
(q :scene-launch)
(c :scene-color)
(, :scene-prev)
(. :scene-next)
(< :scene-swap-prev)
(> :scene-swap-next)
(del :scene-delete))
(def keys-track
(q :track-launch)
(c :track-color)
(, :track-prev)
(. :track-next)
(< :track-swap-prev)
(> :track-swap-next)
(del :track-delete))

View file

@ -1,26 +0,0 @@
use crate::*;
/// Display mode of arranger
#[derive(Clone, PartialEq)]
pub enum ArrangerMode {
/// Tracks are columns
V(usize),
/// Tracks are rows
H,
}
impl<E: Output> Content<E> for ArrangerMode {}
/// Arranger display mode can be cycled
impl ArrangerMode {
/// Cycle arranger display mode
pub fn next (&mut self) {
*self = match self {
Self::H => Self::V(1),
Self::V(1) => Self::V(2),
Self::V(2) => Self::V(2),
Self::V(0) => Self::H,
Self::V(_) => Self::V(0),
}
}
}
fn any_size <E: Output> (_: E::Size) -> Perhaps<E::Size>{
Ok(Some([0.into(),0.into()].into()))
}

View file

@ -1,86 +0,0 @@
use crate::*;
impl Arranger {
pub fn scene_add (&mut self, name: Option<&str>, color: Option<ItemPalette>)
-> Usually<&mut ArrangerScene>
{
let scene = ArrangerScene {
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(ItemPalette::random),
};
self.scenes.push(scene);
let index = self.scenes.len() - 1;
Ok(&mut self.scenes[index])
}
pub fn scene_del (&mut self, index: usize) {
todo!("delete scene");
}
fn scene_default_name (&self) -> Arc<str> {
format!("Sc{:3>}", self.scenes.len() + 1).into()
}
pub fn selected_scene (&self) -> Option<&ArrangerScene> {
self.selected.scene().and_then(|s|self.scenes.get(s))
}
pub fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> {
self.selected.scene().and_then(|s|self.scenes.get_mut(s))
}
pub fn scenes_add (&mut self, n: usize) -> Usually<()> {
let scene_color_1 = ItemColor::random();
let scene_color_2 = ItemColor::random();
for i in 0..n {
let _scene = self.scene_add(None, Some(
scene_color_1.mix(scene_color_2, i as f32 / n as f32).into()
))?;
}
Ok(())
}
}
#[derive(Default, Debug, Clone)] pub struct ArrangerScene {
/// Name of scene
pub(crate) name: Arc<str>,
/// Clips in scene, one per track
pub(crate) clips: Vec<Option<Arc<RwLock<MidiClip>>>>,
/// Identifying color of scene
pub(crate) color: ItemPalette,
}
impl ArrangerScene {
pub fn name (&self) -> &Arc<str> {
&self.name
}
pub fn clips (&self) -> &Vec<Option<Arc<RwLock<MidiClip>>>> {
&self.clips
}
pub fn color (&self) -> ItemPalette {
self.color
}
pub fn longest_name (scenes: &[Self]) -> usize {
scenes.iter().map(|s|s.name.len()).fold(0, usize::max)
}
/// Returns the pulse length of the longest phrase 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 phrases in the scene are
/// currently playing on the given collection of tracks.
pub fn is_playing (&self, tracks: &[ArrangerTrack]) -> bool {
self.clips().iter().any(|clip|clip.is_some()) && self.clips().iter().enumerate()
.all(|(track_index, clip)|match clip {
Some(clip) => tracks
.get(track_index)
.map(|track|{
if let Some((_, Some(phrase))) = track.player().play_phrase() {
*phrase.read().unwrap() == *clip.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 }
}
}

View file

@ -1,56 +0,0 @@
use crate::*;
#[derive(PartialEq, Clone, Copy, Debug)]
/// Represents the current user selection in the arranger
pub enum ArrangerSelection {
/// The whole mix is selected
Mix,
/// A track is selected.
Track(usize),
/// A scene is selected.
Scene(usize),
/// A clip (track × scene) is selected.
Clip(usize, usize),
}
/// Focus identification methods
impl ArrangerSelection {
pub fn is_mix (&self) -> bool { matches!(self, Self::Mix) }
pub fn is_track (&self) -> bool { matches!(self, Self::Track(_)) }
pub fn is_scene (&self) -> bool { matches!(self, Self::Scene(_)) }
pub fn is_clip (&self) -> bool { matches!(self, Self::Clip(_, _)) }
pub fn description (
&self,
tracks: &[ArrangerTrack],
scenes: &[ArrangerScene],
) -> Arc<str> {
format!("Selected: {}", match self {
Self::Mix => "Everything".to_string(),
Self::Track(t) => tracks.get(*t).map(|track|format!("T{t}: {}", &track.name))
.unwrap_or_else(||"T??".into()),
Self::Scene(s) => scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name))
.unwrap_or_else(||"S??".into()),
Self::Clip(t, s) => match (tracks.get(*t), scenes.get(*s)) {
(Some(_), Some(scene)) => match scene.clip(*t) {
Some(clip) => format!("T{t} S{s} C{}", &clip.read().unwrap().name),
None => format!("T{t} S{s}: Empty")
},
_ => format!("T{t} S{s}: Empty"),
}
}).into()
}
pub fn track (&self) -> Option<usize> {
use ArrangerSelection::*;
match self {
Clip(t, _) => Some(*t),
Track(t) => Some(*t),
_ => None
}
}
pub fn scene (&self) -> Option<usize> {
use ArrangerSelection::*;
match self {
Clip(_, s) => Some(*s),
Scene(s) => Some(*s),
_ => None
}
}
}

View file

@ -1,160 +0,0 @@
use crate::*;
impl Arranger {
pub fn track_next_name (&self) -> Arc<str> {
format!("Tr{:02}", self.tracks.len() + 1).into()
}
pub fn track_add (&mut self, name: Option<&str>, color: Option<ItemPalette>)
-> Usually<&mut ArrangerTrack>
{
let name = name.map_or_else(||self.track_next_name(), |x|x.to_string().into());
let track = ArrangerTrack {
width: name.len() + 2,
color: color.unwrap_or_else(ItemPalette::random),
player: MidiPlayer::from(&self.clock),
name,
};
self.tracks.push(track);
let index = self.tracks.len() - 1;
Ok(&mut self.tracks[index])
}
pub fn track_del (&mut self, index: usize) {
self.tracks.remove(index);
for scene in self.scenes.iter_mut() {
scene.clips.remove(index);
}
}
pub fn tracks_add (
&mut self,
count: usize,
width: usize,
midi_from: &[impl AsRef<str>],
midi_to: &[impl AsRef<str>],
) -> Usually<()> {
let jack = self.jack.clone();
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 mut track = self.track_add(None, Some(color))?;
track.width = width;
let name = &format!("{}I", &track.name);
let port = jack.read().unwrap().client().register_port(&name, MidiIn::default())?;
track.player.midi_ins.push(port);
let name = &format!("{}O", &track.name);
let port = jack.read().unwrap().client().register_port(&name, MidiOut::default())?;
track.player.midi_outs.push(port);
}
for connection in midi_from.iter() {
let mut split = connection.as_ref().split("=");
let number = split.next().unwrap().trim();
if let Ok(track) = number.parse::<usize>() {
if track < 1 {
panic!("Tracks are zero-indexed")
}
if track > count {
panic!("Tried to connect track {track} or {count}. Pass -t {track} to increase number of tracks.")
}
if let Some(port) = split.next() {
if let Some(port) = jack.read().unwrap().client().port_by_name(port).as_ref() {
jack.read().unwrap().client().connect_ports(port, &self.tracks[track-1].player.midi_ins[0])?;
} else {
panic!("Missing MIDI output: {port}. Use jack_lsp to list all port names.");
}
} else {
panic!("No port specified for track {track}. Format is TRACK_NUMBER=PORT_NAME")
}
} else {
panic!("Failed to parse track number: {number}")
}
}
for connection in midi_to.iter() {
let mut split = connection.as_ref().split("=");
let number = split.next().unwrap().trim();
if let Ok(track) = number.parse::<usize>() {
if track < 1 {
panic!("Tracks are zero-indexed")
}
if track > count {
panic!("Tried to connect track {track} or {count}. Pass -t {track} to increase number of tracks.")
}
if let Some(port) = split.next() {
if let Some(port) = jack.read().unwrap().client().port_by_name(port).as_ref() {
jack.read().unwrap().client().connect_ports(&self.tracks[track-1].player.midi_outs[0], port)?;
} else {
panic!("Missing MIDI input: {port}. Use jack_lsp to list all port names.");
}
} else {
panic!("No port specified for track {track}. Format is TRACK_NUMBER=PORT_NAME")
}
} else {
panic!("Failed to parse track number: {number}")
}
}
Ok(())
}
}
#[derive(Debug)] pub struct ArrangerTrack {
/// Name of track
pub name: Arc<str>,
/// Preferred width of track column
pub width: usize,
/// Identifying color of track
pub color: ItemPalette,
/// MIDI player state
pub player: MidiPlayer,
}
has_clock!(|self:ArrangerTrack|self.player.clock());
has_player!(|self:ArrangerTrack|self.player);
impl ArrangerTrack {
/// Name of track
pub fn name (&self) -> &Arc<str> {
&self.name
}
/// Preferred width of track column
fn width (&self) -> usize {
self.width
}
/// Preferred width of track column
fn width_mut (&mut self) -> &mut usize {
&mut self.width
}
/// Identifying color of track
pub fn color (&self) -> ItemPalette {
self.color
}
fn longest_name (tracks: &[Self]) -> usize {
tracks.iter().map(|s|s.name.len()).fold(0, usize::max)
}
fn width_inc (&mut self) {
*self.width_mut() += 1;
}
fn width_dec (&mut self) {
if self.width() > Arranger::TRACK_MIN_WIDTH {
*self.width_mut() -= 1;
}
}
}
/// Hosts the JACK callback for a collection of tracks
pub struct TracksAudio<'a>(
// Track collection
pub &'a mut [ArrangerTrack],
/// Note buffer
pub &'a mut Vec<u8>,
/// Note chunk buffer
pub &'a mut Vec<Vec<Vec<u8>>>,
);
impl Audio for TracksAudio<'_> {
#[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
let model = &mut self.0;
let note_buffer = &mut self.1;
let output_buffer = &mut self.2;
for track in model.iter_mut() {
if PlayerAudio(track.player_mut(), note_buffer, output_buffer).process(client, scope) == Control::Quit {
return Control::Quit
}
}
Control::Continue
}
}

View file

@ -1,521 +0,0 @@
use crate::*;
pub(crate) const HEADER_H: u16 = 0; // 5
pub(crate) const SCENES_W_OFFSET: u16 = 0;
render!(TuiOut: (self: Arranger) => {
let toolbar = |x|Bsp::s(self.toolbar_view(), x);
let pool = |x|Bsp::w(self.pool_view(), x);
let editing = |x|Bsp::n(Bsp::e(self.editor.clip_status(), self.editor.edit_status()), x);
let enclosed = |x|Outer(Style::default().fg(Color::Rgb(72,72,72))).enclose(x);
let scenes_w = 16;//.max(SCENES_W_OFFSET + ArrangerScene::longest_name(&self.scenes) as u16);
let arrrrrr = Map::new(
move||[
(0, 2, self.output_row_header(), self.output_row_cells()),
(2, 3, self.elapsed_row_header(), self.elapsed_row_cells()),
(4, 3, self.next_row_header(), self.next_row_cells()),
(6, 3, self.track_row_header(), self.track_row_cells()),
(8, 20, self.scene_row_headers(), self.scene_row_cells()),
(25, 2, self.input_row_header(), self.input_row_cells()),
].into_iter(),
move|(y, h, header, cells), index|map_south(y, h, Fill::x(Align::w(Bsp::e(
Fixed::xy(scenes_w, h, header),
Fixed::xy(self.tracks.len() as u16*6, h, cells)
)))));
self.size.of(toolbar(pool(editing(Bsp::s(arrrrrr, enclosed(&self.editor))))))
});
impl Arranger {
pub const LEFT_SEP: char = '▎';
pub const TRACK_MIN_WIDTH: usize = 4;
/// A 1-row cell.
fn cell <T: Content<TuiOut>> (color: ItemPalette, field: T) -> impl Content<TuiOut> {
Tui::fg_bg(color.lightest.rgb, color.base.rgb, Fixed::y(1, field))
}
/// A phat line
fn phat_lo (fg: Color, bg: Color) -> impl Content<TuiOut> {
Fixed::y(1, Tui::fg_bg(fg, bg, RepeatH(&"")))
}
fn phat_hi (fg: Color, bg: Color) -> impl Content<TuiOut> {
Fixed::y(1, Tui::fg_bg(fg, bg, RepeatH(&"")))
}
/// A cell that is 3-row on its own, but stacks, giving (N+1)*2 rows per N cells.
fn phat_cell <T: Content<TuiOut>> (color: ItemPalette, last: ItemPalette, field: T) -> impl Content<TuiOut> {
Bsp::s(
Self::phat_lo(color.base.rgb, last.base.rgb),
Bsp::n(
Self::phat_hi(color.base.rgb, last.base.rgb),
Fixed::y(1, Fill::x(Tui::fg_bg(color.lightest.rgb, color.base.rgb, field))),
)
)
}
fn phat_cell_3 <T: Content<TuiOut>> (top: Color, middle: Color, bottom: Color, field: T) -> impl Content<TuiOut> {
Bsp::s(
Self::phat_lo(middle, top),
Bsp::n(
Self::phat_hi(bottom, middle),
Fixed::y(1, Fill::x(Tui::bg(middle, field))),
)
)
}
fn output_row_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
(||Tui::bold(true, Tui::fg_bg(TuiTheme::g(0), TuiTheme::g(200), "[ ] Out 1: NI")).boxed()).into()
}
fn output_row_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
//let scenes_w = 16;//.max(SCENES_W_OFFSET + ArrangerScene::longest_name(&self.scenes) as u16);
(move||Fixed::y(2, Map::new(||self.tracks_with_widths(), move|(_, track, x1, x2), i| {
let w = (x2 - x1) as u16;
let color: ItemPalette = track.color().dark.into();
let cell = Bsp::s(format!(" M S "), Self::phat_hi(color.dark.rgb, color.darker.rgb));
map_east(x1 as u16, w, Fixed::x(w, Self::cell(color, cell)))
})).boxed()).into()
}
fn elapsed_row_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
(||Tui::bold(true, Tui::fg(TuiTheme::g(128), "Playing")).boxed()).into()
}
fn elapsed_row_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
(move||Fixed::y(2, Map::new(||self.tracks_with_widths(), move|(_, track, x1, x2), i| {
//let color = track.color();
let color: ItemPalette = track.color().dark.into();
let timebase = self.clock().timebase();
let value = Tui::fg_bg(color.lightest.rgb, color.base.rgb,
if let Some((_, Some(phrase))) = track.player.play_phrase().as_ref() {
let length = phrase.read().unwrap().length;
let elapsed = track.player.pulses_since_start().unwrap() as usize;
format!("+{:>}", timebase.format_beats_1_short((elapsed % length) as f64))
} else {
String::new()
});
let cell = Bsp::s(value, Self::phat_hi(color.dark.rgb, color.darker.rgb));
Tui::bg(color.base.rgb, map_east(x1 as u16, (x2 - x1) as u16, cell))
})).boxed()).into()
}
fn next_row_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
(||Tui::bold(true, Tui::fg(TuiTheme::g(128), "Next")).boxed()).into()
}
fn next_row_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
(move||Fixed::y(2, Map::new(||self.tracks_with_widths(), move|(_, track, x1, x2), i| {
let color: ItemPalette = track.color();
let color: ItemPalette = track.color().dark.into();
let until_next = Self::cell(color, Tui::bold(true, Self::cell_until_next(track, &self.clock().playhead)));
let value = Tui::fg_bg(color.lightest.rgb, color.base.rgb, until_next);
let cell = Bsp::s(value, Self::phat_hi(color.dark.rgb, color.darker.rgb));
Tui::bg(color.base.rgb, map_east(x1 as u16, (x2 - x1) as u16, cell))
})).boxed()).into()
}
fn track_row_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
(||Tui::bold(true, Tui::fg(TuiTheme::g(128), "Track")).boxed()).into()
}
fn track_row_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
let iter = ||self.tracks_with_widths();
(move||Fixed::y(2, Map::new(iter, move|(_, track, x1, x2), i| {
let color = track.color();
let name = format!(" {}", &track.name);
Tui::bg(color.base.rgb, map_east(x1 as u16, (x2 - x1) as u16,
Tui::fg_bg(color.lightest.rgb, color.base.rgb,
Self::phat_cell(color, color.darkest.rgb.into(),
Tui::bold(true, name))))) })).boxed()).into()
}
fn input_row_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
(||Fill::x(Tui::bold(true, Tui::fg_bg(TuiTheme::g(0), TuiTheme::g(200), "[ ] In 1: Korg"))).boxed()).into()
}
fn input_row_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
(move||Fixed::y(2, Map::new(||self.tracks_with_widths(), move|(_, track, x1, x2), i| {
let w = (x2 - x1) as u16;
let cell = Bsp::s("[Rec]", "[Mon]");
let color: ItemPalette = track.color().dark.into();
map_east(x1 as u16, w, Fixed::x(w, Self::cell(color, cell)))
})).boxed()).into()
}
fn scene_row_headers <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
(||{
let scenes_w = 16;//.max(SCENES_W_OFFSET + ArrangerScene::longest_name(&self.scenes) as u16);
let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0))));
Tui::bg(Color::Rgb(0,0,0), Fill::y(Map::new(
||self.scenes_with_heights(2),
move|(_, scene, y1, y2), i| {
let h = (y2 - y1) as u16;
let color = scene.color();
let name = format!("🭬{}", &scene.name);
let cell = Self::phat_cell(color, *last_color.read().unwrap(), name);
*last_color.write().unwrap() = color;
map_south(y1 as u16, 2, Fill::x(cell))
}
))).boxed()
}).into()
}
fn scene_row_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
(move||Fixed::y(2, Map::new(||self.tracks_with_widths(), move|(_, track, x1, x2), i| {
let w = (x2 - x1) as u16;
let cell = Bsp::s("[Rec]", "[Mon]");
let color: ItemPalette = track.color().dark.into();
let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0))));
map_east(x1 as u16, w, Fixed::x(w, Tui::bg(Color::Rgb(0,0,0), Fill::y(Map::new(
||self.scenes_with_heights(2),
move|(_, scene, y1, y2), i| {
let h = (y2 - y1) as u16;
let color = scene.color();
let name = format!("🭬{}", &scene.name);
//*last_color.write().unwrap() = color
map_south(y1 as u16, 2, Fill::x(Self::phat_cell_3(
TuiTheme::g(32).into(),
TuiTheme::g(32).into(),
TuiTheme::g(32).into(),
//Tui::fg(TuiTheme::g(64), " ⏺ ")
Tui::fg(TuiTheme::g(64), "")
)))
}
))).boxed()
))
})).boxed()).into()
}
pub fn tracks_with_widths (&self)
-> impl Iterator<Item = (usize, &ArrangerTrack, usize, usize)>
{
Self::tracks_with_widths_static(self.tracks.as_slice())
}
fn tracks_with_widths_static (tracks: &[ArrangerTrack])
-> impl Iterator<Item = (usize, &ArrangerTrack, usize, usize)>
{
let mut x = 0;
tracks.iter().enumerate().map(move |(index, track)|{
let data = (index, track, x, x + track.width);
x += track.width;
data
})
}
fn track_column_separators <'a> (&'a self) -> impl Content<TuiOut> + 'a {
let scenes_w = 16;//.max(SCENES_W_OFFSET + ArrangerScene::longest_name(&self.scenes) as u16);
let fg = Color::Rgb(64,64,64);
Map::new(move||self.tracks_with_widths(), move|(_n, _track, x1, x2), _i|{
Push::x(scenes_w, map_east(x1 as u16, (x2 - x1) as u16,
Fixed::x((x2 - x1) as u16, Tui::fg(fg, RepeatV(&"·")))))
})
}
/// name and width of track
fn cell_name (track: &ArrangerTrack, _w: usize) -> impl Content<TuiOut> {
Tui::bold(true, Tui::fg(track.color.lightest.rgb, track.name().clone()))
}
/// beats until switchover
fn cell_until_next (track: &ArrangerTrack, current: &Arc<Moment>)
-> Option<impl Content<TuiOut>>
{
let timebase = &current.timebase;
let mut result = String::new();
if let Some((t, _)) = track.player.next_phrase().as_ref() {
let target = t.pulse.get();
let current = current.pulse.get();
if target > current {
result = format!("-{:>}", timebase.format_beats_0_short(target - current))
}
}
Some(result)
}
pub fn scenes_with_heights (&self, h: usize) -> impl Iterator<Item = (usize, &ArrangerScene, usize, usize)> {
let mut y = 0;
self.scenes.iter().enumerate().map(move|(index, scene)|{
let data = (index, scene, y, y + h);
y += h;
data
})
}
fn scene_rows (&self) -> impl Content<TuiOut> + use<'_> {
let scenes_w = 16.max(SCENES_W_OFFSET + ArrangerScene::longest_name(&self.scenes) as u16);
Map::new(||self.scenes_with_heights(1), move|(_, scene, y1, y2), i| {
let h = (y2 - y1) as u16;
let color = scene.color();
let cell = Fixed::y(h, Fixed::x(scenes_w, Self::cell(color, scene.name.clone())));
map_south(y1 as u16, 1, cell)
})
}
pub fn scene_heights (scenes: &[ArrangerScene], factor: usize) -> Vec<(usize, usize)> {
let mut total = 0;
if factor == 0 {
scenes.iter().map(|scene|{
let pulses = scene.pulses().max(PPQ);
total += pulses;
(pulses, total - pulses)
}).collect()
} else {
(0..=scenes.len()).map(|i|{
(factor*PPQ, factor*PPQ*i)
}).collect()
}
}
fn cell_scene <'a> (
tracks: &'a [ArrangerTrack], scene: &'a ArrangerScene, pulses: usize
) -> impl Content<TuiOut> + use<'a> {
let height = 1.max((pulses / PPQ) as u16);
let playing = scene.is_playing(tracks);
let icon = Tui::bg(
scene.color.base.rgb, if playing { "" } else { " " }
);
let name = Tui::fg_bg(scene.color.lightest.rgb, scene.color.base.rgb,
Expand::x(1, Tui::bold(true, scene.name.clone()))
);
let clips = Map::new(||Arranger::tracks_with_widths_static(tracks), move|(index, track, x1, x2), _|
Push::x((x2 - x1) as u16, Self::cell_clip(scene, index, track, (x2 - x1) as u16, height))
);
Fixed::y(height, Bsp::e(icon, Bsp::e(name, clips)))
}
fn cell_clip <'a> (
scene: &'a ArrangerScene, index: usize, track: &'a ArrangerTrack, w: u16, h: u16
) -> impl Content<TuiOut> + use<'a> {
scene.clips.get(index).map(|clip|clip.as_ref().map(|phrase|{
let phrase = phrase.read().unwrap();
let mut bg = TuiTheme::border_bg();
let name = phrase.name.to_string();
let max_w = name.len().min((w as usize).saturating_sub(2));
let color = phrase.color;
bg = color.dark.rgb;
if let Some((_, Some(ref playing))) = track.player.play_phrase() {
if *playing.read().unwrap() == *phrase {
bg = color.light.rgb
}
};
Fixed::xy(w, h, &Tui::bg(bg, Push::x(1, Fixed::x(w, &name.as_str()[0..max_w]))));
}))
}
fn toolbar_view (&self) -> impl Content<TuiOut> + use<'_> {
Fill::x(Fixed::y(2, Align::x(TransportView::new(true, &self.clock))))
}
fn pool_view (&self) -> impl Content<TuiOut> + use<'_> {
let w = self.size.w();
let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
let pool_w = if self.pool.visible { phrase_w } else { 0 };
let pool = Pull::y(1, Fill::y(Align::e(PoolView(self.pool.visible, &self.pool))));
Fixed::x(pool_w, Align::e(Fill::y(PoolView(self.compact, &self.pool))))
}
pub fn track_widths (tracks: &[ArrangerTrack]) -> Vec<(usize, usize)> {
let mut widths = vec![];
let mut total = 0;
for track in tracks.iter() {
let width = track.width;
widths.push((width, total));
total += width;
}
widths.push((0, total));
widths
}
fn scene_row_sep <'a> (&'a self) -> impl Content<TuiOut> + 'a {
let fg = Color::Rgb(255,255,255);
Map::new(move||self.scenes_with_heights(1), |_, _|"")
//Map(||rows.iter(), |(_n, _scene, y1, _y2), _i| {
//let y = to.area().y() + (y / PPQ) as u16 + 1;
//if y >= to.buffer.area.height { break }
//for x in to.area().x()..to.area().x2().saturating_sub(2) {
////if x < to.buffer.area.x && y < to.buffer.area.y {
//if let Some(cell) = to.buffer.cell_mut(ratatui::prelude::Position::from((x, y))) {
//cell.modifier = Modifier::UNDERLINED;
//cell.underline_color = fg;
//}
////}
//}
//})
}
fn cursor (&self) -> impl Content<TuiOut> + '_ {
let color = self.color;
let bg = color.lighter.rgb;//Color::Rgb(0, 255, 0);
let selected = self.selected();
let cols = Arranger::track_widths(&self.tracks);
let rows = Arranger::scene_heights(&self.scenes, 1);
let scenes_w = 16.max(SCENES_W_OFFSET + ArrangerScene::longest_name(&self.scenes) as u16);
let focused = true;
let reticle = Reticle(Style {
fg: Some(self.color.lighter.rgb),
bg: None,
underline_color: None,
add_modifier: Modifier::empty(),
sub_modifier: Modifier::DIM
});
RenderThunk::new(move|to: &mut TuiOut|{
let area = to.area();
let [x, y, w, h] = area.xywh();
let mut track_area: Option<[u16;4]> = match selected {
ArrangerSelection::Track(t) | ArrangerSelection::Clip(t, _) => Some([
x + scenes_w + cols[t].1 as u16, y,
cols[t].0 as u16, h,
]),
_ => None
};
let mut scene_area: Option<[u16;4]> = match selected {
ArrangerSelection::Scene(s) | ArrangerSelection::Clip(_, s) => Some([
x, y + HEADER_H + (rows[s].1 / PPQ) as u16,
w, (rows[s].0 / PPQ) as u16
]),
_ => None
};
let mut clip_area: Option<[u16;4]> = match selected {
ArrangerSelection::Clip(t, s) => Some([
(scenes_w + x + cols[t].1 as u16).saturating_sub(1),
HEADER_H + y + (rows[s].1/PPQ) as u16,
cols[t].0 as u16 + 2,
(rows[s].0 / PPQ) as u16
]),
_ => None
};
if let Some([x, y, width, height]) = track_area {
to.fill_fg([x, y, 1, height], bg);
to.fill_fg([x + width, y, 1, height], bg);
}
if let Some([_, y, _, height]) = scene_area {
to.fill_ul([x, y - 1, w, 1], bg);
to.fill_ul([x, y + height - 1, w, 1], bg);
}
if focused {
to.place(if let Some(clip_area) = clip_area {
clip_area
} else if let Some(track_area) = track_area {
track_area.clip_h(HEADER_H)
} else if let Some(scene_area) = scene_area {
scene_area.clip_w(scenes_w)
} else {
area.clip_w(scenes_w).clip_h(HEADER_H)
}, &reticle)
};
})
}
}
//pub struct ArrangerVCursor {
//cols: Vec<(usize, usize)>,
//rows: Vec<(usize, usize)>,
//color: ItemPalette,
//reticle: Reticle,
//selected: ArrangerSelection,
//scenes_w: u16,
//}
//from!(|args:(&Arranger, usize)|ArrangerVCursor = Self {
//cols: Arranger::track_widths(&args.0.tracks),
//rows: Arranger::scene_heights(&args.0.scenes, args.1),
//selected: args.0.selected(),
//scenes_w: SCENES_W_OFFSET + ArrangerScene::longest_name(&args.0.scenes) as u16,
//color: args.0.color,
//reticle: Reticle(Style {
//fg: Some(args.0.color.lighter.rgb),
//bg: None,
//underline_color: None,
//add_modifier: Modifier::empty(),
//sub_modifier: Modifier::DIM
//}),
//});
//impl Content<TuiOut> for ArrangerVCursor {
//fn render (&self, to: &mut TuiOut) {
//let area = to.area();
//let focused = true;
//let selected = self.selected;
//let get_track_area = |t: usize| [
//self.scenes_w + area.x() + self.cols[t].1 as u16, area.y(),
//self.cols[t].0 as u16, area.h(),
//];
//let get_scene_area = |s: usize| [
//area.x(), HEADER_H + area.y() + (self.rows[s].1 / PPQ) as u16,
//area.w(), (self.rows[s].0 / PPQ) as u16
//];
//let get_clip_area = |t: usize, s: usize| [
//(self.scenes_w + area.x() + self.cols[t].1 as u16).saturating_sub(1),
//HEADER_H + area.y() + (self.rows[s].1/PPQ) as u16,
//self.cols[t].0 as u16 + 2,
//(self.rows[s].0 / PPQ) as u16
//];
//let mut track_area: Option<[u16;4]> = None;
//let mut scene_area: Option<[u16;4]> = None;
//let mut clip_area: Option<[u16;4]> = None;
//let area = match selected {
//ArrangerSelection::Mix => area,
//ArrangerSelection::Track(t) => {
//track_area = Some(get_track_area(t));
//area
//},
//ArrangerSelection::Scene(s) => {
//scene_area = Some(get_scene_area(s));
//area
//},
//ArrangerSelection::Clip(t, s) => {
//track_area = Some(get_track_area(t));
//scene_area = Some(get_scene_area(s));
//clip_area = Some(get_clip_area(t, s));
//area
//},
//};
//let bg = self.color.lighter.rgb;//Color::Rgb(0, 255, 0);
//if let Some([x, y, width, height]) = track_area {
//to.fill_fg([x, y, 1, height], bg);
//to.fill_fg([x + width, y, 1, height], bg);
//}
//if let Some([_, y, _, height]) = scene_area {
//to.fill_ul([area.x(), y - 1, area.w(), 1], bg);
//to.fill_ul([area.x(), y + height - 1, area.w(), 1], bg);
//}
//if focused {
//to.place(if let Some(clip_area) = clip_area {
//clip_area
//} else if let Some(track_area) = track_area {
//track_area.clip_h(HEADER_H)
//} else if let Some(scene_area) = scene_area {
//scene_area.clip_w(self.scenes_w)
//} else {
//area.clip_w(self.scenes_w).clip_h(HEADER_H)
//}, &self.reticle)
//};
//}
//}
//impl Arranger {
//fn render_mode (state: &Self) -> impl Content<TuiOut> + use<'_> {
//match state.mode {
//ArrangerMode::H => todo!("horizontal arranger"),
//ArrangerMode::V(factor) => Self::render_mode_v(state, factor),
//}
//}
//}
//render!(TuiOut: (self: Arranger) => {
//let pool_w = if self.pool.visible { self.splits[1] } else { 0 };
//let color = self.color;
//let layout = Bsp::a(Fill::xy(ArrangerStatus::from(self)),
//Bsp::n(Fixed::x(pool_w, PoolView(self.pool.visible, &self.pool)),
//Bsp::n(TransportView::new(true, &self.clock),
//Bsp::s(Fixed::y(1, MidiEditStatus(&self.editor)),
//Bsp::n(Fill::x(Fixed::y(20,
//Bsp::a(Fill::xy(Tui::bg(color.darkest.rgb, "background")),
//Bsp::a(
//Fill::xy(Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb))),
//Self::render_mode(self))))), Fill::y(&"fixme: self.editor"))))));
//self.size.of(layout)
//});
//Align::n(Fill::xy(lay!(
//Align::n(Fill::xy(Tui::bg(self.color.darkest.rgb, " "))),
//Align::n(Fill::xy(ArrangerVRowSep::from((self, 1)))),
//Align::n(Fill::xy(ArrangerVColSep::from(self))),
//Align::n(Fill::xy(ArrangerVClips::new(self, 1))),
//Align::n(Fill::xy(ArrangerVCursor::from((self, 1))))))))))))))));
//Align::n(Fill::xy(":")))))))))))));
//"todo:"))))))));
//Bsp::s(
//Align::n(Fixed::y(1, Fill::x(ArrangerVIns::from(self)))),
//Bsp::s(
//Fixed::y(20, Align::n(ArrangerVClips::new(self, 1))),
//Fill::x(Fixed::y(1, ArrangerVOuts::from(self)))))))))))));
//Bsp::s(
//Bsp::s(
//Bsp::s(
//Fill::xy(ArrangerVClips::new(self, 1)),
//Fill::x(ArrangerVOuts::from(self)))))

View file

@ -1,103 +0,0 @@
mod groovebox_command; pub use self::groovebox_command::*;
mod groovebox_tui; pub use self::groovebox_tui::*;
use crate::*;
use super::*;
use KeyCode::{Char, Delete, Tab, Up, Down, Left, Right};
use ClockCommand::{Play, Pause};
use GrooveboxCommand as Cmd;
use MidiEditCommand::*;
use MidiPoolCommand::*;
pub struct Groovebox {
_jack: Arc<RwLock<JackConnection>>,
pub player: MidiPlayer,
pub pool: PoolModel,
pub editor: MidiEditor,
pub sampler: Sampler,
pub compact: bool,
pub size: Measure<TuiOut>,
pub status: bool,
pub note_buf: Vec<u8>,
pub midi_buf: Vec<Vec<Vec<u8>>>,
pub perf: PerfModel,
}
audio!(|self: Groovebox, client, scope|{
let t0 = self.perf.get_t0();
if Control::Quit == ClockAudio(&mut self.player).process(client, scope) {
return Control::Quit
}
if Control::Quit == PlayerAudio(
&mut self.player, &mut self.note_buf, &mut self.midi_buf
).process(client, scope) {
return Control::Quit
}
if Control::Quit == SamplerAudio(&mut self.sampler).process(client, scope) {
return Control::Quit
}
// TODO move these to editor and sampler:
for RawMidi { time, bytes } in self.player.midi_ins[0].iter(scope) {
if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() {
match message {
MidiMessage::NoteOn { ref key, .. } => {
self.editor.set_note_point(key.as_int() as usize);
},
MidiMessage::Controller { controller, value } => {
if let Some(sample) = &self.sampler.mapped[self.editor.note_point()] {
sample.write().unwrap().handle_cc(controller, value)
}
}
_ => {}
}
}
}
self.perf.update(t0, scope);
Control::Continue
});
has_clock!(|self: Groovebox|self.player.clock());
impl EdnViewData<TuiOut> for &Groovebox {
fn get_unit (&self, item: EdnItem<&str>) -> u16 {
use EdnItem::*;
match item.to_str() {
":sample-h" => if self.compact { 0 } else { 5 },
":samples-w" => if self.compact { 4 } else { 11 },
":samples-y" => if self.compact { 1 } else { 0 },
":pool-w" => if self.compact { 5 } else {
let w = self.size.w();
if w > 60 { 20 } else if w > 40 { 15 } else { 10 }
},
_ => 0
}
}
fn get_content <'a> (&'a self, item: EdnItem<&str>) -> RenderBox<'a, TuiOut> {
use EdnItem::*;
match item {
Nil => Box::new(()),
Sym(bol) => match bol {
":input-meter-l" => Meter("L/", self.sampler.input_meter[0]).boxed(),
":input-meter-r" => Box::new(Meter("R/", self.sampler.input_meter[1])),
":transport" => Box::new(TransportView::new(true, &self.player.clock)),
":clip-play" => Box::new(self.player.play_status()),
":clip-next" => Box::new(self.player.next_status()),
":clip-edit" => Box::new(self.editor.clip_status()),
":edit-stat" => Box::new(self.editor.edit_status()),
":pool-view" => Box::new(PoolView(self.compact, &self.pool)),
":midi-view" => Box::new(&self.editor),
":sample-view" => Box::new(SampleViewer::from_sampler(&self.sampler, self.editor.note_point())),
":sample-stat" => Box::new(SamplerStatus(&self.sampler, self.editor.note_point())),
":samples-view" => Box::new(SampleList::new(self.compact, &self.sampler, &self.editor)),
_ => panic!("unknown sym {bol:?}")
},
Exp(items) => Box::new(EdnView::from_items(*self, items.as_slice())),
_ => panic!("no content for {item:?}")
}
}
}

View file

@ -1,17 +0,0 @@
(bsp/s
(fill/x (fixed/y 2 (bsp/a
(align/x :transport)
(bsp/a
(align/w :input-meter-l)
(align/e :input-meter-r)))))
(bsp/n
(bsp/e :clip-play (bsp/e :clip-next (bsp/e :clip-edit :edit-stat)))
(bsp/n
(max/y :sample-h (fill/xy :sample-view))
(bsp/n
(align/w (fixed/y 1 :sample-stat))
(bsp/n
(fixed/x :pool-w :pool-view)
(fill/xy (bsp/e
(fixed/x :samples-w (push/y :samples-y :samples-view))
:midi-view)))))))

View file

@ -1,93 +0,0 @@
use crate::*;
use super::*;
use self::GrooveboxCommand as Cmd;
use ClockCommand::{Play, Pause};
pub enum GrooveboxCommand {
Compact(bool),
History(isize),
Clock(ClockCommand),
Pool(PoolCommand),
Editor(MidiEditCommand),
Enqueue(Option<Arc<RwLock<MidiClip>>>),
Sampler(SamplerCommand),
}
command!(|self: GrooveboxCommand, state: Groovebox|match self {
Self::Enqueue(phrase) => {
state.player.enqueue_next(phrase.as_ref());
None
},
Self::Pool(cmd) => match cmd {
// autoselect: automatically load selected phrase in editor
PoolCommand::Select(_) => {
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
state.editor.set_phrase(Some(state.pool.phrase()));
undo
},
// update color in all places simultaneously
PoolCommand::Phrase(SetColor(index, _)) => {
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
state.editor.set_phrase(Some(state.pool.phrase()));
undo
},
_ => cmd.delegate(&mut state.pool, Self::Pool)?
},
Self::Sampler(cmd) => cmd.delegate(&mut state.sampler, Self::Sampler)?,
Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?,
Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?,
Self::History(delta) => { todo!("undo/redo") },
Self::Compact(compact) => if state.compact != compact {
state.compact = compact;
Some(Self::Compact(!compact))
} else {
None
},
});
handle!(TuiIn: |self: Groovebox, input|
GrooveboxCommand::execute_with_state(self, input.event()));
keymap!(<'a> KEYS_GROOVEBOX = |state: Groovebox, input: Event| GrooveboxCommand {
// Tab: Toggle compact mode
key(Tab) => Cmd::Compact(!state.compact),
// q: Enqueue currently edited phrase
key(Char('q')) => Cmd::Enqueue(Some(state.pool.phrase().clone())),
// 0: Enqueue phrase 0 (stop all)
key(Char('0')) => Cmd::Enqueue(Some(state.pool.phrases()[0].clone())),
// TODO: k: toggle on-screen keyboard
ctrl(key(Char('k'))) => todo!("keyboard"),
// Transport: Play from start or rewind to start
ctrl(key(Char(' '))) => Cmd::Clock(
if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }
),
// Shift-R: toggle recording
shift(key(Char('R'))) => Cmd::Sampler(if state.sampler.recording.is_some() {
SamplerCommand::RecordFinish
} else {
SamplerCommand::RecordBegin(u7::from(state.editor.note_point() as u8))
}),
// Shift-Del: delete sample
shift(key(Delete)) => Cmd::Sampler(
SamplerCommand::SetSample(u7::from(state.editor.note_point() as u8), None)
),
// e: Toggle between editing currently playing or other phrase
shift(key(Char('e'))) => if let Some((_, Some(playing))) = state.player.play_phrase() {
let editing = state.editor.phrase().as_ref().map(|p|p.read().unwrap().clone());
let selected = state.pool.phrase().clone();
Cmd::Editor(Show(Some(if Some(selected.read().unwrap().clone()) != editing {
selected
} else {
playing.clone()
})))
} else {
return None
},
}, if let Some(command) = MidiEditCommand::input_to_command(&state.editor, input) {
Cmd::Editor(command)
} else if let Some(command) = PoolCommand::input_to_command(&state.pool, input) {
Cmd::Pool(command)
} else {
return None
});

View file

@ -1,187 +0,0 @@
use crate::*;
use super::*;
use std::marker::ConstParamTy;
use EdnItem::*;
const EDN: &'static str = include_str!("groovebox.edn");
// this works:
render!(TuiOut: (self: Groovebox) => self.size.of(
Bsp::s(self.toolbar_view(),
Bsp::n(self.selector_view(),
Bsp::n(self.sample_view(),
Bsp::n(self.status_view(),
Bsp::w(self.pool_view(), Fill::xy(Bsp::e(self.sampler_view(), &self.editor)))))))));
// this almost does:
//impl Content<TuiOut> for Groovebox {
//fn content (&self) -> impl Render<TuiOut> {
//self.size.of(EdnView::from_source(self, EDN))
//}
//}
impl Groovebox {
fn toolbar_view (&self) -> impl Content<TuiOut> + use<'_> {
Fill::x(Fixed::y(2, lay!(
Align::w(Meter("L/", self.sampler.input_meter[0])),
Align::e(Meter("R/", self.sampler.input_meter[1])),
Align::x(TransportView::new(true, &self.player.clock)),
)))
}
fn selector_view (&self) -> impl Content<TuiOut> + use<'_> {
row!(
self.player.play_status(),
self.player.next_status(),
self.editor.clip_status(),
self.editor.edit_status(),
)
}
fn sample_view (&self) -> impl Content<TuiOut> + use<'_> {
let note_pt = self.editor.note_point();
let sample_h = if self.compact { 0 } else { 5 };
Max::y(sample_h, Fill::xy(
SampleViewer::from_sampler(&self.sampler, note_pt)))
}
fn status_view (&self) -> impl Content<TuiOut> + use<'_> {
let note_pt = self.editor.note_point();
Align::w(Fixed::y(1, SamplerStatus(&self.sampler, note_pt)))
}
fn pool_view (&self) -> impl Content<TuiOut> + use<'_> {
let w = self.size.w();
let pool_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
Fixed::x(if self.compact { 5 } else { pool_w },
PoolView(self.compact, &self.pool))
}
fn sampler_view (&self) -> impl Content<TuiOut> + use<'_> {
let sampler_w = if self.compact { 4 } else { 40 };
let sampler_y = if self.compact { 1 } else { 0 };
Fixed::x(sampler_w, Push::y(sampler_y, Fill::y(
SampleList::new(self.compact, &self.sampler, &self.editor))))
}
}
///// Status bar for sequencer app
//#[derive(Clone)]
//pub struct GrooveboxStatus {
//pub(crate) width: usize,
//pub(crate) cpu: Option<String>,
//pub(crate) size: String,
//pub(crate) playing: bool,
//}
//from!(|state: &Groovebox|GrooveboxStatus = {
//let samples = state.clock().chunk.load(Relaxed);
//let rate = state.clock().timebase.sr.get();
//let buffer = samples as f64 / rate;
//let width = state.size.w();
//Self {
//width,
//playing: state.clock().is_rolling(),
//cpu: state.perf.percentage().map(|cpu|format!("│{cpu:.01}%")),
//size: format!("{}x{}│", width, state.size.h()),
//}
//});
//render!(TuiOut: (self: GrooveboxStatus) => Fixed::y(2, lay!(
//Self::help(),
//Fill::xy(Align::se(Tui::fg_bg(TuiTheme::orange(), TuiTheme::g(25), self.stats()))),
//)));
//impl GrooveboxStatus {
//fn help () -> impl Content<TuiOut> {
//let single = |binding, command|row!(" ", col!(
//Tui::fg(TuiTheme::yellow(), binding),
//command
//));
//let double = |(b1, c1), (b2, c2)|col!(
//row!(" ", Tui::fg(TuiTheme::yellow(), b1), " ", c1,),
//row!(" ", Tui::fg(TuiTheme::yellow(), b2), " ", c2,),
//);
//Tui::fg_bg(TuiTheme::g(255), TuiTheme::g(50), row!(
//single("SPACE", "play/pause"),
//double(("▲▼▶◀", "cursor"), ("Ctrl", "scroll"), ),
//double(("a", "append"), ("s", "set note"),),
//double((",.", "length"), ("<>", "triplet"), ),
//double(("[]", "phrase"), ("{}", "order"), ),
//double(("q", "enqueue"), ("e", "edit"), ),
//double(("c", "color"), ("", ""),),
//))
//}
//fn stats (&self) -> impl Content<TuiOut> + use<'_> {
//row!(&self.cpu, &self.size)
//}
//}
//render!(TuiOut: (self: Groovebox) => self.size.of(
//Bsp::s(self.toolbar_view(),
//Bsp::n(self.selector_view(),
//Bsp::n(self.sample_view(),
//Bsp::n(self.status_view(),
//Bsp::w(self.pool_view(), Fill::xy(Bsp::e(self.sampler_view(), &self.editor)))))))));
//const GROOVEBOX_EDN: &'static str = include_str!("groovebox.edn");
//impl Content<TuiOut> for Groovebox {
//fn content (&self) -> impl Content<TuiOut> {
//EdnView::parse(self.edn.as_slice())
//}
//}
//macro_rules! edn_context {
//($Struct:ident |$l:lifetime, $state:ident| {
//$($key:literal = $field:ident: $Type:ty => $expr:expr,)*
//}) => {
//#[derive(Default)]
//pub struct EdnView<$l> { $($field: Option<$Type>),* }
//impl<$l> EdnView<$l> {
//pub fn parse <'e> (edn: &[Edn<'e>]) -> impl Fn(&$Struct) + use<'e> {
//let imports = Self::imports_all(edn);
//move |state| {
//let mut context = EdnView::default();
//for import in imports.iter() {
//context.import(state, import)
//}
//}
//}
//fn imports_all <'e> (edn: &[Edn<'e>]) -> Vec<&'e str> {
//let mut imports = vec![];
//for edn in edn.iter() {
//for import in Self::imports_one(edn) {
//imports.push(import);
//}
//}
//imports
//}
//fn imports_one <'e> (edn: &Edn<'e>) -> Vec<&'e str> {
//match edn {
//Edn::Symbol(import) => vec![import],
//Edn::List(edn) => Self::imports_all(edn.as_slice()),
//_ => vec![],
//}
//}
//pub fn import (&mut self, $state: &$l$Struct, key: &str) {
//match key {
//$($key => self.$field = Some($expr),)*
//_ => {}
//}
//}
//}
//}
//}
////impl Groovebox {
////fn status_view (&self) -> impl Content<TuiOut> + use<'_> {
////let note_pt = self.editor.note_point();
////Align::w(Fixed::y(1, ))
////}
////fn pool_view (&self) -> impl Content<TuiOut> + use<'_> {
////let w = self.size.w();
////let pool_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
////Fixed::x(if self.compact { 5 } else { pool_w },
////)
////}
////fn sampler_view (&self) -> impl Content<TuiOut> + use<'_> {
////let sampler_w = if self.compact { 4 } else { 11 };
////let sampler_y = if self.compact { 1 } else { 0 };
////Fixed::x(sampler_w, Push::y(sampler_y, Fill::y(
////SampleList::new(self.compact, &self.sampler, &self.editor))))
////}
////}

View file

@ -1,285 +0,0 @@
#![allow(unused)]
#![allow(clippy::unit_arg)]
#![feature(adt_const_params)]
#![feature(type_alias_impl_trait)]
#![feature(impl_trait_in_assoc_type)]
#![feature(associated_type_defaults)]
pub mod arranger; pub use self::arranger::*;
pub mod groovebox; pub use self::groovebox::*;
pub mod meter; pub use self::meter::*;
pub mod mixer; pub use self::mixer::*;
pub mod plugin; pub use self::plugin::*;
pub mod pool; pub use self::pool::*;
pub mod sampler; pub use self::sampler::*;
pub mod sequencer; pub use self::sequencer::*;
pub use ::tek_time; pub(crate) use ::tek_time::*;
pub use ::tek_jack; pub(crate) use ::tek_jack::{*, jack::{*, contrib::*}};
pub use ::tek_midi; pub(crate) use ::tek_midi::{*, midly::{*, num::*, live::*}};
pub use ::tek_tui::{self, tek_edn, tek_input, tek_output};
pub(crate) use ::tek_tui::{
*,
tek_edn::*,
tek_input::*,
tek_output::*,
crossterm::{
self,
event::{
Event, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers,
KeyCode::{self, *},
}
},
ratatui::{
self,
prelude::{Color, Style, Stylize, Buffer, Modifier},
buffer::Cell,
}
};
pub(crate) use std::cmp::{Ord, Eq, PartialEq};
pub(crate) use std::collections::BTreeMap;
pub(crate) use std::error::Error;
pub(crate) use std::fmt::{Debug, Display, Formatter};
pub(crate) use std::io::{Stdout, stdout};
pub(crate) use std::marker::PhantomData;
pub(crate) use std::ops::{Add, Sub, Mul, Div, Rem};
pub(crate) use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::{self, *}};
pub(crate) use std::sync::{Arc, Mutex, RwLock};
pub(crate) use std::thread::{spawn, JoinHandle};
pub(crate) use std::time::Duration;
pub(crate) use std::path::PathBuf;
pub(crate) use std::ffi::OsString;
//#[cfg(test)] mod test_focus {
//use super::focus::*;
//#[test] fn test_focus () {
//struct FocusTest {
//focused: char,
//cursor: (usize, usize)
//}
//impl HasFocus for FocusTest {
//type Item = char;
//fn focused (&self) -> Self::Item {
//self.focused
//}
//fn set_focused (&mut self, to: Self::Item) {
//self.focused = to
//}
//}
//impl FocusGrid for FocusTest {
//fn focus_cursor (&self) -> (usize, usize) {
//self.cursor
//}
//fn focus_cursor_mut (&mut self) -> &mut (usize, usize) {
//&mut self.cursor
//}
//fn focus_layout (&self) -> &[&[Self::Item]] {
//&[
//&['a', 'a', 'a', 'b', 'b', 'd'],
//&['a', 'a', 'a', 'b', 'b', 'd'],
//&['a', 'a', 'a', 'c', 'c', 'd'],
//&['a', 'a', 'a', 'c', 'c', 'd'],
//&['e', 'e', 'e', 'e', 'e', 'e'],
//]
//}
//}
//let mut tester = FocusTest { focused: 'a', cursor: (0, 0) };
//tester.focus_right();
//assert_eq!(tester.cursor.0, 3);
//assert_eq!(tester.focused, 'b');
//tester.focus_down();
//assert_eq!(tester.cursor.1, 2);
//assert_eq!(tester.focused, 'c');
//}
//}
//use crate::*;
//struct TestEngine([u16;4], Vec<Vec<char>>);
//impl Engine for TestEngine {
//type Unit = u16;
//type Size = [Self::Unit;2];
//type Area = [Self::Unit;4];
//type Input = Self;
//type Handled = bool;
//fn exited (&self) -> bool {
//true
//}
//}
//#[derive(Copy, Clone)]
//struct TestArea(u16, u16);
//impl Render<TestEngine> for TestArea {
//fn min_size (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
//Ok(Some([to[0], to[1], self.0, self.1]))
//}
//fn render (&self, to: &mut TestEngine) -> Perhaps<[u16;4]> {
//if let Some(layout) = self.layout(to.area())? {
//for y in layout.y()..layout.y()+layout.h()-1 {
//for x in layout.x()..layout.x()+layout.w()-1 {
//to.1[y as usize][x as usize] = '*';
//}
//}
//Ok(Some(layout))
//} else {
//Ok(None)
//}
//}
//}
//#[test]
//fn test_plus_minus () -> Usually<()> {
//let area = [0, 0, 10, 10];
//let engine = TestEngine(area, vec![vec![' ';10];10]);
//let test = TestArea(4, 4);
//assert_eq!(test.layout(area)?, Some([0, 0, 4, 4]));
//assert_eq!(Push::X(1, test).layout(area)?, Some([1, 0, 4, 4]));
//Ok(())
//}
//#[test]
//fn test_outset_align () -> Usually<()> {
//let area = [0, 0, 10, 10];
//let engine = TestEngine(area, vec![vec![' ';10];10]);
//let test = TestArea(4, 4);
//assert_eq!(test.layout(area)?, Some([0, 0, 4, 4]));
//assert_eq!(Margin::X(1, test).layout(area)?, Some([0, 0, 6, 4]));
//assert_eq!(Align::X(test).layout(area)?, Some([3, 0, 4, 4]));
//assert_eq!(Align::X(Margin::X(1, test)).layout(area)?, Some([2, 0, 6, 4]));
//assert_eq!(Margin::X(1, Align::X(test)).layout(area)?, Some([2, 0, 6, 4]));
//Ok(())
//}
////#[test]
////fn test_misc () -> Usually<()> {
////let area: [u16;4] = [0, 0, 10, 10];
////let test = TestArea(4, 4);
////assert_eq!(test.layout(area)?,
////Some([0, 0, 4, 4]));
////assert_eq!(Align::Center(test).layout(area)?,
////Some([3, 3, 4, 4]));
////assert_eq!(Align::Center(Stack::down(|add|{
////add(&test)?;
////add(&test)
////})).layout(area)?,
////Some([3, 1, 4, 8]));
////assert_eq!(Align::Center(Stack::down(|add|{
////add(&Margin::XY(2, 2, test))?;
////add(&test)
////})).layout(area)?,
////Some([2, 0, 6, 10]));
////assert_eq!(Align::Center(Stack::down(|add|{
////add(&Margin::XY(2, 2, test))?;
////add(&Padding::XY(2, 2, test))
////})).layout(area)?,
////Some([2, 1, 6, 8]));
////assert_eq!(Stack::down(|add|{
////add(&Margin::XY(2, 2, test))?;
////add(&Padding::XY(2, 2, test))
////}).layout(area)?,
////Some([0, 0, 6, 8]));
////assert_eq!(Stack::right(|add|{
////add(&Stack::down(|add|{
////add(&Margin::XY(2, 2, test))?;
////add(&Padding::XY(2, 2, test))
////}))?;
////add(&Align::Center(TestArea(2 ,2)))
////}).layout(area)?,
////Some([0, 0, 8, 8]));
////Ok(())
////}
////#[test]
////fn test_offset () -> Usually<()> {
////let area: [u16;4] = [50, 50, 100, 100];
////let test = TestArea(3, 3);
////assert_eq!(Push::X(1, test).layout(area)?, Some([51, 50, 3, 3]));
////assert_eq!(Push::Y(1, test).layout(area)?, Some([50, 51, 3, 3]));
////assert_eq!(Push::XY(1, 1, test).layout(area)?, Some([51, 51, 3, 3]));
////Ok(())
////}
////#[test]
////fn test_outset () -> Usually<()> {
////let area: [u16;4] = [50, 50, 100, 100];
////let test = TestArea(3, 3);
////assert_eq!(Margin::X(1, test).layout(area)?, Some([49, 50, 5, 3]));
////assert_eq!(Margin::Y(1, test).layout(area)?, Some([50, 49, 3, 5]));
////assert_eq!(Margin::XY(1, 1, test).layout(area)?, Some([49, 49, 5, 5]));
////Ok(())
////}
////#[test]
////fn test_padding () -> Usually<()> {
////let area: [u16;4] = [50, 50, 100, 100];
////let test = TestArea(3, 3);
////assert_eq!(Padding::X(1, test).layout(area)?, Some([51, 50, 1, 3]));
////assert_eq!(Padding::Y(1, test).layout(area)?, Some([50, 51, 3, 1]));
////assert_eq!(Padding::XY(1, 1, test).layout(area)?, Some([51, 51, 1, 1]));
////Ok(())
////}
////#[test]
////fn test_stuff () -> Usually<()> {
////let area: [u16;4] = [0, 0, 100, 100];
////assert_eq!("1".layout(area)?,
////Some([0, 0, 1, 1]));
////assert_eq!("333".layout(area)?,
////Some([0, 0, 3, 1]));
////assert_eq!(Layers::new(|add|{add(&"1")?;add(&"333")}).layout(area)?,
////Some([0, 0, 3, 1]));
////assert_eq!(Stack::down(|add|{add(&"1")?;add(&"333")}).layout(area)?,
////Some([0, 0, 3, 2]));
////assert_eq!(Stack::right(|add|{add(&"1")?;add(&"333")}).layout(area)?,
////Some([0, 0, 4, 1]));
////assert_eq!(Stack::down(|add|{
////add(&Stack::right(|add|{add(&"1")?;add(&"333")}))?;
////add(&"55555")
////}).layout(area)?,
////Some([0, 0, 5, 2]));
////let area: [u16;4] = [1, 1, 100, 100];
////assert_eq!(Margin::X(1, Stack::right(|add|{add(&"1")?;add(&"333")})).layout(area)?,
////Some([0, 1, 6, 1]));
////assert_eq!(Margin::Y(1, Stack::right(|add|{add(&"1")?;add(&"333")})).layout(area)?,
////Some([1, 0, 4, 3]));
////assert_eq!(Margin::XY(1, 1, Stack::right(|add|{add(&"1")?;add(&"333")})).layout(area)?,
////Some([0, 0, 6, 3]));
////assert_eq!(Stack::down(|add|{
////add(&Margin::XY(1, 1, "1"))?;
////add(&Margin::XY(1, 1, "333"))
////}).layout(area)?,
////Some([1, 1, 5, 6]));
////let area: [u16;4] = [1, 1, 95, 100];
////assert_eq!(Align::Center(Stack::down(|add|{
////add(&Margin::XY(1, 1, "1"))?;
////add(&Margin::XY(1, 1, "333"))
////})).layout(area)?,
////Some([46, 48, 5, 6]));
////assert_eq!(Align::Center(Stack::down(|add|{
////add(&Layers::new(|add|{
//////add(&Margin::XY(1, 1, Background(Color::Rgb(0,128,0))))?;
////add(&Margin::XY(1, 1, "1"))?;
////add(&Margin::XY(1, 1, "333"))?;
//////add(&Background(Color::Rgb(0,128,0)))?;
////Ok(())
////}))?;
////add(&Layers::new(|add|{
//////add(&Margin::XY(1, 1, Background(Color::Rgb(0,0,128))))?;
////add(&Margin::XY(1, 1, "555"))?;
////add(&Margin::XY(1, 1, "777777"))?;
//////add(&Background(Color::Rgb(0,0,128)))?;
////Ok(())
////}))
////})).layout(area)?,
////Some([46, 48, 5, 6]));
////Ok(())
////}

275
src/main.rs Normal file
View file

@ -0,0 +1,275 @@
use std::sync::{Arc, RwLock};
use tek::*;
#[allow(unused_imports)] use clap::{self, Parser, Subcommand, ValueEnum};
#[derive(Debug, Parser)]
#[command(version, about, long_about = None)]
pub struct TekCli {
/// Which app to initialize
#[command(subcommand)] mode: TekMode,
/// Name of JACK client
#[arg(short='t', long)] name: Option<String>,
/// Whether to attempt to become transport master
#[arg(short='S', long, default_value_t = false)] sync_lead: bool,
/// Whether to sync to external transport master
#[arg(short='s', long, default_value_t = true)] sync_follow: bool,
/// Initial tempo in beats per minute
#[arg(short='b', long, default_value = None)] bpm: Option<f64>,
}
#[derive(Debug, Clone, Subcommand)]
pub enum TekMode {
/// A standalone transport view.
Clock,
/// A MIDI sequencer.
Sequencer {
/// Whether to include a transport toolbar (default: true)
#[arg(short='t', 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 ins to connect to (multiple instances accepted)
#[arg(short='o', long)] midi_to: Vec<String>,
},
/// A MIDI-controlled audio sampler.
Sampler {
/// MIDI outs to connect to (multiple instances accepted)
#[arg(short='i', long)] midi_from: Vec<String>,
/// Audio outs to connect to left input
#[arg(short='l', long)] l_from: Vec<String>,
/// Audio outs to connect to right input
#[arg(short='r', long)] r_from: Vec<String>,
/// Audio ins to connect from left output
#[arg(short='L', long)] l_to: Vec<String>,
/// Audio ins to connect from right output
#[arg(short='R', long)] r_to: Vec<String>,
},
/// Sequencer and sampler together.
Groovebox {
/// Whether to include a transport toolbar (default: true)
#[arg(short='t', 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 ins to connect to (multiple instances accepted)
#[arg(short='o', long)] midi_to: Vec<String>,
/// Audio outs to connect to left input
#[arg(short='l', long)] l_from: Vec<String>,
/// Audio outs to connect to right input
#[arg(short='r', long)] r_from: Vec<String>,
/// Audio ins to connect from left output
#[arg(short='L', long)] l_to: Vec<String>,
/// Audio ins to connect from right output
#[arg(short='R', long)] r_to: Vec<String>,
},
/// Multi-track MIDI sequencer.
Arranger {
/// Whether to include a transport toolbar (default: true)
#[arg(short='t', 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 ins to connect to (multiple instances accepted)
#[arg(short='o', long)] midi_to: Vec<String>,
/// Audio outs to connect to left input
#[arg(short='l', long)] l_from: Vec<String>,
/// Audio outs to connect to right input
#[arg(short='r', long)] r_from: Vec<String>,
/// Audio ins to connect from left output
#[arg(short='L', long)] l_to: Vec<String>,
/// Audio ins to connect from right output
#[arg(short='R', long)] r_to: Vec<String>,
/// Number of tracks
#[arg(short = 'x', long, default_value_t = 16)] tracks: usize,
/// Width of tracks
#[arg(short = 'w', long, default_value_t = 6)] track_width: usize,
/// Number of scenes
#[arg(short = 'y', long, default_value_t = 8)] scenes: usize,
},
/// TODO: A MIDI-controlled audio mixer
Mixer,
/// TODO: A customizable channel strip
Track,
/// TODO: An audio plugin host
Plugin,
}
/// Application entrypoint.
pub fn main () -> Usually<()> {
let cli = TekCli::parse();
let name = cli.name.as_ref().map_or("tek", |x|x.as_str());
let jack = JackConnection::new(name)?;
let engine = Tui::new()?;
Ok(match cli.mode {
TekMode::Clock => engine.run(&jack.activate_with(|jack|Ok(TransportTui {
clock: Clock::from(jack),
jack: jack.clone()
}))?)?,
TekMode::Sequencer {
midi_from,
midi_to, ..
} => engine.run(&jack.activate_with(|jack|Ok({
let clock = Clock::from(jack);
let phrase = Arc::new(RwLock::new(MidiClip::new(
"Clip", true, 4 * clock.timebase.ppq.get() as usize,
None, Some(ItemColor::random().into())
)));
let midi_in = jack.read().unwrap().register_port("i", MidiIn::default())?;
connect_from(&jack, &midi_in, &midi_from)?;
let midi_out = jack.read().unwrap().register_port("o", MidiOut::default())?;
connect_to(&jack, &midi_out, &midi_to)?;
Sequencer {
_jack: jack.clone(),
pool: PoolModel::from(&phrase),
editor: MidiEditor::from(&phrase),
player: MidiPlayer::new(&clock, &phrase, &[midi_in], &[midi_out])?,
compact: true,
transport: true,
selectors: true,
size: Measure::new(),
midi_buf: vec![vec![];65536],
note_buf: vec![],
perf: PerfModel::default(),
status: true,
clock,
}
}))?)?,
TekMode::Sampler {
midi_from, l_from, r_from, l_to, r_to, ..
} => engine.run(&jack.activate_with(|jack|Ok(
SamplerTui {
cursor: (0, 0),
editing: None,
mode: None,
size: Measure::new(),
note_lo: 36.into(),
note_pt: 36.into(),
color: ItemPalette::from(Color::Rgb(64, 128, 32)),
state: Sampler::new(jack, &"sampler",
&midi_from,
&[&l_from, &r_from],
&[&l_to, &r_to],
)?,
}
))?)?,
TekMode::Groovebox {
midi_from, midi_to, l_from, r_from, l_to, r_to, ..
} => engine.run(&jack.activate_with(|jack|Ok({
let phrase = Arc::new(RwLock::new(MidiClip::new(
"Clip", true, 4 * player.clock.timebase.ppq.get() as usize,
None, Some(ItemColor::random().into())
)));
let mut player = MidiPlayer::new(jack, &"sequencer", Some(&phrase),
&midi_from,
&midi_to
)?;
player.play_phrase = Some((Moment::zero(&player.clock.timebase), Some(phrase.clone())));
let sampler = Sampler::new(jack, &"sampler",
midi_from,
&[l_from, r_from],
&[l_to, r_to ],
)?;
jack.read().unwrap().client().connect_ports(
&player.midi_outs[0],
&sampler.midi_in
)?;
let app = Groovebox {
player,
sampler,
_jack: jack.clone(),
pool: PoolModel::from(&phrase),
editor: MidiEditor::from(&phrase),
compact: true,
status: true,
size: Measure::new(),
midi_buf: vec![vec![];65536],
note_buf: vec![],
perf: PerfModel::default(),
};
if let Some(bpm) = cli.bpm {
app.clock().timebase.bpm.set(bpm);
}
if cli.sync_lead {
jack.read().unwrap().client().register_timebase_callback(false, |mut state|{
app.clock().playhead.update_from_sample(state.position.frame() as f64);
state.position.bbt = Some(app.clock().bbt());
state.position
})?
} else if cli.sync_follow {
jack.read().unwrap().client().register_timebase_callback(false, |state|{
app.clock().playhead.update_from_sample(state.position.frame() as f64);
state.position
})?
}
app
}))?)?,
TekMode::Arranger {
scenes, tracks, track_width, midi_from, midi_to, ..
} => engine.run(&jack.activate_with(|jack|Ok({
let mut app = Arranger::new(jack);
app.tracks_add(tracks, track_width, midi_from.as_slice(), midi_to.as_slice())?;
app.scenes_add(scenes)?;
app
}))?)?,
_ => todo!()
})
}
#[allow(unused)]
fn connect_from (jack: &JackConnection, input: &Port<MidiIn>, ports: &[String]) -> Usually<()> {
for port in ports.iter() {
if let Some(port) = jack.port_by_name(port).as_ref() {
jack.client().connect_ports(port, input)?;
} else {
panic!("Missing MIDI output: {port}. Use jack_lsp to list all port names.");
}
}
Ok(())
}
#[allow(unused)]
fn connect_to (jack: &JackConnection, output: &Port<MidiOut>, ports: &[String]) -> Usually<()> {
for port in ports.iter() {
if let Some(port) = jack.port_by_name(port).as_ref() {
jack.client().connect_ports(output, port)?;
} else {
panic!("Missing MIDI input: {port}. Use jack_lsp to list all port names.");
}
}
Ok(())
}
#[allow(unused)]
fn connect_audio_from (jack: &JackConnection, input: &Port<AudioIn>, ports: &[String]) -> Usually<()> {
for port in ports.iter() {
if let Some(port) = jack.port_by_name(port).as_ref() {
jack.client().connect_ports(port, input)?;
} else {
panic!("Missing MIDI output: {port}. Use jack_lsp to list all port names.");
}
}
Ok(())
}
#[allow(unused)]
fn connect_audio_to (jack: &JackConnection, output: &Port<AudioOut>, ports: &[String]) -> Usually<()> {
for port in ports.iter() {
if let Some(port) = jack.port_by_name(port).as_ref() {
jack.client().connect_ports(output, port)?;
} else {
panic!("Missing MIDI input: {port}. Use jack_lsp to list all port names.");
}
}
Ok(())
}
#[test] fn verify_cli () {
use clap::CommandFactory;
TekCli::command().debug_assert();
}

View file

@ -1,26 +0,0 @@
use crate::*;
pub struct Meter<'a>(pub &'a str, pub f32);
render!(TuiOut: (self: Meter<'a>) => col!(
Field(TuiTheme::g(128).into(), self.0, format!("{:>+9.3}", self.1)),
Fixed::xy(if self.1 >= 0.0 { 13 }
else if self.1 >= -1.0 { 12 }
else if self.1 >= -2.0 { 11 }
else if self.1 >= -3.0 { 10 }
else if self.1 >= -4.0 { 9 }
else if self.1 >= -6.0 { 8 }
else if self.1 >= -9.0 { 7 }
else if self.1 >= -12.0 { 6 }
else if self.1 >= -15.0 { 5 }
else if self.1 >= -20.0 { 4 }
else if self.1 >= -25.0 { 3 }
else if self.1 >= -30.0 { 2 }
else if self.1 >= -40.0 { 1 }
else { 0 }, 1, Tui::bg(if self.1 >= 0.0 { Color::Red }
else if self.1 >= -3.0 { Color::Yellow }
else { Color::Green }, ()))));
pub struct Meters<'a>(pub &'a[f32]);
render!(TuiOut: (self: Meters<'a>) => col!(
format!("L/{:>+9.3}", self.0[0]),
format!("R/{:>+9.3}", self.0[1])));

View file

@ -1,346 +0,0 @@
use crate::*;
#[derive(Debug)]
pub struct Mixer {
/// JACK client handle (needs to not be dropped for standalone mode to work).
pub jack: Arc<RwLock<JackConnection>>,
pub name: Arc<str>,
pub tracks: Vec<MixerTrack>,
pub selected_track: usize,
pub selected_column: usize,
}
/// A mixer track.
#[derive(Debug)]
pub struct MixerTrack {
pub name: Arc<str>,
/// Inputs of 1st device
pub audio_ins: Vec<Port<AudioIn>>,
/// Outputs of last device
pub audio_outs: Vec<Port<AudioOut>>,
/// Device chain
pub devices: Vec<Box<dyn MixerTrackDevice>>,
}
audio!(|self: Mixer, _client, _scope|Control::Continue);
impl Mixer {
pub fn new (jack: &Arc<RwLock<JackConnection>>, name: &str) -> Usually<Self> {
Ok(Self {
jack: jack.clone(),
name: name.into(),
selected_column: 0,
selected_track: 1,
tracks: vec![],
})
}
pub fn track_add (&mut self, name: &str, channels: usize) -> Usually<&mut Self> {
let track = MixerTrack::new(name)?;
self.tracks.push(track);
Ok(self)
}
pub fn track (&self) -> Option<&MixerTrack> {
self.tracks.get(self.selected_track)
}
}
impl MixerTrack {
pub fn new (name: &str) -> Usually<Self> {
Ok(Self {
name: name.to_string().into(),
audio_ins: vec![],
audio_outs: vec![],
devices: vec![],
//ports: JackPorts::default(),
//devices: vec![],
//device: 0,
})
}
//fn get_device_mut (&self, i: usize) -> Option<RwLockWriteGuard<Box<dyn AudioComponent<E>>>> {
//self.devices.get(i).map(|d|d.state.write().unwrap())
//}
//pub fn device_mut (&self) -> Option<RwLockWriteGuard<Box<dyn AudioComponent<E>>>> {
//self.get_device_mut(self.device)
//}
///// Add a device to the end of the chain.
//pub fn append_device (&mut self, device: JackDevice<E>) -> Usually<&mut JackDevice<E>> {
//self.devices.push(device);
//let index = self.devices.len() - 1;
//Ok(&mut self.devices[index])
//}
//pub fn add_device (&mut self, device: JackDevice<E>) {
//self.devices.push(device);
//}
//pub fn connect_first_device (&self) -> Usually<()> {
//if let (Some(port), Some(device)) = (&self.midi_out, self.devices.get(0)) {
//device.client.as_client().connect_ports(&port, &device.midi_ins()?[0])?;
//}
//Ok(())
//}
//pub fn connect_last_device (&self, app: &Track) -> Usually<()> {
//Ok(match self.devices.get(self.devices.len().saturating_sub(1)) {
//Some(device) => {
//app.audio_out(0).map(|left|device.connect_audio_out(0, &left)).transpose()?;
//app.audio_out(1).map(|right|device.connect_audio_out(1, &right)).transpose()?;
//()
//},
//None => ()
//})
//}
}
pub struct TrackView<'a> {
pub chain: Option<&'a MixerTrack>,
pub direction: Direction,
pub focused: bool,
pub entered: bool,
}
impl<'a> Content<TuiOut> for TrackView<'a> {
fn render (&self, to: &mut TuiOut) {
todo!();
//let mut area = to.area();
//if let Some(chain) = self.chain {
//match self.direction {
//Direction::Down => area.width = area.width.min(40),
//Direction::Right => area.width = area.width.min(10),
//_ => { unimplemented!() },
//}
//to.fill_bg(to.area(), Nord::bg_lo(self.focused, self.entered));
//let mut split = Stack::new(self.direction);
//for device in chain.devices.as_slice().iter() {
//split = split.add_ref(device);
//}
//let (area, areas) = split.render_areas(to)?;
//if self.focused && self.entered && areas.len() > 0 {
//Corners(Style::default().green().not_dim()).draw(to.with_rect(areas[0]))?;
//}
//Ok(Some(area))
//} else {
//let [x, y, width, height] = area;
//let label = "No chain selected";
//let x = x + (width - label.len() as u16) / 2;
//let y = y + height / 2;
//to.blit(&label, x, y, Some(Style::default().dim().bold()))?;
//Ok(Some(area))
//}
}
}
//impl Content<TuiOut> for Mixer<Tui> {
//fn content (&self) -> impl Content<TuiOut> {
//Stack::right(|add| {
//for channel in self.tracks.iter() {
//add(channel)?;
//}
//Ok(())
//})
//}
//}
//impl Content<TuiOut> for Track<Tui> {
//fn content (&self) -> impl Content<TuiOut> {
//TrackView {
//chain: Some(&self),
//direction: tek_core::Direction::Right,
//focused: true,
//entered: true,
////pub channels: u8,
////pub input_ports: Vec<Port<AudioIn>>,
////pub pre_gain_meter: f64,
////pub gain: f64,
////pub insert_ports: Vec<Port<AudioOut>>,
////pub return_ports: Vec<Port<AudioIn>>,
////pub post_gain_meter: f64,
////pub post_insert_meter: f64,
////pub level: f64,
////pub pan: f64,
////pub output_ports: Vec<Port<AudioOut>>,
////pub post_fader_meter: f64,
////pub route: String,
//}
//}
//}
handle!(TuiIn: |self: Mixer, engine|{
if let crossterm::event::Event::Key(event) = engine.event() {
match event.code {
//KeyCode::Char('c') => {
//if event.modifiers == KeyModifiers::CONTROL {
//self.exit();
//}
//},
KeyCode::Down => {
self.selected_track = (self.selected_track + 1) % self.tracks.len();
println!("{}", self.selected_track);
return Ok(Some(true))
},
KeyCode::Up => {
if self.selected_track == 0 {
self.selected_track = self.tracks.len() - 1;
} else {
self.selected_track -= 1;
}
println!("{}", self.selected_track);
return Ok(Some(true))
},
KeyCode::Left => {
if self.selected_column == 0 {
self.selected_column = 6
} else {
self.selected_column -= 1;
}
return Ok(Some(true))
},
KeyCode::Right => {
if self.selected_column == 6 {
self.selected_column = 0
} else {
self.selected_column += 1;
}
return Ok(Some(true))
},
_ => {
println!("\n{event:?}");
}
}
}
Ok(None)
});
handle!(TuiIn: |self:MixerTrack,from|{
match from.event() {
//, NONE, "chain_cursor_up", "move cursor up", || {
kpat!(KeyCode::Up) => {
Ok(Some(true))
},
// , NONE, "chain_cursor_down", "move cursor down", || {
kpat!(KeyCode::Down) => {
Ok(Some(true))
},
// Left, NONE, "chain_cursor_left", "move cursor left", || {
kpat!(KeyCode::Left) => {
//if let Some(track) = app.arranger.track_mut() {
//track.device = track.device.saturating_sub(1);
//return Ok(true)
//}
Ok(Some(true))
},
// , NONE, "chain_cursor_right", "move cursor right", || {
kpat!(KeyCode::Right) => {
//if let Some(track) = app.arranger.track_mut() {
//track.device = (track.device + 1).min(track.devices.len().saturating_sub(1));
//return Ok(true)
//}
Ok(Some(true))
},
// , NONE, "chain_mode_switch", "switch the display mode", || {
kpat!(KeyCode::Char('`')) => {
//app.chain_mode = !app.chain_mode;
Ok(Some(true))
},
_ => Ok(None)
}
});
pub enum MixerTrackCommand {}
//impl MixerTrackDevice for LV2Plugin {}
pub trait MixerTrackDevice: Debug + Send + Sync {
fn boxed (self) -> Box<dyn MixerTrackDevice> where Self: Sized + 'static {
Box::new(self)
}
}
impl MixerTrackDevice for Sampler {}
impl MixerTrackDevice for Plugin {}
const SYM_NAME: &str = ":name";
const SYM_GAIN: &str = ":gain";
const SYM_SAMPLER: &str = "sampler";
const SYM_LV2: &str = "lv2";
from_edn!("mixer/track" => |jack: &Arc<RwLock<JackConnection>>, args| -> MixerTrack {
let mut _gain = 0.0f64;
let mut track = MixerTrack {
name: "".into(),
audio_ins: vec![],
audio_outs: vec![],
devices: vec![],
};
edn!(edn in args {
Edn::Map(map) => {
if let Some(Edn::Str(n)) = map.get(&Edn::Key(SYM_NAME)) {
track.name = n.to_string();
}
if let Some(Edn::Double(g)) = map.get(&Edn::Key(SYM_GAIN)) {
_gain = f64::from(*g);
}
},
Edn::List(args) => match args.first() {
// Add a sampler device to the track
Some(Edn::Symbol(SYM_SAMPLER)) => {
track.devices.push(
Box::new(Sampler::from_edn(jack, &args[1..])?) as Box<dyn MixerTrackDevice>
);
panic!(
"unsupported in track {}: {:?}; tek_mixer not compiled with feature \"sampler\"",
&track.name,
args.first().unwrap()
)
},
// Add a LV2 plugin to the track.
Some(Edn::Symbol(SYM_LV2)) => {
track.devices.push(
Box::new(Plugin::from_edn(jack, &args[1..])?) as Box<dyn MixerTrackDevice>
);
panic!(
"unsupported in track {}: {:?}; tek_mixer not compiled with feature \"plugin\"",
&track.name,
args.first().unwrap()
)
},
None =>
panic!("empty list track {}", &track.name),
_ =>
panic!("unexpected in track {}: {:?}", &track.name, args.first().unwrap())
},
_ => {}
});
Ok(track)
});
//impl ArrangerScene {
////TODO
////pub fn from_edn <'a, 'e> (args: &[Edn<'e>]) -> Usually<Self> {
////let mut name = None;
////let mut clips = vec![];
////edn!(edn in args {
////Edn::Map(map) => {
////let key = map.get(&Edn::Key(":name"));
////if let Some(Edn::Str(n)) = key {
////name = Some(*n);
////} else {
////panic!("unexpected key in scene '{name:?}': {key:?}")
////}
////},
////Edn::Symbol("_") => {
////clips.push(None);
////},
////Edn::Int(i) => {
////clips.push(Some(*i as usize));
////},
////_ => panic!("unexpected in scene '{name:?}': {edn:?}")
////});
////Ok(ArrangerScene {
////name: Arc::new(name.unwrap_or("").to_string().into()),
////color: ItemColor::random(),
////clips,
////})
////}
//}

View file

@ -1,278 +0,0 @@
use crate::*;
pub mod lv2; pub(crate) use lv2::*;
pub use self::lv2::LV2Plugin;
/// A plugin device.
#[derive(Debug)]
pub struct Plugin {
/// JACK client handle (needs to not be dropped for standalone mode to work).
pub jack: Arc<RwLock<JackConnection>>,
pub name: Arc<str>,
pub path: Option<Arc<str>>,
pub plugin: Option<PluginKind>,
pub selected: usize,
pub mapping: bool,
pub midi_ins: Vec<Port<MidiIn>>,
pub midi_outs: Vec<Port<MidiOut>>,
pub audio_ins: Vec<Port<AudioIn>>,
pub audio_outs: Vec<Port<AudioOut>>,
}
/// Supported plugin formats.
#[derive(Default)]
pub enum PluginKind {
#[default] None,
LV2(LV2Plugin),
VST2 { instance: () /*::vst::host::PluginInstance*/ },
VST3,
}
impl Debug for PluginKind {
fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
write!(f, "{}", match self {
Self::None => "(none)",
Self::LV2(_) => "LV2",
Self::VST2{..} => "VST2",
Self::VST3 => "VST3",
})
}
}
impl Plugin {
pub fn new_lv2 (
jack: &Arc<RwLock<JackConnection>>,
name: &str,
path: &str,
) -> Usually<Self> {
Ok(Self {
jack: jack.clone(),
name: name.into(),
path: Some(String::from(path).into()),
plugin: Some(PluginKind::LV2(LV2Plugin::new(path)?)),
selected: 0,
mapping: false,
midi_ins: vec![],
midi_outs: vec![],
audio_ins: vec![],
audio_outs: vec![],
})
}
}
pub struct PluginAudio(Arc<RwLock<Plugin>>);
from!(|model: &Arc<RwLock<Plugin>>| PluginAudio = Self(model.clone()));
audio!(|self: PluginAudio, client, scope|{
let state = &mut*self.0.write().unwrap();
match state.plugin.as_mut() {
Some(PluginKind::LV2(LV2Plugin {
features,
ref mut instance,
ref mut input_buffer,
..
})) => {
let urid = features.midi_urid();
input_buffer.clear();
for port in state.midi_ins.iter() {
let mut atom = ::livi::event::LV2AtomSequence::new(
&features,
scope.n_frames() as usize
);
for event in port.iter(scope) {
match event.bytes.len() {
3 => atom.push_midi_event::<3>(
event.time as i64,
urid,
&event.bytes[0..3]
).unwrap(),
_ => {}
}
}
input_buffer.push(atom);
}
let mut outputs = vec![];
for _ in state.midi_outs.iter() {
outputs.push(::livi::event::LV2AtomSequence::new(
features,
scope.n_frames() as usize
));
}
let ports = ::livi::EmptyPortConnections::new()
.with_atom_sequence_inputs(input_buffer.iter())
.with_atom_sequence_outputs(outputs.iter_mut())
.with_audio_inputs(state.audio_ins.iter().map(|o|o.as_slice(scope)))
.with_audio_outputs(state.audio_outs.iter_mut().map(|o|o.as_mut_slice(scope)));
unsafe {
instance.run(scope.n_frames() as usize, ports).unwrap()
};
},
_ => todo!("only lv2 is supported")
}
Control::Continue
});
//fn jack_from_lv2 (name: &str, plugin: &::livi::Plugin) -> Usually<Jack> {
//let counts = plugin.port_counts();
//let mut jack = Jack::new(name)?;
//for i in 0..counts.atom_sequence_inputs {
//jack = jack.midi_in(&format!("midi-in-{i}"))
//}
//for i in 0..counts.atom_sequence_outputs {
//jack = jack.midi_out(&format!("midi-out-{i}"));
//}
//for i in 0..counts.audio_inputs {
//jack = jack.audio_in(&format!("audio-in-{i}"));
//}
//for i in 0..counts.audio_outputs {
//jack = jack.audio_out(&format!("audio-out-{i}"));
//}
//Ok(jack)
//}
impl Plugin {
/// Create a plugin host device.
pub fn new (
jack: &Arc<RwLock<JackConnection>>,
name: &str,
) -> Usually<Self> {
Ok(Self {
//_engine: Default::default(),
jack: jack.clone(),
name: name.into(),
path: None,
plugin: None,
selected: 0,
mapping: false,
audio_ins: vec![],
audio_outs: vec![],
midi_ins: vec![],
midi_outs: vec![],
//ports: JackPorts::default()
})
}
}
impl Content<TuiOut> for Plugin {
fn render (&self, to: &mut TuiOut) {
let area = to.area();
let [x, y, _, height] = area;
let mut width = 20u16;
match &self.plugin {
Some(PluginKind::LV2(LV2Plugin { port_list, instance, .. })) => {
let start = self.selected.saturating_sub((height as usize / 2).saturating_sub(1));
let end = start + height as usize - 2;
//draw_box(buf, Rect { x, y, width, height });
for i in start..end {
if let Some(port) = port_list.get(i) {
let value = if let Some(value) = instance.control_input(port.index) {
value
} else {
port.default_value
};
//let label = &format!("C·· M·· {:25} = {value:.03}", port.name);
let label = &format!("{:25} = {value:.03}", port.name);
width = width.max(label.len() as u16 + 4);
let style = if i == self.selected {
Some(Style::default().green())
} else {
None
} ;
to.blit(&label, x + 2, y + 1 + i as u16 - start as u16, style);
} else {
break
}
}
},
_ => {}
};
draw_header(self, to, x, y, width);
}
}
fn draw_header (state: &Plugin, 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 })
}
handle!(TuiIn: |self:Plugin, from|{
match from.event() {
kpat!(KeyCode::Up) => {
self.selected = self.selected.saturating_sub(1);
Ok(Some(true))
},
kpat!(KeyCode::Down) => {
self.selected = (self.selected + 1).min(match &self.plugin {
Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1,
_ => unimplemented!()
});
Ok(Some(true))
},
kpat!(KeyCode::PageUp) => {
self.selected = self.selected.saturating_sub(8);
Ok(Some(true))
},
kpat!(KeyCode::PageDown) => {
self.selected = (self.selected + 10).min(match &self.plugin {
Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1,
_ => unimplemented!()
});
Ok(Some(true))
},
kpat!(KeyCode::Char(',')) => {
match self.plugin.as_mut() {
Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => {
let index = port_list[self.selected].index;
if let Some(value) = instance.control_input(index) {
instance.set_control_input(index, value - 0.01);
}
},
_ => {}
}
Ok(Some(true))
},
kpat!(KeyCode::Char('.')) => {
match self.plugin.as_mut() {
Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => {
let index = port_list[self.selected].index;
if let Some(value) = instance.control_input(index) {
instance.set_control_input(index, value + 0.01);
}
},
_ => {}
}
Ok(Some(true))
},
kpat!(KeyCode::Char('g')) => {
match self.plugin {
//Some(PluginKind::LV2(ref mut plugin)) => {
//plugin.ui_thread = Some(run_lv2_ui(LV2PluginUI::new()?)?);
//},
Some(_) => unreachable!(),
None => {}
}
Ok(Some(true))
},
_ => Ok(None)
}
});
from_edn!("plugin/lv2" => |jack: &Arc<RwLock<JackConnection>>, args| -> Plugin {
let mut name = String::new();
let mut path = String::new();
edn!(edn in args {
Edn::Map(map) => {
if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) {
name = String::from(*n);
}
if let Some(Edn::Str(p)) = map.get(&Edn::Key(":path")) {
path = String::from(*p);
}
},
_ => panic!("unexpected in lv2 '{name}'"),
});
Plugin::new_lv2(jack, &name, &path)
});

View file

@ -1,40 +0,0 @@
use crate::*;
/// A LV2 plugin.
#[derive(Debug)]
pub struct LV2Plugin {
pub world: livi::World,
pub instance: livi::Instance,
pub plugin: livi::Plugin,
pub features: Arc<livi::Features>,
pub port_list: Vec<livi::Port>,
pub input_buffer: Vec<livi::event::LV2AtomSequence>,
pub ui_thread: Option<JoinHandle<()>>,
}
impl LV2Plugin {
const INPUT_BUFFER: usize = 1024;
pub fn new (uri: &str) -> Usually<Self> {
let world = livi::World::with_load_bundle(&uri);
let features = world
.build_features(livi::FeaturesBuilder {
min_block_length: 1,
max_block_length: 65536,
});
let plugin = world.iter_plugins().nth(0)
.unwrap_or_else(||panic!("plugin not found: {uri}"));
Ok(Self {
instance: unsafe {
plugin
.instantiate(features.clone(), 48000.0)
.expect(&format!("instantiate failed: {uri}"))
},
port_list: plugin.ports().collect::<Vec<_>>(),
input_buffer: Vec::with_capacity(Self::INPUT_BUFFER),
ui_thread: None,
world,
features,
plugin,
})
}
}

View file

@ -1,59 +0,0 @@
use crate::*;
use std::thread::{spawn, JoinHandle};
use ::winit::{
application::ApplicationHandler,
event::WindowEvent,
event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
window::{Window, WindowId},
platform::x11::EventLoopBuilderExtX11
};
//pub struct LV2PluginUI {
//write: (),
//controller: (),
//widget: (),
//features: (),
//transfer: (),
//}
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()
}))
}
/// A LV2 plugin's X11 UI.
pub struct LV2PluginUI {
pub window: Option<Window>
}
impl LV2PluginUI {
pub fn new () -> Usually<Self> {
Ok(Self { window: None })
}
}
impl ApplicationHandler for LV2PluginUI {
fn resumed (&mut self, event_loop: &ActiveEventLoop) {
self.window = Some(event_loop.create_window(Window::default_attributes()).unwrap());
}
fn window_event (&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) {
match event {
WindowEvent::CloseRequested => {
self.window.as_ref().unwrap().set_visible(false);
event_loop.exit();
},
WindowEvent::RedrawRequested => {
self.window.as_ref().unwrap().request_redraw();
}
_ => (),
}
}
}
fn lv2_ui_instantiate (kind: &str) {
//let host = Suil
}

View file

@ -1,47 +0,0 @@
use super::*;
use ::livi::{
World,
Instance,
Plugin as LiviPlugin,
Features,
FeaturesBuilder,
Port,
event::LV2AtomSequence,
};
use std::thread::JoinHandle;
/// A LV2 plugin.
pub struct LV2Plugin {
pub world: World,
pub instance: Instance,
pub plugin: LiviPlugin,
pub features: Arc<Features>,
pub port_list: Vec<Port>,
pub input_buffer: Vec<LV2AtomSequence>,
pub ui_thread: Option<JoinHandle<()>>,
}
impl LV2Plugin {
const INPUT_BUFFER: usize = 1024;
pub fn new (uri: &str) -> Usually<Self> {
// Get 1st plugin at URI
let world = World::with_load_bundle(&uri);
let features = FeaturesBuilder { min_block_length: 1, max_block_length: 65536 };
let features = world.build_features(features);
let mut plugin = None;
if let Some(p) = world.iter_plugins().next() { plugin = Some(p); }
let plugin = plugin.expect("plugin not found");
let err = &format!("init {uri}");
let instance = unsafe { plugin.instantiate(features.clone(), 48000.0).expect(&err) };
let mut port_list = vec![];
for port in plugin.ports() {
port_list.push(port);
}
let input_buffer = Vec::with_capacity(Self::INPUT_BUFFER);
// Instantiate
Ok(Self {
world, instance, port_list, plugin, features, input_buffer, ui_thread: None
})
}
}

View file

@ -1,14 +0,0 @@
use crate::*;
impl<E: Engine> ::vst::host::Host for Plugin<E> {}
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()?
})
}

View file

@ -1,2 +0,0 @@
//! TODO

View file

@ -1,267 +0,0 @@
mod pool_tui; pub use self::pool_tui::*;
mod clip_length; pub use self::clip_length::*;
mod clip_rename; pub use self::clip_rename::*;
mod clip_select; pub use self::clip_select::*;
use super::*;
#[derive(Debug)]
pub struct PoolModel {
pub(crate) visible: bool,
/// Collection of phrases
pub(crate) phrases: Vec<Arc<RwLock<MidiClip>>>,
/// Selected phrase
pub(crate) phrase: AtomicUsize,
/// Mode switch
pub(crate) mode: Option<PoolMode>,
/// Rendered size
size: Measure<TuiOut>,
/// Scroll offset
scroll: usize,
}
/// Modes for phrase pool
#[derive(Debug, Clone)]
pub enum PoolMode {
/// Renaming a pattern
Rename(usize, Arc<str>),
/// Editing the length of a pattern
Length(usize, usize, PhraseLengthFocus),
/// Load phrase from disk
Import(usize, FileBrowser),
/// Save phrase to disk
Export(usize, FileBrowser),
}
#[derive(Clone, PartialEq, Debug)]
pub enum PoolCommand {
Show(bool),
/// Update the contents of the phrase pool
Phrase(MidiPoolCommand),
/// Select a phrase from the phrase pool
Select(usize),
/// Rename a phrase
Rename(PhraseRenameCommand),
/// Change the length of a phrase
Length(PhraseLengthCommand),
/// Import from file
Import(FileBrowserCommand),
/// Export to file
Export(FileBrowserCommand),
}
command!(|self:PoolCommand, state: PoolModel|{
use PoolCommand::*;
match self {
Show(visible) => {
state.visible = visible;
Some(Self::Show(!visible))
}
Rename(command) => match command {
PhraseRenameCommand::Begin => {
let length = state.phrases()[state.phrase_index()].read().unwrap().length;
*state.phrases_mode_mut() = Some(
PoolMode::Length(state.phrase_index(), length, PhraseLengthFocus::Bar)
);
None
},
_ => command.execute(state)?.map(Rename)
},
Length(command) => match command {
PhraseLengthCommand::Begin => {
let name = state.phrases()[state.phrase_index()].read().unwrap().name.clone();
*state.phrases_mode_mut() = Some(
PoolMode::Rename(state.phrase_index(), name)
);
None
},
_ => command.execute(state)?.map(Length)
},
Import(command) => match command {
FileBrowserCommand::Begin => {
*state.phrases_mode_mut() = Some(
PoolMode::Import(state.phrase_index(), FileBrowser::new(None)?)
);
None
},
_ => command.execute(state)?.map(Import)
},
Export(command) => match command {
FileBrowserCommand::Begin => {
*state.phrases_mode_mut() = Some(
PoolMode::Export(state.phrase_index(), FileBrowser::new(None)?)
);
None
},
_ => command.execute(state)?.map(Export)
},
Select(phrase) => {
state.set_phrase_index(phrase);
None
},
Phrase(command) => command.execute(state)?.map(Phrase),
}
});
input_to_command!(PoolCommand: |state: PoolModel, input: Event|match state.phrases_mode() {
Some(PoolMode::Rename(..)) => Self::Rename(PhraseRenameCommand::input_to_command(state, input)?),
Some(PoolMode::Length(..)) => Self::Length(PhraseLengthCommand::input_to_command(state, input)?),
Some(PoolMode::Import(..)) => Self::Import(FileBrowserCommand::input_to_command(state, input)?),
Some(PoolMode::Export(..)) => Self::Export(FileBrowserCommand::input_to_command(state, input)?),
_ => to_phrases_command(state, input)?
});
fn to_phrases_command (state: &PoolModel, input: &Event) -> Option<PoolCommand> {
use KeyCode::{Up, Down, Delete, Char};
use PoolCommand as Cmd;
let index = state.phrase_index();
let count = state.phrases().len();
Some(match input {
kpat!(Char('n')) => Cmd::Rename(PhraseRenameCommand::Begin),
kpat!(Char('t')) => Cmd::Length(PhraseLengthCommand::Begin),
kpat!(Char('m')) => Cmd::Import(FileBrowserCommand::Begin),
kpat!(Char('x')) => Cmd::Export(FileBrowserCommand::Begin),
kpat!(Char('c')) => Cmd::Phrase(MidiPoolCommand::SetColor(index, ItemColor::random())),
kpat!(Char('[')) | kpat!(Up) => Cmd::Select(
index.overflowing_sub(1).0.min(state.phrases().len() - 1)
),
kpat!(Char(']')) | kpat!(Down) => Cmd::Select(
index.saturating_add(1) % state.phrases().len()
),
kpat!(Char('<')) => if index > 1 {
state.set_phrase_index(state.phrase_index().saturating_sub(1));
Cmd::Phrase(MidiPoolCommand::Swap(index - 1, index))
} else {
return None
},
kpat!(Char('>')) => if index < count.saturating_sub(1) {
state.set_phrase_index(state.phrase_index() + 1);
Cmd::Phrase(MidiPoolCommand::Swap(index + 1, index))
} else {
return None
},
kpat!(Delete) => if index > 0 {
state.set_phrase_index(index.min(count.saturating_sub(1)));
Cmd::Phrase(MidiPoolCommand::Delete(index))
} else {
return None
},
kpat!(Char('a')) | kpat!(Shift-Char('A')) => Cmd::Phrase(MidiPoolCommand::Add(count, MidiClip::new(
"Clip", true, 4 * PPQ, None, Some(ItemPalette::random())
))),
kpat!(Char('i')) => Cmd::Phrase(MidiPoolCommand::Add(index + 1, MidiClip::new(
"Clip", true, 4 * PPQ, None, Some(ItemPalette::random())
))),
kpat!(Char('d')) | kpat!(Shift-Char('D')) => {
let mut phrase = state.phrases()[index].read().unwrap().duplicate();
phrase.color = ItemPalette::random_near(phrase.color, 0.25);
Cmd::Phrase(MidiPoolCommand::Add(index + 1, phrase))
},
_ => return None
})
}
impl Default for PoolModel {
fn default () -> Self {
Self {
visible: true,
phrases: vec![RwLock::new(MidiClip::default()).into()],
phrase: 0.into(),
scroll: 0,
mode: None,
size: Measure::new(),
}
}
}
from!(|phrase:&Arc<RwLock<MidiClip>>|PoolModel = {
let mut model = Self::default();
model.phrases.push(phrase.clone());
model.phrase.store(1, Relaxed);
model
});
has_phrases!(|self: PoolModel|self.phrases);
has_phrase!(|self: PoolModel|self.phrases[self.phrase_index()]);
impl PoolModel {
pub(crate) fn phrase_index (&self) -> usize {
self.phrase.load(Relaxed)
}
pub(crate) fn set_phrase_index (&self, value: usize) {
self.phrase.store(value, Relaxed);
}
pub(crate) fn phrases_mode (&self) -> &Option<PoolMode> {
&self.mode
}
pub(crate) fn phrases_mode_mut (&mut self) -> &mut Option<PoolMode> {
&mut self.mode
}
pub fn file_picker (&self) -> Option<&FileBrowser> {
match self.mode {
Some(PoolMode::Import(_, ref file_picker)) => Some(file_picker),
Some(PoolMode::Export(_, ref file_picker)) => Some(file_picker),
_ => None
}
}
}
command!(|self: FileBrowserCommand, state: PoolModel|{
use PoolMode::*;
use FileBrowserCommand::*;
let mode = &mut state.mode;
match mode {
Some(Import(index, ref mut browser)) => match self {
Cancel => { *mode = None; },
Chdir(cwd) => { *mode = Some(Import(*index, FileBrowser::new(Some(cwd))?)); },
Select(index) => { browser.index = index; },
Confirm => if browser.is_file() {
let index = *index;
let path = browser.path();
*mode = None;
MidiPoolCommand::Import(index, path).execute(state)?;
} else if browser.is_dir() {
*mode = Some(Import(*index, browser.chdir()?));
},
_ => todo!(),
},
Some(Export(index, ref mut browser)) => match self {
Cancel => { *mode = None; },
Chdir(cwd) => { *mode = Some(Export(*index, FileBrowser::new(Some(cwd))?)); },
Select(index) => { browser.index = index; },
_ => unreachable!()
},
_ => unreachable!(),
};
None
});
input_to_command!(FileBrowserCommand: |state: PoolModel, input: Event|{
use FileBrowserCommand::*;
use KeyCode::{Up, Down, Left, Right, Enter, Esc, Backspace, Char};
if let Some(PoolMode::Import(_index, browser)) = &state.mode {
match input {
kpat!(Up) => Select(browser.index.overflowing_sub(1).0
.min(browser.len().saturating_sub(1))),
kpat!(Down) => Select(browser.index.saturating_add(1)
% browser.len()),
kpat!(Right) => Chdir(browser.cwd.clone()),
kpat!(Left) => Chdir(browser.cwd.clone()),
kpat!(Enter) => Confirm,
kpat!(Char(_)) => { todo!() },
kpat!(Backspace) => { todo!() },
kpat!(Esc) => Cancel,
_ => return None
}
} else if let Some(PoolMode::Export(_index, browser)) = &state.mode {
match input {
kpat!(Up) => Select(browser.index.overflowing_sub(1).0
.min(browser.len())),
kpat!(Down) => Select(browser.index.saturating_add(1)
% browser.len()),
kpat!(Right) => Chdir(browser.cwd.clone()),
kpat!(Left) => Chdir(browser.cwd.clone()),
kpat!(Enter) => Confirm,
kpat!(Char(_)) => { todo!() },
kpat!(Backspace) => { todo!() },
kpat!(Esc) => Cancel,
_ => return None
}
} else {
unreachable!()
}
});

View file

@ -1,144 +0,0 @@
use crate::*;
use super::*;
use PhraseLengthFocus::*;
use PhraseLengthCommand::*;
use KeyCode::{Up, Down, Left, Right, Enter, Esc};
/// Displays and edits phrase length.
#[derive(Clone)]
pub struct PhraseLength {
/// Pulses per beat (quaver)
pub ppq: usize,
/// Beats per bar
pub bpb: usize,
/// Length of phrase in pulses
pub pulses: usize,
/// Selected subdivision
pub focus: Option<PhraseLengthFocus>,
}
impl PhraseLength {
pub fn new (pulses: usize, focus: Option<PhraseLengthFocus>) -> Self {
Self { ppq: PPQ, bpb: 4, pulses, focus }
}
pub fn bars (&self) -> usize {
self.pulses / (self.bpb * self.ppq)
}
pub fn beats (&self) -> usize {
(self.pulses % (self.bpb * self.ppq)) / self.ppq
}
pub fn ticks (&self) -> usize {
self.pulses % self.ppq
}
pub fn bars_string (&self) -> Arc<str> {
format!("{}", self.bars()).into()
}
pub fn beats_string (&self) -> Arc<str> {
format!("{}", self.beats()).into()
}
pub fn ticks_string (&self) -> Arc<str> {
format!("{:>02}", self.ticks()).into()
}
}
/// Focused field of `PhraseLength`
#[derive(Copy, Clone, Debug)]
pub enum PhraseLengthFocus {
/// Editing the number of bars
Bar,
/// Editing the number of beats
Beat,
/// Editing the number of ticks
Tick,
}
impl PhraseLengthFocus {
pub fn next (&mut self) {
*self = match self {
Self::Bar => Self::Beat,
Self::Beat => Self::Tick,
Self::Tick => Self::Bar,
}
}
pub fn prev (&mut self) {
*self = match self {
Self::Bar => Self::Tick,
Self::Beat => Self::Bar,
Self::Tick => Self::Beat,
}
}
}
render!(TuiOut: (self: PhraseLength) => {
let bars = ||self.bars_string();
let beats = ||self.beats_string();
let ticks = ||self.ticks_string();
match self.focus {
None =>
row!(" ", bars(), ".", beats(), ".", ticks()),
Some(PhraseLengthFocus::Bar) =>
row!("[", bars(), "]", beats(), ".", ticks()),
Some(PhraseLengthFocus::Beat) =>
row!(" ", bars(), "[", beats(), "]", ticks()),
Some(PhraseLengthFocus::Tick) =>
row!(" ", bars(), ".", beats(), "[", ticks()),
}
});
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum PhraseLengthCommand {
Begin,
Cancel,
Set(usize),
Next,
Prev,
Inc,
Dec,
}
command!(|self:PhraseLengthCommand,state:PoolModel|{
match state.phrases_mode_mut().clone() {
Some(PoolMode::Length(phrase, ref mut length, ref mut focus)) => match self {
Cancel => { *state.phrases_mode_mut() = None; },
Prev => { focus.prev() },
Next => { focus.next() },
Inc => match focus {
Bar => { *length += 4 * PPQ },
Beat => { *length += PPQ },
Tick => { *length += 1 },
},
Dec => match focus {
Bar => { *length = length.saturating_sub(4 * PPQ) },
Beat => { *length = length.saturating_sub(PPQ) },
Tick => { *length = length.saturating_sub(1) },
},
Set(length) => {
let mut phrase = state.phrases()[phrase].write().unwrap();
let old_length = phrase.length;
phrase.length = length;
std::mem::drop(phrase);
*state.phrases_mode_mut() = None;
return Ok(Some(Self::Set(old_length)))
},
_ => unreachable!()
},
_ => unreachable!()
};
None
});
input_to_command!(PhraseLengthCommand: |state: PoolModel, input: Event|{
if let Some(PoolMode::Length(_, length, _)) = state.phrases_mode() {
match input {
kpat!(Up) => Self::Inc,
kpat!(Down) => Self::Dec,
kpat!(Right) => Self::Next,
kpat!(Left) => Self::Prev,
kpat!(Enter) => Self::Set(*length),
kpat!(Esc) => Self::Cancel,
_ => return None
}
} else {
unreachable!()
}
});

View file

@ -1,60 +0,0 @@
use crate::*;
use super::*;
#[derive(Clone, Debug, PartialEq)]
pub enum PhraseRenameCommand {
Begin,
Cancel,
Confirm,
Set(Arc<str>),
}
impl Command<PoolModel> for PhraseRenameCommand {
fn execute (self, state: &mut PoolModel) -> Perhaps<Self> {
use PhraseRenameCommand::*;
match state.phrases_mode_mut().clone() {
Some(PoolMode::Rename(phrase, ref mut old_name)) => match self {
Set(s) => {
state.phrases()[phrase].write().unwrap().name = s;
return Ok(Some(Self::Set(old_name.clone().into())))
},
Confirm => {
let old_name = old_name.clone();
*state.phrases_mode_mut() = None;
return Ok(Some(Self::Set(old_name)))
},
Cancel => {
state.phrases()[phrase].write().unwrap().name = old_name.clone().into();
},
_ => unreachable!()
},
_ => unreachable!()
};
Ok(None)
}
}
impl InputToCommand<Event, PoolModel> for PhraseRenameCommand {
fn input_to_command (state: &PoolModel, input: &Event) -> Option<Self> {
use KeyCode::{Char, Backspace, Enter, Esc};
if let Some(PoolMode::Rename(_, ref old_name)) = state.phrases_mode() {
Some(match input {
kpat!(Char(c)) => {
let mut new_name = old_name.clone().to_string();
new_name.push(*c);
Self::Set(new_name.into())
},
kpat!(Backspace) => {
let mut new_name = old_name.clone().to_string();
new_name.pop();
Self::Set(new_name.into())
},
kpat!(Enter) => Self::Confirm,
kpat!(Esc) => Self::Cancel,
_ => return None
})
} else {
unreachable!()
}
}
}

View file

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

View file

@ -1,24 +0,0 @@
use crate::*;
pub struct PoolView<'a>(pub bool, pub &'a PoolModel);
render!(TuiOut: (self: PoolView<'a>) => {
let Self(compact, model) = self;
let PoolModel { phrases, mode, .. } = self.1;
let color = self.1.phrase().read().unwrap().color;
Bsp::b(Repeat(" "), Tui::bg(color.darkest.rgb, Outer(
Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)
).enclose(Map::new(||model.phrases().iter(), |clip, i|{
let item_height = 1;
let item_offset = i as u16 * item_height;
let selected = i == model.phrase_index();
let MidiClip { ref name, color, length, .. } = *clip.read().unwrap();
let name = if *compact { format!(" {i:>3}") } else { format!(" {i:>3} {name}") };
let length = if *compact { String::default() } else { format!("{length} ") };
map_south(item_offset, item_height, Tui::bg(if selected { color.light.rgb } else { color.base.rgb }, lay!(
Align::w(Tui::fg(color.lightest.rgb, Tui::bold(selected, name))),
Align::e(Tui::fg(color.lightest.rgb, Tui::bold(selected, length))),
Align::w(When(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "")))),
Align::e(When(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "")))),
)))
}))))
});

View file

@ -1,156 +0,0 @@
mod sample; pub use self::sample::*;
mod sample_import; pub use self::sample_import::*;
mod sample_list; pub use self::sample_list::*;
mod sample_viewer; pub use self::sample_viewer::*;
mod sampler_audio; pub use self::sampler_audio::*;
mod sampler_command; pub use self::sampler_command::*;
mod sampler_status; pub use self::sampler_status::*;
mod sampler_tui; pub use self::sampler_tui::*;
mod voice; pub use self::voice::*;
use crate::*;
use KeyCode::Char;
use std::fs::File;
use symphonia::{
core::{
formats::Packet,
codecs::{Decoder, CODEC_TYPE_NULL},
errors::Error,
io::MediaSourceStream,
probe::Hint,
audio::SampleBuffer,
},
default::get_codecs,
};
/// The sampler plugin plays sounds.
#[derive(Debug)]
pub struct Sampler {
pub jack: Arc<RwLock<JackConnection>>,
pub name: String,
pub mapped: [Option<Arc<RwLock<Sample>>>;128],
pub recording: Option<(usize, Arc<RwLock<Sample>>)>,
pub unmapped: Vec<Arc<RwLock<Sample>>>,
pub voices: Arc<RwLock<Vec<Voice>>>,
pub midi_in: Port<MidiIn>,
pub audio_ins: Vec<Port<AudioIn>>,
pub input_meter: Vec<f32>,
pub audio_outs: Vec<Port<AudioOut>>,
pub buffer: Vec<Vec<f32>>,
pub output_gain: f32
}
impl Sampler {
pub fn new (
jack: &Arc<RwLock<JackConnection>>,
name: impl AsRef<str>,
midi_from: &[impl AsRef<str>],
audio_from: &[&[impl AsRef<str>];2],
audio_to: &[&[impl AsRef<str>];2],
) -> Usually<Self> {
let name = name.as_ref();
Ok(Self {
midi_in: jack.midi_in(&format!("M/{name}"), midi_from)?,
audio_ins: vec![
jack.audio_in(&format!("L/{name}"), audio_from[0])?,
jack.audio_in(&format!("R/{name}"), audio_from[1])?
],
input_meter: vec![0.0;2],
audio_outs: vec![
jack.audio_out(&format!("{name}/L"), audio_to[0])?,
jack.audio_out(&format!("{name}/R"), audio_to[1])?,
],
jack: jack.clone(),
name: name.into(),
mapped: [const { None };128],
unmapped: vec![],
voices: Arc::new(RwLock::new(vec![])),
buffer: vec![vec![0.0;16384];2],
output_gain: 1.,
recording: None,
})
}
pub fn cancel_recording (&mut self) {
self.recording = None;
}
pub fn begin_recording (&mut self, index: usize) {
self.recording = Some((
index,
Arc::new(RwLock::new(Sample::new("Sample", 0, 0, vec![vec![];self.audio_ins.len()])))
));
}
pub fn finish_recording (&mut self) -> Option<Arc<RwLock<Sample>>> {
let recording = self.recording.take();
if let Some((index, sample)) = recording {
let old = self.mapped[index].clone();
self.mapped[index] = Some(sample);
old
} else {
None
}
}
}
from_edn!("sampler" => |jack: &Arc<RwLock<JackConnection>>, args| -> crate::Sampler {
let mut name = String::new();
let mut dir = String::new();
let mut samples = BTreeMap::new();
edn!(edn in args {
Edn::Map(map) => {
if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) {
name = String::from(*n);
}
if let Some(Edn::Str(n)) = map.get(&Edn::Key(":dir")) {
dir = String::from(*n);
}
},
Edn::List(args) => match args.first() {
Some(Edn::Symbol("sample")) => {
let (midi, sample) = MidiSample::from_edn((jack, &dir), &args[1..])?;
if let Some(midi) = midi {
samples.insert(midi, sample);
} else {
panic!("sample without midi binding: {}", sample.read().unwrap().name);
}
},
_ => panic!("unexpected in sampler {name}: {args:?}")
},
_ => panic!("unexpected in sampler {name}: {edn:?}")
});
Self::new(jack, &name)
});
type MidiSample = (Option<u7>, Arc<RwLock<crate::Sample>>);
from_edn!("sample" => |(_jack, dir): (&Arc<RwLock<JackConnection>>, &str), args| -> MidiSample {
let mut name = String::new();
let mut file = String::new();
let mut midi = None;
let mut start = 0usize;
edn!(edn in args {
Edn::Map(map) => {
if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) {
name = String::from(*n);
}
if let Some(Edn::Str(f)) = map.get(&Edn::Key(":file")) {
file = String::from(*f);
}
if let Some(Edn::Int(i)) = map.get(&Edn::Key(":start")) {
start = *i as usize;
}
if let Some(Edn::Int(m)) = map.get(&Edn::Key(":midi")) {
midi = Some(u7::from(*m as u8));
}
},
_ => panic!("unexpected in sample {name}"),
});
let (end, data) = Sample::read_data(&format!("{dir}/{file}"))?;
Ok((midi, Arc::new(RwLock::new(crate::Sample {
name,
start,
end,
channels: data,
rate: None,
gain: 1.0
}))))
});

View file

@ -1,143 +0,0 @@
use crate::*;
use super::*;
/// A sound sample.
#[derive(Default, Debug)]
pub struct Sample {
pub name: Arc<str>,
pub start: usize,
pub end: usize,
pub channels: Vec<Vec<f32>>,
pub rate: Option<usize>,
pub gain: f32,
}
/// Load sample from WAV and assign to MIDI note.
#[macro_export] macro_rules! sample {
($note:expr, $name:expr, $src:expr) => {{
let (end, data) = read_sample_data($src)?;
(
u7::from_int_lossy($note).into(),
Sample::new($name, 0, end, data).into()
)
}};
}
impl Sample {
pub fn new (name: impl AsRef<str>, start: usize, end: usize, channels: Vec<Vec<f32>>) -> Self {
Self { name: name.as_ref().into(), start, end, channels, rate: None, gain: 1.0 }
}
pub fn play (sample: &Arc<RwLock<Self>>, after: usize, velocity: &u7) -> Voice {
Voice {
sample: sample.clone(),
after,
position: sample.read().unwrap().start,
velocity: velocity.as_int() as f32 / 127.0,
}
}
/// Read WAV from file
pub fn read_data (src: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
let mut channels: Vec<wavers::Samples<f32>> = vec![];
for channel in wavers::Wav::from_path(src)?.channels() {
channels.push(channel);
}
let mut end = 0;
let mut data: Vec<Vec<f32>> = vec![];
for samples in channels.iter() {
let channel = Vec::from(samples.as_ref());
end = end.max(channel.len());
data.push(channel);
}
Ok((end, data))
}
pub fn from_file (path: &PathBuf) -> Usually<Self> {
let name = path.file_name().unwrap().to_string_lossy().into();
let mut sample = Self { name, ..Default::default() };
// Use file extension if present
let mut hint = Hint::new();
if let Some(ext) = path.extension() {
hint.with_extension(&ext.to_string_lossy());
}
let probed = symphonia::default::get_probe().format(
&hint,
MediaSourceStream::new(
Box::new(File::open(path)?),
Default::default(),
),
&Default::default(),
&Default::default()
)?;
let mut format = probed.format;
let params = &format.tracks().iter()
.find(|t| t.codec_params.codec != CODEC_TYPE_NULL)
.expect("no tracks found")
.codec_params;
let mut decoder = get_codecs().make(params, &Default::default())?;
loop {
match format.next_packet() {
Ok(packet) => sample.decode_packet(&mut decoder, packet)?,
Err(symphonia::core::errors::Error::IoError(_)) => break decoder.last_decoded(),
Err(err) => return Err(err.into()),
};
};
sample.end = sample.channels.iter().fold(0, |l, c|l + c.len());
Ok(sample)
}
fn decode_packet (
&mut self, decoder: &mut Box<dyn Decoder>, packet: Packet
) -> Usually<()> {
// Decode a packet
let decoded = decoder
.decode(&packet)
.map_err(|e|Box::<dyn crate::Error>::from(e))?;
// Determine sample rate
let spec = *decoded.spec();
if let Some(rate) = self.rate {
if rate != spec.rate as usize {
panic!("sample rate changed");
}
} else {
self.rate = Some(spec.rate as usize);
}
// Determine channel count
while self.channels.len() < spec.channels.count() {
self.channels.push(vec![]);
}
// Load sample
let mut samples = SampleBuffer::new(
decoded.frames() as u64,
spec
);
if samples.capacity() > 0 {
samples.copy_interleaved_ref(decoded);
for frame in samples.samples().chunks(spec.channels.count()) {
for (chan, frame) in frame.iter().enumerate() {
self.channels[chan].push(*frame)
}
}
}
Ok(())
}
pub fn handle_cc (&mut self, controller: u7, value: u7) {
let percentage = value.as_int() as f64 / 127.;
match controller.as_int() {
20 => {
self.start = (percentage * self.end as f64) as usize;
},
21 => {
let length = self.channels[0].len();
self.end = length.min(
self.start + (percentage * (length as f64 - self.start as f64)) as usize
);
},
22 => { /*attack*/ },
23 => { /*decay*/ },
24 => {
self.gain = percentage as f32 * 2.0;
},
26 => { /* pan */ }
25 => { /* pitch */ }
_ => {}
}
}
}

View file

@ -1,242 +0,0 @@
use crate::*;
use super::*;
input_to_command!(FileBrowserCommand: |state:SamplerTui, input: Event|match input {
_ => return None
});
command!(|self:FileBrowserCommand,state:SamplerTui|match self {
_ => todo!()
});
pub struct AddSampleModal {
exited: bool,
dir: PathBuf,
subdirs: Vec<OsString>,
files: Vec<OsString>,
cursor: usize,
offset: usize,
sample: Arc<RwLock<Sample>>,
voices: Arc<RwLock<Vec<Voice>>>,
_search: Option<String>,
}
impl AddSampleModal {
fn exited (&self) -> bool {
self.exited
}
fn exit (&mut self) {
self.exited = true
}
}
impl AddSampleModal {
pub fn new (
sample: &Arc<RwLock<Sample>>,
voices: &Arc<RwLock<Vec<Voice>>>
) -> Usually<Self> {
let dir = std::env::current_dir()?;
let (subdirs, files) = scan(&dir)?;
Ok(Self {
exited: false,
dir,
subdirs,
files,
cursor: 0,
offset: 0,
sample: sample.clone(),
voices: voices.clone(),
_search: None
})
}
fn rescan (&mut self) -> Usually<()> {
scan(&self.dir).map(|(subdirs, files)|{
self.subdirs = subdirs;
self.files = files;
})
}
fn prev (&mut self) {
self.cursor = self.cursor.saturating_sub(1);
}
fn next (&mut self) {
self.cursor = self.cursor + 1;
}
fn try_preview (&mut self) -> Usually<()> {
if let Some(path) = self.cursor_file() {
if let Ok(sample) = Sample::from_file(&path) {
*self.sample.write().unwrap() = sample;
self.voices.write().unwrap().push(
Sample::play(&self.sample, 0, &u7::from(100u8))
);
}
//load_sample(&path)?;
//let src = std::fs::File::open(&path)?;
//let mss = MediaSourceStream::new(Box::new(src), Default::default());
//let mut hint = Hint::new();
//if let Some(ext) = path.extension() {
//hint.with_extension(&ext.to_string_lossy());
//}
//let meta_opts: MetadataOptions = Default::default();
//let fmt_opts: FormatOptions = Default::default();
//if let Ok(mut probed) = symphonia::default::get_probe()
//.format(&hint, mss, &fmt_opts, &meta_opts)
//{
//panic!("{:?}", probed.format.metadata());
//};
}
Ok(())
}
fn cursor_dir (&self) -> Option<PathBuf> {
if self.cursor < self.subdirs.len() {
Some(self.dir.join(&self.subdirs[self.cursor]))
} else {
None
}
}
fn cursor_file (&self) -> Option<PathBuf> {
if self.cursor < self.subdirs.len() {
return None
}
let index = self.cursor.saturating_sub(self.subdirs.len());
if index < self.files.len() {
Some(self.dir.join(&self.files[index]))
} else {
None
}
}
fn pick (&mut self) -> Usually<bool> {
if self.cursor == 0 {
if let Some(parent) = self.dir.parent() {
self.dir = parent.into();
self.rescan()?;
self.cursor = 0;
return Ok(false)
}
}
if let Some(dir) = self.cursor_dir() {
self.dir = dir;
self.rescan()?;
self.cursor = 0;
return Ok(false)
}
if let Some(path) = self.cursor_file() {
let (end, channels) = read_sample_data(&path.to_string_lossy())?;
let mut sample = self.sample.write().unwrap();
sample.name = path.file_name().unwrap().to_string_lossy().into();
sample.end = end;
sample.channels = channels;
return Ok(true)
}
return Ok(false)
}
}
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))
}
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)
}
impl Content<TuiOut> for AddSampleModal {
fn render (&self, to: &mut TuiOut) {
todo!()
//let area = to.area();
//to.make_dim();
//let area = center_box(
//area,
//64.max(area.w().saturating_sub(8)),
//20.max(area.w().saturating_sub(8)),
//);
//to.fill_fg(area, Color::Reset);
//to.fill_bg(area, Nord::bg_lo(true, true));
//to.fill_char(area, ' ');
//to.blit(&format!("{}", &self.dir.to_string_lossy()), area.x()+2, area.y()+1, Some(Style::default().bold()))?;
//to.blit(&"Select sample:", area.x()+2, area.y()+2, Some(Style::default().bold()))?;
//for (i, (is_dir, name)) in self.subdirs.iter()
//.map(|path|(true, path))
//.chain(self.files.iter().map(|path|(false, path)))
//.enumerate()
//.skip(self.offset)
//{
//if i >= area.h() as usize - 4 {
//break
//}
//let t = if is_dir { "" } else { "" };
//let line = format!("{t} {}", name.to_string_lossy());
//let line = &line[..line.len().min(area.w() as usize - 4)];
//to.blit(&line, area.x() + 2, area.y() + 3 + i as u16, Some(if i == self.cursor {
//Style::default().green()
//} else {
//Style::default().white()
//}))?;
//}
//Lozenge(Style::default()).draw(to)
}
}
//impl Handle<TuiIn> for AddSampleModal {
//fn handle (&mut self, from: &TuiIn) -> Perhaps<bool> {
//if from.handle_keymap(self, KEYMAP_ADD_SAMPLE)? {
//return Ok(Some(true))
//}
//Ok(Some(true))
//}
//}
//pub const KEYMAP_ADD_SAMPLE: &'static [KeyBinding<AddSampleModal>] = keymap!(AddSampleModal {
//[Esc, NONE, "sampler/add/close", "close help dialog", |modal: &mut AddSampleModal|{
//modal.exit();
//Ok(true)
//}],
//[Up, NONE, "sampler/add/prev", "select previous entry", |modal: &mut AddSampleModal|{
//modal.prev();
//Ok(true)
//}],
//[Down, NONE, "sampler/add/next", "select next entry", |modal: &mut AddSampleModal|{
//modal.next();
//Ok(true)
//}],
//[Enter, NONE, "sampler/add/enter", "activate selected entry", |modal: &mut AddSampleModal|{
//if modal.pick()? {
//modal.exit();
//}
//Ok(true)
//}],
//[Char('p'), NONE, "sampler/add/preview", "preview selected entry", |modal: &mut AddSampleModal|{
//modal.try_preview()?;
//Ok(true)
//}]
//});

View file

@ -1,50 +0,0 @@
use crate::*;
pub struct SampleList<'a> {
compact: bool,
sampler: &'a Sampler,
editor: &'a MidiEditor
}
impl<'a> SampleList<'a> {
pub fn new (compact: bool, sampler: &'a Sampler, editor: &'a MidiEditor) -> Self {
Self { compact, sampler, editor }
}
}
render!(TuiOut: (self: SampleList<'a>) => {
let Self { compact, sampler, editor } = self;
let note_lo = editor.note_lo().load(Relaxed);
let note_pt = editor.note_point();
let note_hi = editor.note_hi();
Outer(Style::default().fg(TuiTheme::g(96))).enclose(Map::new(move||(note_lo..=note_hi).rev(), move|note, i| {
let offset = |a|Push::y(i as u16, Align::n(Fixed::y(1, Fill::x(a))));
let mut bg = if note == note_pt { TuiTheme::g(64) } else { Color::Reset };
let mut fg = TuiTheme::g(160);
if sampler.mapped[note].is_some() {
fg = TuiTheme::g(224);
bg = Color::Rgb(0, if note == note_pt { 96 } else { 64 }, 0);
}
if let Some((index, _)) = sampler.recording {
if note == index {
bg = if note == note_pt { Color::Rgb(96,24,0) } else { Color::Rgb(64,16,0) };
fg = Color::Rgb(224,64,32)
}
}
let label = if *compact {
String::default()
} else if let Some(sample) = &sampler.mapped[note] {
let sample = sample.read().unwrap();
format!("{:8} {:3} {:6}-{:6}/{:6}",
sample.name,
sample.gain,
sample.start,
sample.end,
sample.channels[0].len()
)
} else {
String::from("(none)")
};
offset(Tui::fg_bg(fg, bg, format!("{note:3} {}", label)))
}))
});

View file

@ -1,68 +0,0 @@
use crate::*;
use std::ops::Deref;
use ratatui::{prelude::Rect, widgets::{Widget, canvas::{Canvas, Points, Line}}};
const EMPTY: &[(f64, f64)] = &[(0., 0.), (1., 1.), (2., 2.), (0., 2.), (2., 0.)];
pub struct SampleViewer(pub Option<Arc<RwLock<Sample>>>);
impl SampleViewer {
pub fn from_sampler (sampler: &Sampler, note_pt: usize) -> Self {
if let Some((_, sample)) = &sampler.recording {
SampleViewer(Some(sample.clone()))
} else if let Some(sample) = &sampler.mapped[note_pt] {
SampleViewer(Some(sample.clone()))
} else {
SampleViewer(None)
}
}
}
render!(TuiOut: |self: SampleViewer, to|{
let [x, y, width, height] = to.area();
let area = Rect { x, y, width, height };
let min_db = -40.0;
let (x_bounds, y_bounds, lines): ([f64;2], [f64;2], Vec<Line>) =
if let Some(sample) = &self.0 {
let sample = sample.read().unwrap();
let start = sample.start as f64;
let end = sample.end as f64;
let length = end - start;
let step = length / width as f64;
let mut t = start;
let mut lines = vec![];
while t < end {
let chunk = &sample.channels[0][t as usize..((t + step) as usize).min(sample.end)];
let total: f32 = chunk.iter().map(|x|x.abs()).sum();
let count = chunk.len() as f32;
let meter = 10. * (total / count).log10();
let x = t as f64;
let y = meter as f64;
lines.push(Line::new(x, min_db, x, y, Color::Green));
t += step / 2.;
}
(
[sample.start as f64, sample.end as f64],
[min_db, 0.],
lines
)
} else {
(
[0.0, width as f64],
[0.0, height as f64],
vec![
Line::new(0.0, 0.0, width as f64, height as f64, Color::Red),
Line::new(width as f64, 0.0, 0.0, height as f64, Color::Red),
]
)
};
Canvas::default()
.x_bounds(x_bounds)
.y_bounds(y_bounds)
.paint(|ctx| { for line in lines.iter() { ctx.draw(line) } })
.render(area, &mut to.buffer);
});

View file

@ -1,111 +0,0 @@
use crate::*;
audio!(|self: SamplerTui, client, scope|{
SamplerAudio(&mut self.state).process(client, scope)
});
pub struct SamplerAudio<'a>(pub &'a mut Sampler);
audio!(|self: SamplerAudio<'a>, _client, scope|{
self.0.process_midi_in(scope);
self.0.clear_output_buffer();
self.0.process_audio_out(scope);
self.0.write_output_buffer(scope);
self.0.process_audio_in(scope);
Control::Continue
});
impl Sampler {
pub fn process_audio_in (&mut self, scope: &ProcessScope) {
let Sampler { audio_ins, input_meter, recording, .. } = self;
if audio_ins.len() != input_meter.len() {
*input_meter = vec![0.0;audio_ins.len()];
}
if let Some((_, sample)) = recording {
let mut sample = sample.write().unwrap();
if sample.channels.len() != audio_ins.len() {
panic!("channel count mismatch");
}
let iterator = audio_ins.iter().zip(input_meter).zip(sample.channels.iter_mut());
let mut length = 0;
for ((input, meter), channel) in iterator {
let slice = input.as_slice(scope);
length = length.max(slice.len());
let total: f32 = slice.iter().map(|x|x.abs()).sum();
let count = slice.len() as f32;
*meter = 10. * (total / count).log10();
channel.extend_from_slice(slice);
}
sample.end += length;
} else {
for (input, meter) in audio_ins.iter().zip(input_meter) {
let slice = input.as_slice(scope);
let total: f32 = slice.iter().map(|x|x.abs()).sum();
let count = slice.len() as f32;
*meter = 10. * (total / count).log10();
}
}
}
/// Create [Voice]s from [Sample]s in response to MIDI input.
pub fn process_midi_in (&mut self, scope: &ProcessScope) {
let Sampler { midi_in, mapped, voices, .. } = self;
for RawMidi { time, bytes } in midi_in.iter(scope) {
if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() {
match message {
MidiMessage::NoteOn { ref key, ref vel } => {
if let Some(ref sample) = mapped[key.as_int() as usize] {
voices.write().unwrap().push(Sample::play(sample, time as usize, vel));
}
},
MidiMessage::Controller { controller, value } => {
// TODO
}
_ => {}
}
}
}
}
/// Zero the output buffer.
pub fn clear_output_buffer (&mut self) {
for buffer in self.buffer.iter_mut() {
buffer.fill(0.0);
}
}
/// Mix all currently playing samples into the output.
pub fn process_audio_out (&mut self, scope: &ProcessScope) {
let Sampler { ref mut buffer, voices, output_gain, .. } = self;
let channel_count = buffer.len();
voices.write().unwrap().retain_mut(|voice|{
for index in 0..scope.n_frames() as usize {
if let Some(frame) = voice.next() {
for (channel, sample) in frame.iter().enumerate() {
// Averaging mixer:
//self.buffer[channel % channel_count][index] = (
//(self.buffer[channel % channel_count][index] + sample * self.output_gain) / 2.0
//);
buffer[channel % channel_count][index] += sample * *output_gain;
}
} else {
return false
}
}
true
});
}
/// Write output buffer to output ports.
pub fn write_output_buffer (&mut self, scope: &ProcessScope) {
let Sampler { ref mut audio_outs, buffer, .. } = self;
for (i, port) in audio_outs.iter_mut().enumerate() {
let buffer = &buffer[i];
for (i, value) in port.as_mut_slice(scope).iter_mut().enumerate() {
*value = *buffer.get(i).unwrap_or(&0.0);
}
}
}
}

View file

@ -1,71 +0,0 @@
use crate::*;
handle!(TuiIn: |self: SamplerTui, input|SamplerTuiCommand::execute_with_state(self, input.event()));
pub enum SamplerTuiCommand {
Import(FileBrowserCommand),
Select(usize),
Sample(SamplerCommand),
}
pub enum SamplerCommand {
RecordBegin(u7),
RecordCancel,
RecordFinish,
SetSample(u7, Option<Arc<RwLock<Sample>>>),
SetStart(u7, usize),
SetGain(f32),
NoteOn(u7, u7),
NoteOff(u7),
}
input_to_command!(SamplerTuiCommand: |state: SamplerTui, input: Event|match state.mode{
Some(SamplerMode::Import(..)) => Self::Import(
FileBrowserCommand::input_to_command(state, input)?
),
_ => match input {
// load sample
kpat!(Shift-Char('L')) => Self::Import(FileBrowserCommand::Begin),
kpat!(KeyCode::Up) => Self::Select(state.note_point().overflowing_add(1).0.min(127)),
kpat!(KeyCode::Down) => Self::Select(state.note_point().overflowing_sub(1).0.min(127)),
_ => return None
}
});
command!(|self: SamplerTuiCommand, state: SamplerTui|match self {
Self::Import(FileBrowserCommand::Begin) => {
let voices = &state.state.voices;
let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![])));
state.mode = Some(SamplerMode::Import(0, FileBrowser::new(None)?));
None
},
Self::Select(index) => {
let old = state.note_point();
state.set_note_point(index);
Some(Self::Select(old))
},
Self::Sample(cmd) => cmd.execute(&mut state.state)?.map(Self::Sample),
_ => todo!()
});
command!(|self: SamplerCommand, state: Sampler|match self {
Self::SetSample(index, sample) => {
let i = index.as_int() as usize;
let old = state.mapped[i].clone();
state.mapped[i] = sample;
Some(Self::SetSample(index, old))
},
Self::RecordBegin(index) => {
state.begin_recording(index.as_int() as usize);
None
},
Self::RecordCancel => {
state.cancel_recording();
None
},
Self::RecordFinish => {
state.finish_recording();
None
},
_ => todo!()
});

View file

@ -1,9 +0,0 @@
use crate::*;
pub struct SamplerStatus<'a>(pub &'a Sampler, pub usize);
render!(TuiOut: (self: SamplerStatus<'a>) => Tui::bold(true, Tui::fg(TuiTheme::g(224), self.0.mapped[self.1].as_ref().map(|sample|format!(
"Sample {}-{}",
sample.read().unwrap().start,
sample.read().unwrap().end,
)).unwrap_or_else(||"No sample".to_string()))));

View file

@ -1,91 +0,0 @@
use crate::*;
use KeyCode::Char;
pub struct SamplerTui {
pub state: Sampler,
pub cursor: (usize, usize),
pub editing: Option<Arc<RwLock<Sample>>>,
pub mode: Option<SamplerMode>,
/// Size of actual notes area
pub size: Measure<TuiOut>,
/// Lowest note displayed
pub note_lo: AtomicUsize,
pub note_pt: AtomicUsize,
pub color: ItemPalette
}
impl SamplerTui {
/// Immutable reference to sample at cursor.
pub fn sample (&self) -> Option<&Arc<RwLock<Sample>>> {
for (i, sample) in self.state.mapped.iter().enumerate() {
if i == self.cursor.0 {
return sample.as_ref()
}
}
for (i, sample) in self.state.unmapped.iter().enumerate() {
if i + self.state.mapped.len() == self.cursor.0 {
return Some(sample)
}
}
None
}
}
render!(TuiOut: (self: SamplerTui) => {
let keys_width = 5;
let keys = move||"";//SamplerKeys(self);
let fg = self.color.base.rgb;
let bg = self.color.darkest.rgb;
let border = Fill::xy(Outer(Style::default().fg(fg).bg(bg)));
let with_border = |x|lay!(border, Fill::xy(x));
let with_size = |x|lay!(self.size.clone(), x);
Tui::bg(bg, Fill::xy(with_border(Bsp::s(
Tui::fg(self.color.light.rgb, Tui::bold(true, &"Sampler")),
with_size(Shrink::y(1, Bsp::e(
Fixed::x(keys_width, keys()),
Fill::xy(SamplesTui {
color: self.color,
note_hi: self.note_hi(),
note_pt: self.note_point(),
height: self.size.h(),
}),
))),
))))
});
struct SamplesTui {
color: ItemPalette,
note_hi: usize,
note_pt: usize,
height: usize,
}
render!(TuiOut: |self: SamplesTui, render|{
let x = render.area.x();
let bg_base = self.color.darkest.rgb;
let bg_selected = self.color.darker.rgb;
let style_empty = Style::default().fg(self.color.base.rgb);
let style_full = Style::default().fg(self.color.lighter.rgb);
for y in 0..self.height {
let note = self.note_hi - y as usize;
let bg = if note == self.note_pt { bg_selected } else { bg_base };
let style = Some(style_empty.bg(bg));
render.blit(&" (no sample) ", x, render.area.y() + y as u16, style);
}
});
impl NoteRange for SamplerTui {
fn note_lo (&self) -> &AtomicUsize { &self.note_lo }
fn note_axis (&self) -> &AtomicUsize { &self.size.y }
}
impl NotePoint for SamplerTui {
fn note_len (&self) -> usize {0/*TODO*/}
fn set_note_len (&self, x: usize) {}
fn note_point (&self) -> usize { self.note_pt.load(Relaxed) }
fn set_note_point (&self, x: usize) { self.note_pt.store(x, Relaxed); }
}
pub enum SamplerMode {
// Load sample from path
Import(usize, FileBrowser),
}

View file

@ -1,30 +0,0 @@
use crate::*;
/// A currently playing instance of a sample.
#[derive(Default, Debug, Clone)]
pub struct Voice {
pub sample: Arc<RwLock<Sample>>,
pub after: usize,
pub position: usize,
pub velocity: f32,
}
impl Iterator for Voice {
type Item = [f32;2];
fn next (&mut self) -> Option<Self::Item> {
if self.after > 0 {
self.after -= 1;
return Some([0.0, 0.0])
}
let sample = self.sample.read().unwrap();
if self.position < sample.end {
let position = self.position;
self.position += 1;
return sample.channels[0].get(position).map(|_amplitude|[
sample.channels[0][position] * self.velocity * sample.gain,
sample.channels[0][position] * self.velocity * sample.gain,
])
}
None
}
}

View file

@ -1,208 +0,0 @@
use crate::*;
use ClockCommand::{Play, Pause};
use KeyCode::{Tab, Char};
use SequencerCommand as Cmd;
use MidiEditCommand::*;
use MidiPoolCommand::*;
/// Root view for standalone `tek_sequencer`.
pub struct Sequencer {
pub _jack: Arc<RwLock<JackConnection>>,
pub pool: PoolModel,
pub editor: MidiEditor,
pub player: MidiPlayer,
pub transport: bool,
pub selectors: bool,
pub compact: bool,
pub clock: Clock,
pub size: Measure<TuiOut>,
pub status: bool,
pub note_buf: Vec<u8>,
pub midi_buf: Vec<Vec<Vec<u8>>>,
pub perf: PerfModel,
}
render!(TuiOut: (self: Sequencer) => self.size.of(
Bsp::s(self.toolbar_view(),
Bsp::n(self.selector_view(),
Bsp::n(self.status_view(),
Bsp::w(self.pool_view(), Fill::xy(&self.editor)))))));
impl Sequencer {
fn toolbar_view (&self) -> impl Content<TuiOut> + use<'_> {
Fill::x(Fixed::y(2, Align::x(TransportView::new(true, &self.player.clock))))
}
fn status_view (&self) -> impl Content<TuiOut> + use<'_> {
row!(
When(self.selectors, Bsp::e(
self.player.play_status(),
self.player.next_status(),
)),
self.editor.clip_status(),
self.editor.edit_status(),
)
}
fn selector_view (&self) -> impl Content<TuiOut> + use<'_> {
row!(
self.player.play_status(),
self.player.next_status(),
self.editor.clip_status(),
self.editor.edit_status(),
)
}
fn pool_view (&self) -> impl Content<TuiOut> + use<'_> {
let w = self.size.w();
let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
let pool_w = if self.pool.visible { phrase_w } else { 0 };
let pool = Pull::y(1, Fill::y(Align::e(PoolView(self.pool.visible, &self.pool))));
Fixed::x(pool_w, Align::e(Fill::y(PoolView(self.compact, &self.pool))))
}
fn help () -> impl Content<TuiOut> {
let single = |binding, command|row!(" ", col!(
Tui::fg(TuiTheme::yellow(), binding),
command
));
let double = |(b1, c1), (b2, c2)|col!(
row!(" ", Tui::fg(TuiTheme::yellow(), b1), " ", c1,),
row!(" ", Tui::fg(TuiTheme::yellow(), b2), " ", c2,),
);
Tui::fg_bg(TuiTheme::g(255), TuiTheme::g(50), row!(
single("SPACE", "play/pause"),
double(("▲▼▶◀", "cursor"), ("Ctrl", "scroll"), ),
double(("a", "append"), ("s", "set note"),),
double((",.", "length"), ("<>", "triplet"), ),
double(("[]", "phrase"), ("{}", "order"), ),
double(("q", "enqueue"), ("e", "edit"), ),
double(("c", "color"), ("", ""),),
))
}
}
audio!(|self:Sequencer, client, scope|{
// Start profiling cycle
let t0 = self.perf.get_t0();
// Update transport clock
if Control::Quit == ClockAudio(self).process(client, scope) {
return Control::Quit
}
// Update MIDI sequencer
if Control::Quit == PlayerAudio(
&mut self.player, &mut self.note_buf, &mut self.midi_buf
).process(client, scope) {
return Control::Quit
}
// End profiling cycle
self.perf.update(t0, scope);
Control::Continue
});
has_size!(<TuiOut>|self:Sequencer|&self.size);
has_clock!(|self:Sequencer|&self.clock);
has_phrases!(|self:Sequencer|self.pool.phrases);
has_editor!(|self:Sequencer|self.editor);
handle!(TuiIn: |self:Sequencer,input|SequencerCommand::execute_with_state(self, input.event()));
#[derive(Clone, Debug)] pub enum SequencerCommand {
Compact(bool),
History(isize),
Clock(ClockCommand),
Pool(PoolCommand),
Editor(MidiEditCommand),
Enqueue(Option<Arc<RwLock<MidiClip>>>),
}
keymap!(KEYS_SEQUENCER = |state: Sequencer, input: Event| SequencerCommand {
// TODO: k: toggle on-screen keyboard
ctrl(key(Char('k'))) => { todo!("keyboard") },
// Transport: Play/pause
key(Char(' ')) => Cmd::Clock(if state.clock().is_stopped() { Play(None) } else { Pause(None) }),
// Transport: Play from start or rewind to start
shift(key(Char(' '))) => Cmd::Clock(if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }),
// u: undo
key(Char('u')) => Cmd::History(-1),
// Shift-U: redo
key(Char('U')) => Cmd::History( 1),
// Tab: Toggle compact mode
key(Tab) => Cmd::Compact(!state.compact),
// q: Enqueue currently edited phrase
key(Char('q')) => Cmd::Enqueue(Some(state.pool.phrase().clone())),
// 0: Enqueue phrase 0 (stop all)
key(Char('0')) => Cmd::Enqueue(Some(state.phrases()[0].clone())),
// e: Toggle between editing currently playing or other phrase
key(Char('e')) => if let Some((_, Some(playing))) = state.player.play_phrase() {
let editing = state.editor.phrase().as_ref().map(|p|p.read().unwrap().clone());
let selected = state.pool.phrase().clone();
Cmd::Editor(Show(Some(if Some(selected.read().unwrap().clone()) != editing {
selected
} else {
playing.clone()
})))
} else {
return None
}
}, if let Some(command) = MidiEditCommand::input_to_command(&state.editor, input) {
Cmd::Editor(command)
} else if let Some(command) = PoolCommand::input_to_command(&state.pool, input) {
Cmd::Pool(command)
} else {
return None
});
command!(|self: SequencerCommand, state: Sequencer|match self {
Self::Enqueue(phrase) => {
state.player.enqueue_next(phrase.as_ref());
None
},
Self::Pool(cmd) => match cmd {
// autoselect: automatically load selected phrase in editor
PoolCommand::Select(_) => {
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
state.editor.set_phrase(Some(state.pool.phrase()));
undo
},
// update color in all places simultaneously
PoolCommand::Phrase(SetColor(index, _)) => {
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
state.editor.set_phrase(Some(state.pool.phrase()));
undo
},
_ => cmd.delegate(&mut state.pool, Self::Pool)?
},
Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?,
Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?,
Self::History(delta) => {
todo!("undo/redo")
},
Self::Compact(compact) => if state.compact != compact {
state.compact = compact;
Some(Self::Compact(!compact))
} else {
None
},
});
/// Status bar for sequencer app
#[derive(Clone)]
pub struct SequencerStatus {
pub(crate) width: usize,
pub(crate) cpu: Option<Arc<str>>,
pub(crate) size: Arc<str>,
pub(crate) playing: bool,
}
from!(|state:&Sequencer|SequencerStatus = {
let samples = state.clock.chunk.load(Relaxed);
let rate = state.clock.timebase.sr.get();
let buffer = samples as f64 / rate;
let width = state.size.w();
Self {
width,
playing: state.clock.is_rolling(),
cpu: state.perf.percentage().map(|cpu|format!("{cpu:.01}%").into()),
size: format!("{}x{}│", width, state.size.h()).into(),
}
});
render!(TuiOut: (self: SequencerStatus) => Fixed::y(2, lay!(
Sequencer::help(),
Fill::xy(Align::se(Tui::fg_bg(TuiTheme::orange(), TuiTheme::g(25), self.stats()))),
)));
impl SequencerStatus {
fn stats (&self) -> impl Content<TuiOut> + use<'_> {
row!(&self.cpu, &self.size)
}
}

25
src/todo_cli_mixer.rs Normal file
View file

@ -0,0 +1,25 @@
include!("./lib.rs");
pub fn main () -> Usually<()> {
MixerCli::parse().run()
}
#[derive(Debug, Parser)] #[command(version, about, long_about = None)] pub struct MixerCli {
/// Name of JACK client
#[arg(short, long)] name: Option<String>,
/// Number of tracks
#[arg(short, long)] channels: Option<usize>,
}
impl MixerCli {
fn run (&self) -> Usually<()> {
Tui::run(JackConnection::new("tek_mixer")?.activate_with(|jack|{
let mut mixer = Mixer::new(jack, self.name.as_ref().map(|x|x.as_str()).unwrap_or("mixer"))?;
for channel in 0..self.channels.unwrap_or(8) {
mixer.track_add(&format!("Track {}", channel + 1), 1)?;
}
Ok(mixer)
})?)?;
Ok(())
}
}

26
src/todo_cli_plugin.rs Normal file
View file

@ -0,0 +1,26 @@
include!("./lib.rs");
pub fn main () -> Usually<()> {
PluginCli::parse().run()
}
#[derive(Debug, Parser)] #[command(version, about, long_about = None)] pub struct PluginCli {
/// Name of JACK client
#[arg(short, long)] name: Option<String>,
/// Path to plugin
#[arg(short, long)] path: Option<String>,
}
impl PluginCli {
fn run (&self) -> Usually<()> {
Tui::run(JackConnection::new("tek_plugin")?.activate_with(|jack|{
let mut plugin = Plugin::new_lv2(
jack,
self.name.as_ref().map(|x|x.as_str()).unwrap_or("mixer"),
self.path.as_ref().expect("pass --path /to/lv2/plugin.so")
)?;
Ok(plugin)
})?)?;
Ok(())
}
}

26
src/todo_cli_sampler.rs Normal file
View file

@ -0,0 +1,26 @@
include!("./lib.rs");
pub fn main () -> Usually<()> {
SamplerCli::parse().run()
}
#[derive(Debug, Parser)] #[command(version, about, long_about = None)] pub struct SamplerCli {
/// Name of JACK client
#[arg(short, long)] name: Option<String>,
/// Path to plugin
#[arg(short, long)] path: Option<String>,
}
impl SamplerCli {
fn run (&self) -> Usually<()> {
Tui::run(JackConnection::new("tek_sampler")?.activate_with(|jack|{
let mut plugin = Sampler::new(
jack,
self.name.as_ref().map(|x|x.as_str()).unwrap_or("mixer"),
None,
)?;
Ok(plugin)
})?)?;
Ok(())
}
}