collect crates/ and deps/

This commit is contained in:
🪞👃🪞 2025-04-19 01:23:43 +03:00
parent 2f8882f6cd
commit 8fa0f8a409
140 changed files with 23 additions and 21 deletions

28
crates/app/Cargo.toml Normal file
View file

@ -0,0 +1,28 @@
[package]
name = "tek"
edition = "2021"
version = "0.2.0"
[dependencies]
tengri = { workspace = true }
tek_jack = { workspace = true }
tek_time = { workspace = true }
tek_midi = { workspace = true }
tek_sampler = { workspace = true }
tek_plugin = { workspace = true, optional = true }
backtrace = { workspace = true }
clap = { workspace = true, optional = true }
palette = { workspace = true }
rand = { workspace = true }
toml = { workspace = true }
[dev-dependencies]
proptest = { workspace = true }
proptest-derive = { workspace = true }
[features]
default = ["cli"]
cli = ["clap"]
host = ["tek_plugin"]

View file

View file

View file

@ -0,0 +1,12 @@
(mixer
(track
(name "Drums")
(sampler
(dir "/home/user/Lab/Music/pak")
(sample (midi 34) (name "808 D") (file "808.wav"))))
(track
(name "Lead")
(lv2
(name "Odin2")
(path "file:///home/user/.lv2/Odin2.lv2"))
(gain 0.0)))

View file

View file

@ -0,0 +1,18 @@
(arranger
(track
(name "Drums")
(phrase
(name "4 kicks")
(beats 4)
(steps 16)
(:00 (36 128))
(:04 (36 100))
(:08 (36 100))
(:12 (36 100))))
(track
(name "Bass")
(phrase
(beats 4)
(steps 16)
(:04 (36 100))
(:12 (36 100)))))

201
crates/app/src/api.rs Normal file
View file

@ -0,0 +1,201 @@
use crate::*;
view!(TuiOut: |self: Tek| self.size.of(View(self, self.view)); {
//":inputs" => self.view_inputs().boxed(),
//":outputs" => self.view_outputs().boxed(),
//":scene-add" => self.view_scene_add().boxed(),
//":scenes" => self.view_scenes().boxed(),
//":tracks" => self.view_tracks().boxed(),
":nil" => Box::new("nil"),
":transport" => self.view_transport().boxed(),
":arranger" => ArrangerView::new(self).boxed(),
":editor" => self.editor.as_ref().map(|e|Bsp::e(e.clip_status(), e.edit_status())).boxed(),
":sample" => ().boxed(),//self.view_sample(self.is_editing()).boxed(),
":sampler" => ().boxed(),//self.view_sampler(self.is_editing(), &self.editor).boxed(),
":status" => self.view_status().boxed(),
":pool" => self.pool.as_ref()
.map(|pool|Fixed::x(self.w_sidebar(), PoolView(self.is_editing(), pool)))
.boxed(),
});
expose!([self: Tek] {
[bool] => {}
[u16] => {
":h-ins" => self.h_inputs(),
":h-outs" => self.h_outputs(),
":h-sample" => if self.is_editing() { 0 } else { 5 },
":w-samples" => if self.is_editing() { 4 } else { 11 },
":w-sidebar" => self.w_sidebar(),
":y-ins" => (self.size.h() as u16).saturating_sub(self.h_inputs() + 1),
":y-outs" => (self.size.h() as u16).saturating_sub(self.h_outputs() + 1),
":y-samples" => if self.is_editing() { 1 } else { 0 },
}
[usize] => {
":scene-last" => self.scenes.len(),
":track-last" => self.tracks.len(),
}
[isize] => {}
[Option<usize>] => {
":scene" => self.selected.scene(),
":track" => self.selected.track(),
}
[Color] => {}
[Arc<RwLock<MidiClip>>] => {}
[Option<Arc<RwLock<MidiClip>>>] => {
":clip" => match self.selected {
Selection::Clip(t, s) => self.scenes[s].clips[t].clone(),
_ => None
}
}
[Selection] => {
":scene-next" => match self.selected {
Selection::Mix => Selection::Scene(0),
Selection::Track(t) => Selection::Clip(t, 0),
Selection::Scene(s) if s + 1 < self.scenes.len() => Selection::Scene(s + 1),
Selection::Scene(s) => Selection::Mix,
Selection::Clip(t, s) if s + 1 < self.scenes.len() => Selection::Clip(t, s + 1),
Selection::Clip(t, s) => Selection::Track(t),
},
":scene-prev" => match self.selected {
Selection::Mix => Selection::Mix,
Selection::Track(t) => Selection::Track(t),
Selection::Scene(0) => Selection::Mix,
Selection::Scene(s) => Selection::Scene(s - 1),
Selection::Clip(t, 0) => Selection::Track(t),
Selection::Clip(t, s) => Selection::Clip(t, s - 1),
},
":track-next" => match self.selected {
Selection::Mix => Selection::Track(0),
Selection::Track(t) if t + 1 < self.tracks.len() => Selection::Track(t + 1),
Selection::Track(t) => Selection::Mix,
Selection::Scene(s) => Selection::Clip(0, s),
Selection::Clip(t, s) if t + 1 < self.tracks.len() => Selection::Clip(t + 1, s),
Selection::Clip(t, s) => Selection::Scene(s),
},
":track-prev" => match self.selected {
Selection::Mix => Selection::Mix,
Selection::Scene(s) => Selection::Scene(s),
Selection::Track(0) => Selection::Mix,
Selection::Track(t) => Selection::Track(t - 1),
Selection::Clip(0, s) => Selection::Scene(s),
Selection::Clip(t, s) => Selection::Clip(t - 1, s),
},
}
});
impose!([app: Tek] {
TekCommand => {
("stop" []
Some(Self::StopAll))
("undo" [d: usize]
Some(Self::History(-(d.unwrap_or(0)as isize))))
("redo" [d: usize]
Some(Self::History(d.unwrap_or(0) as isize)))
("zoom" [z: usize]
Some(Self::Zoom(z)))
("edit" []
Some(Self::Edit(None)))
("edit" [c: bool]
Some(Self::Edit(c)))
("color" [c: Color]
Some(Self::Color(ItemPalette::random())))
("color" [c: Color]
Some(Self::Color(c.map(ItemPalette::from).expect("no color"))))
("enqueue" [c: Arc<RwLock<MidiClip>>]
Some(Self::Enqueue(c)))
("launch" []
Some(Self::Launch))
("clip" [,..a]
ClipCommand::try_from_expr(app, a).map(Self::Clip))
("clock" [,..a]
ClockCommand::try_from_expr(app.clock(), a).map(Self::Clock))
("editor" [,..a]
MidiEditCommand::try_from_expr(app.editor.as_ref().expect("no editor"), a).map(Self::Editor))
("pool" [,..a]
PoolCommand::try_from_expr(app.pool.as_ref().expect("no pool"), a).map(Self::Pool))
//("sampler" [,..a]
// Self::Sampler( //SamplerCommand::try_from_expr(app.sampler().as_ref().expect("no sampler"), a).expect("invalid command")))
("scene" [,..a]
SceneCommand::try_from_expr(app, a).map(Self::Scene))
("track" [,..a]
TrackCommand::try_from_expr(app, a).map(Self::Track))
("input" [,..a]
InputCommand::try_from_expr(app, a).map(Self::Input))
("output" [,..a]
OutputCommand::try_from_expr(app, a).map(Self::Output))
("select" [t: Selection]
Some(t.map(Self::Select).expect("no selection")))
("select" [t: usize, s: usize]
Some(match (t.expect("no track"), s.expect("no scene")) {
(0, 0) => Self::Select(Selection::Mix),
(t, 0) => Self::Select(Selection::Track(t)),
(0, s) => Self::Select(Selection::Scene(s)),
(t, s) => Self::Select(Selection::Clip(t, s)),
}))
}
ClipCommand => {
("get" [a: usize, b: usize]
Some(Self::Get(a.unwrap(), b.unwrap())))
("put" [a: usize, b: usize, c: Option<Arc<RwLock<MidiClip>>>]
Some(Self::Put(a.unwrap(), b.unwrap(), c.unwrap())))
("enqueue" [a: usize, b: usize]
Some(Self::Enqueue(a.unwrap(), b.unwrap())))
("edit" [a: Option<Arc<RwLock<MidiClip>>>]
Some(Self::Edit(a.unwrap())))
("loop" [a: usize, b: usize, c: bool]
Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap())))
("color" [a: usize, b: usize]
Some(Self::SetColor(a.unwrap(), b.unwrap(), ItemPalette::random())))
}
InputCommand => {
("add" [] Some(Self::Add))
}
OutputCommand => {
("add" [] Some(Self::Add))
}
SceneCommand => {
("add" []
Some(Self::Add))
("del" [a: usize]
Some(Self::Del(0)))
("zoom" [a: usize]
Some(Self::SetZoom(a.unwrap())))
("color" [a: usize]
Some(Self::SetColor(a.unwrap(), ItemPalette::G[128])))
("enqueue" [a: usize]
Some(Self::Enqueue(a.unwrap())))
("swap" [a: usize, b: usize]
Some(Self::Swap(a.unwrap(), b.unwrap())))
}
TrackCommand => {
("add" []
Some(Self::Add))
("size" [a: usize]
Some(Self::SetSize(a.unwrap())))
("zoom" [a: usize]
Some(Self::SetZoom(a.unwrap())))
("color" [a: usize]
Some(Self::SetColor(a.unwrap(), ItemPalette::random())))
("del" [a: usize]
Some(Self::Del(a.unwrap())))
("stop" [a: usize]
Some(Self::Stop(a.unwrap())))
("swap" [a: usize, b: usize]
Some(Self::Swap(a.unwrap(), b.unwrap())))
("play" []
Some(Self::TogglePlay))
("solo" []
Some(Self::ToggleSolo))
("rec" []
Some(Self::ToggleRecord))
("mon" []
Some(Self::ToggleMonitor))
}
});

95
crates/app/src/audio.rs Normal file
View file

@ -0,0 +1,95 @@
use crate::*;
impl HasJack for Tek { fn jack (&self) -> &Jack { &self.jack } }
audio!(
|self: Tek, client, scope|{
// Start profiling cycle
let t0 = self.perf.get_t0();
// Update transport clock
self.clock().update_from_scope(scope).unwrap();
// Collect MIDI input (TODO preallocate)
let midi_in = self.midi_ins.iter()
.map(|port|port.port().iter(scope)
.map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes)))
.collect::<Vec<_>>())
.collect::<Vec<_>>();
// Update standalone MIDI sequencer
//if let Some(player) = self.player.as_mut() {
//if Control::Quit == PlayerAudio(
//player,
//&mut self.note_buf,
//&mut self.midi_buf,
//).process(client, scope) {
//return Control::Quit
//}
//}
// Update standalone sampler
//if let Some(sampler) = self.sampler.as_mut() {
//if Control::Quit == SamplerAudio(sampler).process(client, scope) {
//return Control::Quit
//}
//for port in midi_in.iter() {
//for message in port.iter() {
//match message {
//Ok(M
//}
//}
//}
//}
// TODO move these to editor and sampler?:
//for port in midi_in.iter() {
//for event in port.iter() {
//match event {
//(time, Ok(LiveEvent::Midi {message, ..})) => match message {
//MidiMessage::NoteOn {ref key, ..} if let Some(editor) = self.editor.as_ref() => {
//editor.set_note_pos(key.as_int() as usize);
//},
//MidiMessage::Controller {controller, value} if let (Some(editor), Some(sampler)) = (
//self.editor.as_ref(),
//self.sampler.as_ref(),
//) => {
//// TODO: give sampler its own cursor
//if let Some(sample) = &sampler.mapped[editor.note_pos()] {
//sample.write().unwrap().handle_cc(*controller, *value)
//}
//}
//_ =>{}
//},
//_ =>{}
//}
//}
//}
// Update track sequencers
for track in self.tracks.iter_mut() {
if PlayerAudio(
track.player_mut(), &mut self.note_buf, &mut self.midi_buf
).process(client, scope) == Control::Quit {
return Control::Quit
}
}
// End profiling cycle
self.perf.update_from_jack_scope(t0, scope);
Control::Continue
};
|self, event|{
use JackEvent::*;
match event {
SampleRate(sr) => { self.clock.timebase.sr.set(sr as f64); },
PortRegistration(id, true) => {
//let port = self.jack().port_by_id(id);
//println!("\rport add: {id} {port:?}");
//println!("\rport add: {id}");
},
PortRegistration(id, false) => {
/*println!("\rport del: {id}")*/
},
PortsConnected(a, b, true) => { /*println!("\rport conn: {a} {b}")*/ },
PortsConnected(a, b, false) => { /*println!("\rport disc: {a} {b}")*/ },
ClientRegistration(id, true) => {},
ClientRegistration(id, false) => {},
ThreadInit => {},
XRun => {},
GraphReorder => {},
_ => { panic!("{event:?}"); }
}
}
);

10
crates/app/src/device.rs Normal file
View file

@ -0,0 +1,10 @@
use crate::*;
pub trait Device: Send + Sync + std::fmt::Debug {
fn boxed <'a> (self) -> Box<dyn Device + 'a> where Self: Sized + 'a { Box::new(self) }
}
impl Device for Sampler {}
#[cfg(feature = "host")]
impl Device for Plugin {}

177
crates/app/src/keys.rs Normal file
View file

@ -0,0 +1,177 @@
use crate::*;
mod keys_clip; pub use self::keys_clip::*;
mod keys_ins; pub use self::keys_ins::*;
mod keys_outs; pub use self::keys_outs::*;
mod keys_scene; pub use self::keys_scene::*;
mod keys_track; pub use self::keys_track::*;
handle!(TuiIn: |self: Tek, input|Ok({
// If editing, editor keys take priority
if self.is_editing() {
if self.editor.handle(input)? == Some(true) {
return Ok(Some(true))
}
}
// Handle from root keymap
if let Some(command) = self.keys.command::<_, TekCommand, _>(self, input) {
if let Some(undo) = command.execute(self)? { self.history.push(undo); }
return Ok(Some(true))
}
// Handle from selection-dependent keymaps
if let Some(command) = match self.selected() {
Selection::Clip(_, _) => self.keys_clip,
Selection::Track(_) => self.keys_track,
Selection::Scene(_) => self.keys_scene,
Selection::Mix => self.keys_mix,
}.command::<_, TekCommand, _>(self, input) {
if let Some(undo) = command.execute(self)? { self.history.push(undo); }
return Ok(Some(true))
}
None
}));
#[derive(Clone, Debug)] pub enum TekCommand {
Clip(ClipCommand),
Clock(ClockCommand),
Color(ItemPalette),
Edit(Option<bool>),
Editor(MidiEditCommand),
Enqueue(Option<Arc<RwLock<MidiClip>>>),
History(isize),
Input(InputCommand),
Launch,
Output(OutputCommand),
Pool(PoolCommand),
Sampler(SamplerCommand),
Scene(SceneCommand),
Select(Selection),
StopAll,
Track(TrackCommand),
Zoom(Option<usize>),
}
command!(|self: TekCommand, app: Tek|match self {
Self::Zoom(_) => { println!("\n\rtodo: global zoom"); None },
Self::History(delta) => { println!("\n\rtodo: undo/redo"); None },
Self::Select(s) => {
app.selected = s;
// autoedit: load focused clip in editor.
if let Some(ref mut editor) = app.editor {
editor.set_clip(match app.selected {
Selection::Clip(t, s) if let Some(Some(Some(clip))) = app
.scenes.get(s).map(|s|s.clips.get(t)) => Some(clip),
_ => None
});
}
None
},
Self::Edit(value) => {
if let Some(value) = value {
if app.is_editing() != value {
app.editing.store(value, Relaxed);
}
} else {
app.editing.store(!app.is_editing(), Relaxed);
};
// autocreate: create new clip from pool when entering empty cell
if let Some(ref pool) = app.pool {
if app.is_editing() {
if let Selection::Clip(t, s) = app.selected {
if let Some(scene) = app.scenes.get_mut(s) {
if let Some(slot) = scene.clips.get_mut(t) {
if slot.is_none() {
let (index, mut clip) = pool.add_new_clip();
// autocolor: new clip colors from scene and track color
clip.write().unwrap().color = ItemColor::random_near(
app.tracks[t].color.base.mix(
scene.color.base,
0.5
),
0.2
).into();
if let Some(ref mut editor) = app.editor {
editor.set_clip(Some(&clip));
}
*slot = Some(clip);
}
}
}
}
}
}
None
},
Self::Clock(cmd) => cmd.delegate(app, Self::Clock)?,
Self::Scene(cmd) => cmd.delegate(app, Self::Scene)?,
Self::Track(cmd) => cmd.delegate(app, Self::Track)?,
Self::Input(cmd) => cmd.delegate(app, Self::Input)?,
Self::Output(cmd) => cmd.delegate(app, Self::Output)?,
Self::Clip(cmd) => cmd.delegate(app, Self::Clip)?,
Self::Editor(cmd) => app.editor.as_mut()
.map(|editor|cmd.delegate(editor, Self::Editor)).transpose()?.flatten(),
//Self::Sampler(cmd) => app.sampler.as_mut()
//.map(|sampler|cmd.delegate(sampler, Self::Sampler)).transpose()?.flatten(),
//Self::Enqueue(clip) => app.player.as_mut()
//.map(|player|{player.enqueue_next(clip.as_ref());None}).flatten(),
Self::Launch => {
use Selection::*;
match app.selected {
Track(t) => app.tracks[t].player.enqueue_next(None),
Clip(t, s) => app.tracks[t].player.enqueue_next(app.scenes[s].clips[t].as_ref()),
Scene(s) => {
for t in 0..app.tracks.len() {
app.tracks[t].player.enqueue_next(app.scenes[s].clips[t].as_ref())
}
},
_ => {}
};
None
},
Self::Color(palette) => {
use Selection::*;
Some(Self::Color(match app.selected {
Mix => {
let old = app.color;
app.color = palette;
old
},
Track(t) => {
let old = app.tracks[t].color;
app.tracks[t].color = palette;
old
}
Scene(s) => {
let old = app.scenes[s].color;
app.scenes[s].color = palette;
old
}
Clip(t, s) => {
if let Some(ref clip) = app.scenes[s].clips[t] {
let mut clip = clip.write().unwrap();
let old = clip.color;
clip.color = palette;
old
} else {
return Ok(None)
}
}
}))
},
Self::StopAll => {
for track in 0..app.tracks.len(){app.tracks[track].player.enqueue_next(None);}
None
},
Self::Pool(cmd) => if let Some(pool) = app.pool.as_mut() {
let undo = cmd.clone().delegate(pool, Self::Pool)?;
if let Some(editor) = app.editor.as_mut() {
match cmd {
// autoselect: automatically load selected clip in editor
// autocolor: update color in all places simultaneously
PoolCommand::Select(_) | PoolCommand::Clip(PoolClipCommand::SetColor(_, _)) =>
editor.set_clip(pool.clip().as_ref()),
_ => {}
}
};
undo
} else {
None
},
_ => todo!("{self:?}")
});

View file

@ -0,0 +1,31 @@
use crate::*;
#[derive(Clone, Debug)] pub enum ClipCommand {
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),
}
command!(|self: ClipCommand, app: Tek|match self {
Self::Get(track, scene) => { todo!() },
Self::Put(track, scene, clip) => {
let old = app.scenes[scene].clips[track].clone();
app.scenes[scene].clips[track] = clip;
Some(Self::Put(track, scene, old))
},
Self::Enqueue(track, scene) => {
app.tracks[track].player.enqueue_next(app.scenes[scene].clips[track].as_ref());
None
},
Self::SetColor(track, scene, color) => {
app.scenes[scene].clips[track].as_ref().map(|clip|{
let mut clip = clip.write().unwrap();
let old = clip.color.clone();
clip.color = color.clone();
panic!("{color:?} {old:?}");
Self::SetColor(track, scene, old)
})
},
_ => None
});

View file

@ -0,0 +1,8 @@
use crate::*;
#[derive(Clone, Debug)] pub enum InputCommand { Add }
command!(|self: InputCommand, app: Tek|match self {
Self::Add => {
app.midi_ins.push(JackMidiIn::new(&app.jack, &format!("M/{}", app.midi_ins.len()), &[])?);
None
},
});

View file

@ -0,0 +1,8 @@
use crate::*;
#[derive(Clone, Debug)] pub enum OutputCommand { Add }
command!(|self: OutputCommand, app: Tek|match self {
Self::Add => {
app.midi_outs.push(JackMidiOut::new(&app.jack, &format!("{}/M", app.midi_outs.len()), &[])?);
None
},
});

View file

@ -0,0 +1,35 @@
use crate::*;
#[derive(Clone, Debug)] pub enum SceneCommand {
Add,
Del(usize),
Swap(usize, usize),
SetSize(usize),
SetZoom(usize),
SetColor(usize, ItemPalette),
Enqueue(usize),
}
command!(|self: SceneCommand, app: Tek|match self {
Self::Add => {
use Selection::*;
let index = app.scene_add(None, None)?.0;
app.selected = match app.selected {
Scene(s) => Scene(index),
Clip(t, s) => Clip(t, index),
_ => app.selected
};
Some(Self::Del(index))
},
Self::Del(index) => { app.scene_del(index); None },
Self::SetColor(index, color) => {
let old = app.scenes[index].color;
app.scenes[index].color = color;
Some(Self::SetColor(index, old))
},
Self::Enqueue(scene) => {
for track in 0..app.tracks.len() {
app.tracks[track].player.enqueue_next(app.scenes[scene].clips[track].as_ref());
}
None
},
_ => None
});

View file

@ -0,0 +1,52 @@
use crate::*;
#[derive(Clone, Debug)] pub enum TrackCommand {
Add,
Del(usize),
Stop(usize),
Swap(usize, usize),
SetSize(usize),
SetZoom(usize),
SetColor(usize, ItemPalette),
TogglePlay,
ToggleSolo,
ToggleRecord,
ToggleMonitor,
}
command!(|self: TrackCommand, app: Tek|match self {
Self::Add => {
use Selection::*;
let index = app.track_add(None, None, &[], &[])?.0;
app.selected = match app.selected {
Track(t) => Track(index),
Clip(t, s) => Clip(index, s),
_ => app.selected
};
Some(Self::Del(index))
},
Self::Del(index) => { app.track_del(index); None },
Self::Stop(track) => { app.tracks[track].player.enqueue_next(None); None },
Self::SetColor(index, color) => {
let old = app.tracks[index].color;
app.tracks[index].color = color;
Some(Self::SetColor(index, old))
},
Self::TogglePlay => {
Some(Self::TogglePlay)
},
Self::ToggleSolo => {
Some(Self::ToggleSolo)
},
Self::ToggleRecord => {
if let Some(t) = app.selected.track() {
app.tracks[t-1].player.recording = !app.tracks[t-1].player.recording;
}
Some(Self::ToggleRecord)
},
Self::ToggleMonitor => {
if let Some(t) = app.selected.track() {
app.tracks[t-1].player.monitoring = !app.tracks[t-1].player.monitoring;
}
Some(Self::ToggleMonitor)
},
_ => None
});

50
crates/app/src/lib.rs Normal file
View file

@ -0,0 +1,50 @@
// ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
//██Let me play the world's tiniest piano for you. ██
//█▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀█
//█▙▙█▙▙▙█▙▙█▙▙▙█▙▙█▙▙▙█▙▙█▙▙▙█▙▙█▙▙▙█▙▙█▙▙▙█▙▙█▙▙▙██
//█▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄█
//███████████████████████████████████████████████████
//█ ▀ ▀ ▀ █
#![allow(unused)]
#![allow(clippy::unit_arg)]
#![feature(adt_const_params)]
#![feature(associated_type_defaults)]
#![feature(if_let_guard)]
#![feature(impl_trait_in_assoc_type)]
#![feature(type_alias_impl_trait)]
#![feature(trait_alias)]
#![feature(type_changing_struct_update)]
/// Standard result type.
pub type Usually<T> = std::result::Result<T, Box<dyn std::error::Error>>;
/// Standard optional result type.
pub type Perhaps<T> = std::result::Result<Option<T>, Box<dyn std::error::Error>>;
pub use ::tek_time::{self, *};
pub use ::tek_jack::{self, *, jack::*};
pub use ::tek_midi::{self, *, midly::{MidiMessage, num::*, live::*}};
pub use ::tek_sampler::{self, *};
#[cfg(feature = "host")] pub use ::tek_plugin::{self, *};
pub use ::tengri::dsl::*;
pub use ::tengri::input::*;
pub use ::tengri::output::*;
pub use ::tengri::tui::*;
pub use ::tengri::tui::ratatui;
pub use ::tengri::tui::ratatui::prelude::buffer::Cell;
pub use ::tengri::tui::ratatui::prelude::Color::{self, *};
pub use ::tengri::tui::ratatui::prelude::{Style, Stylize, Buffer, Modifier};
pub use ::tengri::tui::crossterm;
pub use ::tengri::tui::crossterm::event::{Event, KeyCode::{self, *}};
pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicBool, Ordering::Relaxed}};
mod api; pub use self::api::*;
mod audio; pub use self::audio::*;
mod device; pub use self::device::*;
mod keys; pub use self::keys::*;
mod model; pub use self::model::*;
mod view; pub use self::view::*;
#[cfg(test)] #[test] fn test_model () {
let mut tek = Tek::default();
let _ = tek.clip();
let _ = tek.toggle_loop();
let _ = tek.activate();
}

126
crates/app/src/model.rs Normal file
View file

@ -0,0 +1,126 @@
use crate::*;
mod model_track; pub use self::model_track::*;
mod model_scene; pub use self::model_scene::*;
mod model_select; pub use self::model_select::*;
#[derive(Default, Debug)] pub struct Tek {
/// Must not be dropped for the duration of the process
pub jack: Jack,
/// Source of time
pub clock: Clock,
/// Theme
pub color: ItemPalette,
/// Contains all clips in the project
pub pool: Option<MidiPool>,
/// Contains the currently edited MIDI clip
pub editor: Option<MidiEditor>,
/// Contains a render of the project arrangement, redrawn on update.
pub arranger: Arc<RwLock<Buffer>>,
/// List of global midi inputs
pub midi_ins: Vec<JackMidiIn>,
/// List of global midi outputs
pub midi_outs: Vec<JackMidiOut>,
/// List of global audio inputs
pub audio_ins: Vec<JackAudioIn>,
/// List of global audio outputs
pub audio_outs: Vec<JackAudioOut>,
/// Buffer for writing a midi event
pub note_buf: Vec<u8>,
/// Buffer for writing a chunk of midi events
pub midi_buf: Vec<Vec<Vec<u8>>>,
/// List of tracks
pub tracks: Vec<Track>,
/// Scroll offset of tracks
pub track_scroll: usize,
/// List of scenes
pub scenes: Vec<Scene>,
/// Scroll offset of scenes
pub scene_scroll: usize,
/// Selected UI element
pub selected: Selection,
/// Display size
pub size: Measure<TuiOut>,
/// Performance counter
pub perf: PerfModel,
/// Whether in edit mode
pub editing: AtomicBool,
/// Undo history
pub history: Vec<TekCommand>,
/// Port handles
pub ports: std::collections::BTreeMap<u32, Port<Unowned>>,
/// View definition
pub view: SourceIter<'static>,
// Input definitions
pub keys: SourceIter<'static>,
// Input definitions when a clip is focused
pub keys_clip: SourceIter<'static>,
// Input definitions when a track is focused
pub keys_track: SourceIter<'static>,
// Input definitions when a scene is focused
pub keys_scene: SourceIter<'static>,
// Input definitions when the mix is focused
pub keys_mix: SourceIter<'static>,
// Cache of formatted strings
pub view_cache: Arc<RwLock<ViewCache>>,
}
impl Tek {
pub(crate) fn clip (&self) -> Option<Arc<RwLock<MidiClip>>> {
self.scene()?.clips.get(self.selected().track()?)?.clone()
}
pub(crate) fn toggle_loop (&mut self) {
if let Some(clip) = self.clip() {
clip.write().unwrap().toggle_loop()
}
}
pub(crate) fn activate (&mut self) -> Usually<()> {
let selected = self.selected().clone();
match selected {
Selection::Scene(s) => {
let mut clips = vec![];
for (t, _) in self.tracks().iter().enumerate() {
clips.push(self.scenes()[s].clips[t].clone());
}
for (t, track) in self.tracks_mut().iter_mut().enumerate() {
if track.player.play_clip.is_some() || clips[t].is_some() {
track.player.enqueue_next(clips[t].as_ref());
}
}
if self.clock().is_stopped() {
self.clock().play_from(Some(0))?;
}
},
Selection::Clip(t, s) => {
let clip = self.scenes()[s].clips[t].clone();
self.tracks_mut()[t].player.enqueue_next(clip.as_ref());
},
_ => {}
}
Ok(())
}
}
has_size!(<TuiOut>|self: Tek|&self.size);
has_clock!(|self: Tek|self.clock);
has_clips!(|self: Tek|self.pool.as_ref().expect("no clip pool").clips);
has_editor!(|self: Tek|{
editor = self.editor;
editor_w = {
let size = self.size.w();
let editor = self.editor.as_ref().expect("missing editor");
let time_len = editor.time_len().get();
let time_zoom = editor.time_zoom().get().max(1);
(5 + (time_len / time_zoom)).min(size.saturating_sub(20)).max(16)
};
editor_h = 15;
is_editing = self.editing.load(Relaxed);
});
//has_sampler!(|self: Tek|{
//sampler = self.sampler;
//index = self.editor.as_ref().map(|e|e.note_pos()).unwrap_or(0);
//});

View file

@ -0,0 +1,97 @@
use crate::*;
impl Tek {
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 _ = self.scene_add(None, Some(
scene_color_1.mix(scene_color_2, i as f32 / n as f32).into()
))?;
}
Ok(())
}
pub fn scene_add (&mut self, name: Option<&str>, color: Option<ItemPalette>)
-> Usually<(usize, &mut Scene)>
{
let scene = Scene {
name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()),
clips: vec![None;self.tracks().len()],
color: color.unwrap_or_else(ItemPalette::random),
};
self.scenes_mut().push(scene);
let index = self.scenes().len() - 1;
Ok((index, &mut self.scenes_mut()[index]))
}
pub fn scene_default_name (&self) -> Arc<str> {
format!("Sc{:3>}", self.scenes().len() + 1).into()
}
}
pub trait HasScenes: HasSelection + HasEditor + Send + Sync {
fn scenes (&self) -> &Vec<Scene>;
fn scenes_mut (&mut self) -> &mut Vec<Scene>;
fn scene_longest (&self) -> usize {
self.scenes().iter().map(|s|s.name.len()).fold(0, usize::max)
}
fn scene (&self) -> Option<&Scene> {
self.selected().scene().and_then(|s|self.scenes().get(s))
}
fn scene_mut (&mut self) -> Option<&mut Scene> {
self.selected().scene().and_then(|s|self.scenes_mut().get_mut(s))
}
fn scene_del (&mut self, index: usize) {
self.selected().scene().and_then(|s|Some(self.scenes_mut().remove(index)));
}
}
#[derive(Debug, Default)] pub struct Scene {
/// Name of scene
pub name: Arc<str>,
/// Clips in scene, one per track
pub clips: Vec<Option<Arc<RwLock<MidiClip>>>>,
/// Identifying color of scene
pub color: ItemPalette,
}
impl Scene {
/// Returns the pulse length of the longest clip in the scene
fn pulses (&self) -> usize {
self.clips.iter().fold(0, |a, p|{
a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0))
})
}
/// Returns true if all clips in the scene are
/// currently playing on the given collection of tracks.
fn is_playing (&self, tracks: &[Track]) -> bool {
self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate()
.all(|(track_index, clip)|match clip {
Some(c) => tracks
.get(track_index)
.map(|track|{
if let Some((_, Some(clip))) = track.player().play_clip() {
*clip.read().unwrap() == *c.read().unwrap()
} else {
false
}
})
.unwrap_or(false),
None => true
})
}
pub fn clip (&self, index: usize) -> Option<&Arc<RwLock<MidiClip>>> {
match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None }
}
}
impl HasScenes for Tek {
fn scenes (&self) -> &Vec<Scene> { &self.scenes }
fn scenes_mut (&mut self) -> &mut Vec<Scene> { &mut self.scenes }
}
#[cfg(test)] #[test] fn test_model_scene () {
let mut app = Tek::default();
let _ = app.scene_longest();
let _ = app.scene();
let _ = app.scene_mut();
let _ = app.scene_add(None, None);
app.scene_del(0);
let scene = Scene::default();
let _ = scene.pulses();
let _ = scene.is_playing(&[]);
}

View file

@ -0,0 +1,51 @@
use crate::*;
pub trait HasSelection {
fn selected (&self) -> &Selection;
fn selected_mut (&mut self) -> &mut Selection;
}
/// Represents the current user selection in the arranger
#[derive(PartialEq, Clone, Copy, Debug, Default)] pub enum Selection {
/// The whole mix is selected
#[default] 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 Selection {
fn is_mix (&self) -> bool { matches!(self, Self::Mix) }
fn is_track (&self) -> bool { matches!(self, Self::Track(_)) }
fn is_scene (&self) -> bool { matches!(self, Self::Scene(_)) }
fn is_clip (&self) -> bool { matches!(self, Self::Clip(_, _)) }
pub fn track (&self) -> Option<usize> {
use Selection::*;
match self { Clip(t, _) => Some(*t), Track(t) => Some(*t), _ => None }
}
pub fn scene (&self) -> Option<usize> {
use Selection::*;
match self { Clip(_, s) => Some(*s), Scene(s) => Some(*s), _ => None }
}
pub fn describe (&self, tracks: &[Track], scenes: &[Scene]) -> Arc<str> {
format!("{}", 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()
}
}
impl HasSelection for Tek {
fn selected (&self) -> &Selection { &self.selected }
fn selected_mut (&mut self) -> &mut Selection { &mut self.selected }
}

View file

@ -0,0 +1,103 @@
use crate::*;
impl Tek {
pub fn tracks_add (
&mut self, count: usize, width: Option<usize>,
midi_from: &[PortConnect], midi_to: &[PortConnect],
) -> 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), midi_from, midi_to)?.1;
if let Some(width) = width {
track.width = width;
}
}
Ok(())
}
pub fn track_add (
&mut self, name: Option<&str>, color: Option<ItemPalette>,
midi_froms: &[PortConnect],
midi_tos: &[PortConnect],
) -> Usually<(usize, &mut Track)> {
let name = name.map_or_else(||self.track_next_name(), |x|x.to_string().into());
let mut track = Track {
width: (name.len() + 2).max(12),
color: color.unwrap_or_else(ItemPalette::random),
player: MidiPlayer::new(
&format!("{name}"),
self.jack(),
Some(self.clock()),
None,
midi_froms,
midi_tos
)?,
name,
..Default::default()
};
self.tracks_mut().push(track);
let len = self.tracks().len();
let index = len - 1;
for scene in self.scenes_mut().iter_mut() {
while scene.clips.len() < len {
scene.clips.push(None);
}
}
Ok((index, &mut self.tracks_mut()[index]))
}
pub fn track_del (&mut self, index: usize) {
self.tracks_mut().remove(index);
for scene in self.scenes_mut().iter_mut() {
scene.clips.remove(index);
}
}
}
pub trait HasTracks: HasSelection + HasClock + HasJack + HasEditor + Send + Sync {
fn midi_ins (&self) -> &Vec<JackMidiIn>;
fn midi_outs (&self) -> &Vec<JackMidiOut>;
fn tracks (&self) -> &Vec<Track>;
fn tracks_mut (&mut self) -> &mut Vec<Track>;
fn track_longest (&self) -> usize {
self.tracks().iter().map(|s|s.name.len()).fold(0, usize::max)
}
const WIDTH_OFFSET: usize = 1;
fn track_next_name (&self) -> Arc<str> {
format!("Track{:02}", self.tracks().len() + 1).into()
}
fn track (&self) -> Option<&Track> {
self.selected().track().and_then(|s|self.tracks().get(s))
}
fn track_mut (&mut self) -> Option<&mut Track> {
self.selected().track().and_then(|s|self.tracks_mut().get_mut(s))
}
}
#[derive(Debug, Default)] pub struct Track {
/// 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,
/// Device chain
pub devices: Vec<Box<dyn Device>>,
/// Inputs of 1st device
pub audio_ins: Vec<JackAudioIn>,
/// Outputs of last device
pub audio_outs: Vec<JackAudioOut>,
}
has_clock!(|self: Track|self.player.clock);
has_player!(|self: Track|self.player);
impl Track {
const MIN_WIDTH: usize = 9;
fn width_inc (&mut self) { self.width += 1; }
fn width_dec (&mut self) { if self.width > Track::MIN_WIDTH { self.width -= 1; } }
}
impl HasTracks for Tek {
fn midi_ins (&self) -> &Vec<JackMidiIn> { &self.midi_ins }
fn midi_outs (&self) -> &Vec<JackMidiOut> { &self.midi_outs }
fn tracks (&self) -> &Vec<Track> { &self.tracks }
fn tracks_mut (&mut self) -> &mut Vec<Track> { &mut self.tracks }
}

282
crates/app/src/view.rs Normal file
View file

@ -0,0 +1,282 @@
use crate::*;
mod view_clock; pub use self::view_clock::*;
mod view_color; pub use self::view_color::*;
mod view_memo; pub use self::view_memo::*;
mod view_meter; pub use self::view_meter::*;
mod view_track; pub use self::view_track::*;
mod view_ports; pub use self::view_ports::*;
mod view_layout; pub use self::view_layout::*;
pub(crate) use std::fmt::Write;
pub(crate) use ::tengri::tui::ratatui::prelude::Position;
pub(crate) trait ScenesColors<'a> = Iterator<Item=SceneWithColor<'a>>;
pub(crate) type SceneWithColor<'a> = (usize, &'a Scene, usize, usize, Option<ItemPalette>);
pub(crate) struct ArrangerView<'a> {
app: &'a Tek,
is_editing: bool,
width: u16,
width_mid: u16,
width_side: u16,
inputs_count: usize,
inputs_height: u16,
outputs_count: usize,
outputs_height: u16,
scene_last: usize,
scene_count: usize,
scene_scroll: Fill<Fixed<u16, ScrollbarV>>,
scene_selected: Option<usize>,
scenes_height: u16,
track_scroll: Fill<Fixed<u16, ScrollbarH>>,
track_count: usize,
track_selected: Option<usize>,
tracks_height: u16,
show_debug_info: bool,
}
impl<'a> Content<TuiOut> for ArrangerView<'a> {
fn content (&self) -> impl Render<TuiOut> {
let ins = |x|Bsp::s(self.inputs(), x);
let tracks = |x|Bsp::s(self.tracks(), x);
let outs = |x|Bsp::n(self.outputs(), x);
let bg = |x|Tui::bg(Color::Reset, x);
//let track_scroll = |x|Bsp::s(&self.track_scroll, x);
//let scene_scroll = |x|Bsp::e(&self.scene_scroll, x);
ins(tracks(outs(bg(self.scenes()))))
}
}
impl<'a> ArrangerView<'a> {
pub fn new (app: &'a Tek) -> Self {
Self {
app,
is_editing: app.is_editing(),
width: app.w(),
width_mid: app.w_tracks_area(),
width_side: app.w_sidebar(),
inputs_height: app.h_inputs().saturating_sub(1),
inputs_count: app.midi_ins.len(),
outputs_height: app.h_outputs().saturating_sub(1),
outputs_count: app.midi_outs.len(),
scenes_height: app.h_scenes_area(),
scene_selected: app.selected().scene(),
scene_count: app.scenes.len(),
scene_last: app.scenes.len().saturating_sub(1),
scene_scroll: Fill::y(Fixed::x(1, ScrollbarV {
offset: app.scene_scroll,
length: app.h_scenes_area() as usize,
total: app.h_scenes() as usize,
})),
tracks_height: app.h_tracks_area(),
track_count: app.tracks.len(),
track_selected: app.selected().track(),
track_scroll: Fill::x(Fixed::y(1, ScrollbarH {
offset: app.track_scroll,
length: app.h_tracks_area() as usize,
total: app.h_scenes() as usize,
})),
show_debug_info: false
}
}
pub(crate) fn tracks_with_sizes_scrolled (&'a self)
-> impl TracksSizes<'a>
{
let width = self.width_mid;
self.app.tracks_with_sizes().map_while(move|(t, track, x1, x2)|{
(width > x2 as u16).then_some((t, track, x1, x2))
})
}
pub(crate) fn scenes_with_scene_colors (&self)
-> impl ScenesColors<'_>
{
self.app.scenes_with_sizes(self.is_editing, Tek::H_SCENE, Tek::H_EDITOR).map_while(
move|(s, scene, y1, y2)|if y2 as u16 > self.scenes_height {
None
} else { Some((s, scene, y1, y2, if s == 0 {
None
} else {
Some(self.app.scenes()[s-1].color)
}))
})
}
pub(crate) fn scenes_with_track_colors (&self)
-> impl ScenesColors<'_>
{
self.app.scenes_with_sizes(self.is_editing, Tek::H_SCENE, Tek::H_EDITOR).map_while(
move|(s, scene, y1, y2)|if y2 as u16 > self.scenes_height {
None
} else {
Some((s, scene, y1, y2, if s == 0 {
None
} else {
Some(self.app.scenes[s-1].clips[self.track_selected.unwrap_or(0)].as_ref()
.map(|c|c.read().unwrap().color)
.unwrap_or(ItemPalette::G[32]))
}))
}
)
}
}
impl Tek {
/// Spacing between tracks.
pub(crate) const TRACK_SPACING: usize = 0;
/// Default scene height.
pub(crate) const H_SCENE: usize = 2;
/// Default editor height.
pub(crate) const H_EDITOR: usize = 15;
/// Width of display
pub(crate) fn w (&self) -> u16 {
self.size.w() as u16
}
pub(crate) fn w_sidebar (&self) -> u16 {
self.w() / if self.is_editing() { 16 } else { 8 } as u16
}
/// Width taken by all tracks.
pub(crate) fn w_tracks (&self) -> u16 {
self.tracks_with_sizes().last().map(|(_, _, _, x)|x as u16).unwrap_or(0)
}
/// Width available to display tracks.
pub(crate) fn w_tracks_area (&self) -> u16 {
self.w().saturating_sub(2 * self.w_sidebar())
}
/// Height of display
pub(crate) fn h (&self) -> u16 {
self.size.h() as u16
}
/// Height available to display track headers.
pub(crate) fn h_tracks_area (&self) -> u16 {
5
//self.h().saturating_sub(self.h_inputs() + self.h_outputs())
}
/// Height available to display tracks.
pub(crate) fn h_scenes_area (&self) -> u16 {
//15
self.h().saturating_sub(self.h_inputs() + self.h_outputs() + 11)
}
/// Height taken by all inputs.
pub(crate) fn h_inputs (&self) -> u16 {
1 + self.inputs_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0)
}
/// Height taken by all outputs.
pub(crate) fn h_outputs (&self) -> u16 {
1 + self.outputs_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0)
}
/// Height taken by all scenes.
pub(crate) fn h_scenes (&self) -> u16 {
self.scenes_with_sizes(self.is_editing(), Self::H_SCENE, Self::H_EDITOR).last()
.map(|(_, _, _, y)|y as u16).unwrap_or(0)
}
pub(crate) fn inputs_with_sizes (&self)
-> impl PortsSizes<'_>
{
let mut y = 0;
self.midi_ins.iter().enumerate().map(move|(i, input)|{
let height = 1 + input.conn().len();
let data = (i, input.name(), input.conn(), y, y + height);
y += height;
data
})
}
pub(crate) fn outputs_with_sizes (&self)
-> impl PortsSizes<'_>
{
let mut y = 0;
self.midi_outs.iter().enumerate().map(move|(i, output)|{
let height = 1 + output.conn().len();
let data = (i, output.name(), output.conn(), y, y + height);
y += height;
data
})
}
pub(crate) fn tracks_with_sizes (&self)
-> impl TracksSizes<'_>
{
let mut x = 0;
let editing = self.is_editing();
let active = match self.selected() {
Selection::Track(t) if editing => Some(t),
Selection::Clip(t, _) if editing => Some(t),
_ => None
};
let bigger = self.editor_w();
self.tracks().iter().enumerate().map(move |(index, track)|{
let width = if Some(index) == active.copied() { bigger } else { track.width.max(8) };
let data = (index, track, x, x + width);
x += width + Tek::TRACK_SPACING;
data
})
}
pub(crate) fn scenes_with_sizes (&self, editing: bool, height: usize, larger: usize)
-> impl ScenesSizes<'_>
{
let (selected_track, selected_scene) = match self.selected() {
Selection::Track(t) => (Some(*t), None),
Selection::Scene(s) => (None, Some(*s)),
Selection::Clip(t, s) => (Some(*t), Some(*s)),
_ => (None, None)
};
let mut y = 0;
self.scenes().iter().enumerate().map(move|(s, scene)|{
let active = editing && selected_track.is_some() && selected_scene == Some(s);
let height = if active { larger } else { height };
let data = (s, scene, y, y + height);
y += height;
data
})
}
}
/// Define a type alias for iterators of sized items (columns).
macro_rules! def_sizes_iter {
($Type:ident => $($Item:ty),+) => {
pub(crate) trait $Type<'a> =
Iterator<Item=(usize, $(&'a $Item,)+ usize, usize)> + Send + Sync + 'a;}}
def_sizes_iter!(ScenesSizes => Scene);
def_sizes_iter!(TracksSizes => Track);
def_sizes_iter!(InputsSizes => JackMidiIn);
def_sizes_iter!(OutputsSizes => JackMidiOut);
def_sizes_iter!(PortsSizes => Arc<str>, [PortConnect]);
#[cfg(test)] #[test] fn test_view_iter () {
let mut tek = Tek::default();
tek.editor = Some(Default::default());
let _: Vec<_> = tek.inputs_with_sizes().collect();
let _: Vec<_> = tek.outputs_with_sizes().collect();
let _: Vec<_> = tek.tracks_with_sizes().collect();
let _: Vec<_> = tek.scenes_with_sizes(true, 10, 10).collect();
//let _: Vec<_> = tek.scenes_with_colors(true, 10).collect();
//let _: Vec<_> = tek.scenes_with_track_colors(true, 10, 10).collect();
}
#[cfg(test)] #[test] fn test_view_sizes () {
let app = Tek::default();
let _ = app.w();
let _ = app.w_sidebar();
let _ = app.w_tracks_area();
let _ = app.h();
let _ = app.h_tracks_area();
let _ = app.h_inputs();
let _ = app.h_outputs();
let _ = app.h_scenes();
}

View file

@ -0,0 +1,88 @@
use crate::*;
impl Tek {
fn update_clock (&self) {
ViewCache::update_clock(&self.view_cache, self.clock(), self.size.w() > 80)
}
pub(crate) fn view_transport (&self) -> impl Content<TuiOut> + use<'_> {
self.update_clock();
let cache = self.view_cache.read().unwrap();
view_transport(
self.clock.is_rolling(),
cache.bpm.view.clone(),
cache.beat.view.clone(),
cache.time.view.clone(),
)
}
pub(crate) fn view_status (&self) -> impl Content<TuiOut> + use<'_> {
self.update_clock();
let cache = self.view_cache.read().unwrap();
view_status(
self.selected.describe(&self.tracks, &self.scenes),
cache.sr.view.clone(),
cache.buf.view.clone(),
cache.lat.view.clone(),
)
}
}
fn view_transport (
play: bool,
bpm: Arc<RwLock<String>>,
beat: Arc<RwLock<String>>,
time: Arc<RwLock<String>>,
) -> impl Content<TuiOut> {
let theme = ItemPalette::G[96];
Tui::bg(Black, row!(Bsp::a(
Fill::xy(Align::w(button_play_pause(play))),
Fill::xy(Align::e(row!(
FieldH(theme, "BPM", bpm),
FieldH(theme, "Beat", beat),
FieldH(theme, "Time", time),
)))
)))
}
fn view_status (
sel: Arc<str>,
sr: Arc<RwLock<String>>,
buf: Arc<RwLock<String>>,
lat: Arc<RwLock<String>>,
) -> impl Content<TuiOut> {
let theme = ItemPalette::G[96];
Tui::bg(Black, row!(Bsp::a(
Fill::xy(Align::w(FieldH(theme, "Selected", sel))),
Fill::xy(Align::e(row!(
FieldH(theme, "SR", sr),
FieldH(theme, "Buf", buf),
FieldH(theme, "Lat", lat),
)))
)))
}
fn button_play_pause (playing: bool) -> impl Content<TuiOut> {
let compact = true;//self.is_editing();
Tui::bg(
if playing{Rgb(0,128,0)}else{Rgb(128,64,0)},
Either::new(compact,
Thunk::new(move||Fixed::x(9, Either::new(playing,
Tui::fg(Rgb(0, 255, 0), " PLAYING "),
Tui::fg(Rgb(255, 128, 0), " STOPPED ")))
),
Thunk::new(move||Fixed::x(5, Either::new(playing,
Tui::fg(Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)),
Tui::fg(Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",))))
)
)
)
}
#[cfg(test)] mod test {
use super::*;
#[test] fn test_view_clock () {
let _ = button_play_pause(true);
let mut app = Tek::default();
let _ = app.view_transport();
let _ = app.view_status();
let _ = app.update_clock();
}
}

View file

@ -0,0 +1,32 @@
use crate::*;
impl Tek {
pub(crate) fn colors (
theme: &ItemPalette,
prev: Option<ItemPalette>,
selected: bool,
neighbor: bool,
is_last: bool,
) -> [Color;4] {
let fg = theme.lightest.rgb;
let bg = if selected { theme.light } else { theme.base }.rgb;
let hi = Self::color_hi(prev, neighbor);
let lo = Self::color_lo(theme, is_last, selected);
[fg, bg, hi, lo]
}
pub(crate) fn color_hi (prev: Option<ItemPalette>, neighbor: bool) -> Color {
prev.map(|prev|if neighbor {
prev.light.rgb
} else {
prev.base.rgb
}).unwrap_or(Reset)
}
pub(crate) fn color_lo (theme: &ItemPalette, is_last: bool, selected: bool) -> Color {
if is_last {
Reset
} else if selected {
theme.light.rgb
} else {
theme.base.rgb
}
}
}

View file

@ -0,0 +1,178 @@
use crate::*;
/// A three-column layout.
pub(crate) struct Tryptich<A, B, C> {
pub top: bool,
pub h: u16,
pub left: (u16, A),
pub middle: (u16, B),
pub right: (u16, C),
}
impl Tryptich<(), (), ()> {
pub fn center (h: u16) -> Self {
Self { h, top: false, left: (0, ()), middle: (0, ()), right: (0, ()) }
}
pub fn top (h: u16) -> Self {
Self { h, top: true, left: (0, ()), middle: (0, ()), right: (0, ()) }
}
}
impl<A, B, C> Tryptich<A, B, C> {
pub fn left <D> (self, w: u16, content: D) -> Tryptich<D, B, C> {
Tryptich { left: (w, content), ..self }
}
pub fn middle <D> (self, w: u16, content: D) -> Tryptich<A, D, C> {
Tryptich { middle: (w, content), ..self }
}
pub fn right <D> (self, w: u16, content: D) -> Tryptich<A, B, D> {
Tryptich { right: (w, content), ..self }
}
}
impl<A, B, C> Content<TuiOut> for Tryptich<A, B, C>
where A: Content<TuiOut>, B: Content<TuiOut>, C: Content<TuiOut> {
fn content (&self) -> impl Render<TuiOut> {
let Self { top, h, left: (w_a, ref a), middle: (w_b, ref b), right: (w_c, ref c) } = *self;
Fixed::y(h, if top {
Bsp::a(
Fill::x(Align::n(Fixed::x(w_b, Align::x(Tui::bg(Reset, b))))),
Bsp::a(
Fill::x(Align::nw(Fixed::x(w_a, Tui::bg(Reset, a)))),
Fill::x(Align::ne(Fixed::x(w_c, Tui::bg(Reset, c)))),
),
)
} else {
Bsp::a(
Fill::xy(Align::c(Fixed::x(w_b, Align::x(Tui::bg(Reset, b))))),
Bsp::a(
Fill::xy(Align::w(Fixed::x(w_a, Tui::bg(Reset, a)))),
Fill::xy(Align::e(Fixed::x(w_c, Tui::bg(Reset, c)))),
),
)
})
}
}
pub(crate) fn wrap (
bg: Color, fg: Color, content: impl Content<TuiOut>
) -> impl Content<TuiOut> {
Bsp::e(Tui::fg_bg(bg, Reset, ""),
Bsp::w(Tui::fg_bg(bg, Reset, ""),
Tui::fg_bg(fg, bg, content)))
}
pub(crate) fn button_2 <'a, K, L> (
key: K, label: L, editing: bool,
) -> impl Content<TuiOut> + 'a where
K: Content<TuiOut> + 'a,
L: Content<TuiOut> + 'a,
{
let key = Tui::fg_bg(Tui::g(0), Tui::orange(), Bsp::e(
Tui::fg_bg(Tui::orange(), Reset, ""),
Bsp::e(key, Tui::fg(Tui::g(96), ""))
));
let label = When::new(!editing, Tui::fg_bg(Tui::g(255), Tui::g(96), label));
Tui::bold(true, Bsp::e(key, label))
}
pub(crate) fn button_3 <'a, K, L, V> (
key: K,
label: L,
value: V,
editing: bool,
) -> impl Content<TuiOut> + 'a where
K: Content<TuiOut> + 'a,
L: Content<TuiOut> + 'a,
V: Content<TuiOut> + 'a,
{
let key = Tui::fg_bg(Tui::g(0), Tui::orange(),
Bsp::e(Tui::fg_bg(Tui::orange(), Reset, ""), Bsp::e(key, Tui::fg(if editing {
Tui::g(128)
} else {
Tui::g(96)
}, ""))));
let label = Bsp::e(
When::new(!editing, Bsp::e(
Tui::fg_bg(Tui::g(255), Tui::g(96), label),
Tui::fg_bg(Tui::g(128), Tui::g(96), ""),
)),
Bsp::e(
Tui::fg_bg(Tui::g(224), Tui::g(128), value),
Tui::fg_bg(Tui::g(128), Reset, ""),
));
Tui::bold(true, Bsp::e(key, label))
}
pub(crate) fn heading <'a> (
key: &'a str,
label: &'a str,
count: usize,
content: impl Content<TuiOut> + Send + Sync + 'a,
editing: bool,
) -> impl Content<TuiOut> + 'a {
let count = format!("{count}");
Fill::xy(Align::w(Bsp::s(Fill::x(Align::w(button_3(key, label, count, editing))), content)))
}
pub(crate) fn io_ports <'a, T: PortsSizes<'a>> (
fg: Color, bg: Color, iter: impl Fn()->T + Send + Sync + 'a
) -> impl Content<TuiOut> + 'a {
Map::new(iter,
move|(index, name, connections, y, y2): (usize, &'a Arc<str>, &'a [PortConnect], usize, usize), _|
map_south(y as u16, (y2-y) as u16, Bsp::s(
Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(Bsp::e(" 󰣲 ", name))))),
Map::new(||connections.iter(), move|connect: &'a PortConnect, index|map_south(index as u16, 1,
Fill::x(Align::w(Tui::bold(false, Tui::fg_bg(fg, bg,
&connect.info)))))))))
}
pub(crate) fn io_conns <'a, T: PortsSizes<'a>> (
fg: Color, bg: Color, iter: impl Fn()->T + Send + Sync + 'a
) -> impl Content<TuiOut> + 'a {
Map::new(iter,
move|(index, name, connections, y, y2): (usize, &'a Arc<str>, &'a [PortConnect], usize, usize), _|
map_south(y as u16, (y2-y) as u16, Bsp::s(
Fill::x(Tui::bold(true, wrap(bg, fg, Fill::x(Align::w("▞▞▞▞ ▞▞▞▞"))))),
Map::new(||connections.iter(), move|connect, index|map_south(index as u16, 1,
Fill::x(Align::w(Tui::bold(false, wrap(bg, fg, Fill::x(""))))))))))
}
pub(crate) fn per_track_top <'a, T: Content<TuiOut> + 'a, U: TracksSizes<'a>> (
width: u16,
tracks: impl Fn() -> U + Send + Sync + 'a,
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
) -> impl Content<TuiOut> + 'a {
Align::x(Tui::bg(Reset, Map::new(tracks,
move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{
let width = (x2 - x1) as u16;
map_east(x1 as u16, width, Fixed::x(width, Tui::fg_bg(
track.color.lightest.rgb,
track.color.base.rgb,
callback(index, track))))})))
}
pub(crate) fn per_track <'a, T: Content<TuiOut> + 'a, U: TracksSizes<'a>> (
width: u16,
tracks: impl Fn() -> U + Send + Sync + 'a,
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
) -> impl Content<TuiOut> + 'a {
per_track_top(
width,
tracks,
move|index, track|Fill::y(Align::y(callback(index, track)))
)
}
#[cfg(test)] mod test {
use super::*;
#[test] fn test_view () {
let _ = button_2("", "", true);
let _ = button_2("", "", false);
let _ = button_3("", "", "", true);
let _ = button_3("", "", "", false);
let _ = heading("", "", 0, "", true);
let _ = heading("", "", 0, "", false);
let _ = wrap(Reset, Reset, "");
}
}

View file

@ -0,0 +1,120 @@
use crate::*;
/// Clear a pre-allocated buffer, then write into it.
#[macro_export] macro_rules! rewrite {
($buf:ident, $($rest:tt)*) => { |$buf,_,_|{ $buf.clear(); write!($buf, $($rest)*) } }
}
#[derive(Debug, Default)] pub(crate) struct ViewMemo<T, U> {
pub(crate) value: T,
pub(crate) view: Arc<RwLock<U>>
}
impl<T: PartialEq, U> ViewMemo<T, U> {
fn new (value: T, view: U) -> Self {
Self { value, view: Arc::new(view.into()) }
}
pub(crate) fn update <R> (
&mut self,
newval: T,
render: impl Fn(&mut U, &T, &T)->R
) -> Option<R> {
if newval != self.value {
let result = render(&mut*self.view.write().unwrap(), &newval, &self.value);
self.value = newval;
return Some(result);
}
None
}
}
#[derive(Debug)] pub struct ViewCache {
pub(crate) sr: ViewMemo<Option<(bool, f64)>, String>,
pub(crate) buf: ViewMemo<Option<f64>, String>,
pub(crate) lat: ViewMemo<Option<f64>, String>,
pub(crate) bpm: ViewMemo<Option<f64>, String>,
pub(crate) beat: ViewMemo<Option<f64>, String>,
pub(crate) time: ViewMemo<Option<f64>, String>,
pub(crate) scns: ViewMemo<Option<(usize, usize)>, String>,
pub(crate) trks: ViewMemo<Option<(usize, usize)>, String>,
pub(crate) stop: Arc<str>,
pub(crate) edit: Arc<str>,
}
impl Default for ViewCache {
fn default () -> Self {
let mut beat = String::with_capacity(16);
write!(beat, "{}", Self::BEAT_EMPTY);
let mut time = String::with_capacity(16);
write!(time, "{}", Self::TIME_EMPTY);
let mut bpm = String::with_capacity(16);
write!(bpm, "{}", Self::BPM_EMPTY);
Self {
beat: ViewMemo::new(None, beat),
time: ViewMemo::new(None, time),
bpm: ViewMemo::new(None, bpm),
sr: ViewMemo::new(None, String::with_capacity(16)),
buf: ViewMemo::new(None, String::with_capacity(16)),
lat: ViewMemo::new(None, String::with_capacity(16)),
scns: ViewMemo::new(None, String::with_capacity(16)),
trks: ViewMemo::new(None, String::with_capacity(16)),
stop: "".into(),
edit: "edit".into(),
}
}
}
impl ViewCache {
pub const BEAT_EMPTY: &'static str = "-.-.--";
pub const TIME_EMPTY: &'static str = "-.---s";
pub const BPM_EMPTY: &'static str = "---.---";
pub(crate) fn track_counter (cache: &Arc<RwLock<Self>>, track: usize, tracks: usize)
-> Arc<RwLock<String>>
{
let data = (track, tracks);
cache.write().unwrap().trks.update(Some(data), rewrite!(buf, "{}/{}", data.0, data.1));
cache.read().unwrap().trks.view.clone()
}
pub(crate) fn scene_add (cache: &Arc<RwLock<Self>>, scene: usize, scenes: usize, is_editing: bool)
-> impl Content<TuiOut>
{
let data = (scene, scenes);
cache.write().unwrap().scns.update(Some(data), rewrite!(buf, "({}/{})", data.0, data.1));
button_3("S", "add scene", cache.read().unwrap().scns.view.clone(), is_editing)
}
pub(crate) fn update_clock (cache: &Arc<RwLock<Self>>, clock: &Clock, compact: bool) {
let rate = clock.timebase.sr.get();
let chunk = clock.chunk.load(Relaxed) as f64;
let lat = chunk / rate * 1000.;
let delta = |start: &Moment|clock.global.usec.get() - start.usec.get();
let mut cache = cache.write().unwrap();
cache.buf.update(Some(chunk), rewrite!(buf, "{chunk}"));
cache.lat.update(Some(lat), rewrite!(buf, "{lat:.1}ms"));
cache.sr.update(Some((compact, rate)), |buf,_,_|{
buf.clear();
if compact {
write!(buf, "{:.1}kHz", rate / 1000.)
} else {
write!(buf, "{:.0}Hz", rate)
}
});
if let Some(now) = clock.started.read().unwrap().as_ref().map(delta) {
let pulse = clock.timebase.usecs_to_pulse(now);
let time = now/1000000.;
let bpm = clock.timebase.bpm.get();
cache.beat.update(Some(pulse), |buf, _, _|{
buf.clear();
clock.timebase.format_beats_1_to(buf, pulse)
});
cache.time.update(Some(time), rewrite!(buf, "{:.3}s", time));
cache.bpm.update(Some(bpm), rewrite!(buf, "{:.3}", bpm));
} else {
cache.beat.update(None, rewrite!(buf, "{}", ViewCache::BEAT_EMPTY));
cache.time.update(None, rewrite!(buf, "{}", ViewCache::TIME_EMPTY));
cache.bpm.update(None, rewrite!(buf, "{}", ViewCache::BPM_EMPTY));
}
}
}

View file

@ -0,0 +1,48 @@
use crate::*;
fn view_meter <'a> (label: &'a str, value: f32) -> impl Content<TuiOut> + 'a {
col!(
FieldH(ItemPalette::G[128], label, format!("{:>+9.3}", value)),
Fixed::xy(if value >= 0.0 { 13 }
else if value >= -1.0 { 12 }
else if value >= -2.0 { 11 }
else if value >= -3.0 { 10 }
else if value >= -4.0 { 9 }
else if value >= -6.0 { 8 }
else if value >= -9.0 { 7 }
else if value >= -12.0 { 6 }
else if value >= -15.0 { 5 }
else if value >= -20.0 { 4 }
else if value >= -25.0 { 3 }
else if value >= -30.0 { 2 }
else if value >= -40.0 { 1 }
else { 0 }, 1, Tui::bg(if value >= 0.0 { Red }
else if value >= -3.0 { Yellow }
else { Green }, ())))
}
fn view_meters (values: &[f32;2]) -> impl Content<TuiOut> + use<'_> {
Bsp::s(
format!("L/{:>+9.3}", values[0]),
format!("R/{:>+9.3}", values[1]),
)
}
#[cfg(test)] mod test_view_meter {
use super::*;
use proptest::prelude::*;
#[test] fn test_view_meter () {
let _ = view_meter("", 0.0);
let _ = view_meters(&[0.0, 0.0]);
}
proptest! {
#[test] fn proptest_view_meter (
label in "\\PC*", value in f32::MIN..f32::MAX
) {
let _ = view_meter(&label, value);
}
#[test] fn proptest_view_meters (
value1 in f32::MIN..f32::MAX,
value2 in f32::MIN..f32::MAX
) {
let _ = view_meters(&[value1, value2]);
}
}
}

View file

@ -0,0 +1,140 @@
use crate::*;
impl<'a> ArrangerView<'a> {
/// Render input matrix.
pub(crate) fn inputs (&'a self) -> impl Content<TuiOut> + 'a {
Tui::bg(Color::Reset,
Bsp::s(Bsp::s(self.input_routes(), self.input_ports()), self.input_intos()))
}
fn input_routes (&'a self) -> impl Content<TuiOut> + 'a {
Tryptich::top(self.inputs_height)
.left(self.width_side,
io_ports(Tui::g(224), Tui::g(32), ||self.app.inputs_with_sizes()))
.middle(self.width_mid,
per_track_top(
self.width_mid,
||self.app.tracks_with_sizes(),
move|_, &Track { color, .. }|{
io_conns(color.dark.rgb, color.darker.rgb, ||self.app.inputs_with_sizes())
}
))
}
fn input_ports (&'a self) -> impl Content<TuiOut> + 'a {
Tryptich::top(1)
.left(self.width_side,
button_3("i", "midi ins", format!("{}", self.inputs_count), self.is_editing))
.right(self.width_side,
button_2("I", "add midi in", self.is_editing))
.middle(self.width_mid,
per_track_top(
self.width_mid,
||self.app.tracks_with_sizes(),
move|t, track|{
let rec = track.player.recording;
let mon = track.player.monitoring;
let rec = if rec { White } else { track.color.darkest.rgb };
let mon = if mon { White } else { track.color.darkest.rgb };
let bg = if self.track_selected == Some(t) {
track.color.light.rgb
} else {
track.color.base.rgb
};
//let bg2 = if t > 0 { track.color.base.rgb } else { Reset };
wrap(bg, Tui::g(224), Tui::bold(true, Fill::x(Bsp::e(
Tui::fg_bg(rec, bg, "Rec "),
Tui::fg_bg(mon, bg, "Mon "),
))))
}))
}
fn input_intos (&'a self) -> impl Content<TuiOut> + 'a {
Tryptich::top(2)
.left(self.width_side,
Bsp::s(Align::e("Input:"), Align::e("Into:")))
.middle(self.width_mid,
per_track_top(
self.width_mid,
||self.app.tracks_with_sizes(),
|_, _|{
Tui::bg(Reset, Align::c(Bsp::s(OctaveVertical::default(), " ------ ")))
}))
}
/// Render output matrix.
pub(crate) fn outputs (&'a self) -> impl Content<TuiOut> + 'a {
Tui::bg(Color::Reset, Align::n(Bsp::s(
Bsp::s(
self.output_nexts(),
self.output_froms(),
),
Bsp::s(
self.output_ports(),
self.output_conns(),
)
)))
}
fn output_nexts (&'a self) -> impl Content<TuiOut> + 'a {
Tryptich::top(2)
.left(self.width_side, Align::ne("From:"))
.middle(self.width_mid, per_track_top(
self.width_mid,
||self.tracks_with_sizes_scrolled(),
|_, _|{
Tui::bg(Reset, Align::c(Bsp::s(" ------ ", OctaveVertical::default())))
}))
}
fn output_froms (&'a self) -> impl Content<TuiOut> + 'a {
Tryptich::top(2)
.left(self.width_side, Align::ne("Next:"))
.middle(self.width_mid, per_track_top(
self.width_mid,
||self.tracks_with_sizes_scrolled(),
|t, track|Either(
track.player.next_clip.is_some(),
Thunk::new(||Tui::bg(Reset, format!("{:?}",
track.player.next_clip.as_ref()
.map(|(moment, clip)|clip.as_ref()
.map(|clip|clip.read().unwrap().name.clone()))
.flatten().as_ref()))),
Thunk::new(||Tui::bg(Reset, " ------ "))
)))
}
fn output_ports (&'a self) -> impl Content<TuiOut> + 'a {
Tryptich::top(1)
.left(self.width_side,
button_3("o", "midi outs", format!("{}", self.outputs_count), self.is_editing))
.right(self.width_side,
button_2("O", "add midi out", self.is_editing))
.middle(self.width_mid,
per_track_top(
self.width_mid,
||self.tracks_with_sizes_scrolled(),
move|i, t|{
let mute = false;
let solo = false;
let mute = if mute { White } else { t.color.darkest.rgb };
let solo = if solo { White } else { t.color.darkest.rgb };
let bg_1 = if self.track_selected == Some(i) { t.color.light.rgb } else { t.color.base.rgb };
let bg_2 = if i > 0 { t.color.base.rgb } else { Reset };
let mute = Tui::fg_bg(mute, bg_1, "Play ");
let solo = Tui::fg_bg(solo, bg_1, "Solo ");
wrap(bg_1, Tui::g(224), Tui::bold(true, Fill::x(Bsp::e(mute, solo))))
}))
}
fn output_conns (&'a self) -> impl Content<TuiOut> + 'a {
Tryptich::top(self.outputs_height)
.left(self.width_side,
io_ports(Tui::g(224), Tui::g(32), ||self.app.outputs_with_sizes()))
.middle(self.width_mid, per_track_top(
self.width_mid,
||self.tracks_with_sizes_scrolled(),
|_, t|io_conns(
t.color.dark.rgb,
t.color.darker.rgb,
||self.app.outputs_with_sizes()
)))
}
}

View file

@ -0,0 +1,147 @@
use crate::*;
impl<'a> ArrangerView<'a> {
/// Render track headers
pub(crate) fn tracks (&'a self) -> impl Content<TuiOut> + 'a {
let Self { width_side, width_mid, track_count, track_selected, is_editing, .. } = self;
Tryptich::center(3)
.left(*width_side, button_3("t", "track", format!("{}", *track_count), *is_editing))
.right(*width_side, button_2("T", "add track", *is_editing))
.middle(*width_mid, per_track(*width_mid,
||self.tracks_with_sizes_scrolled(),
|t, track|view_track_header(t, track, *track_selected == Some(t))))
}
/// Render scenes with clips
pub(crate) fn scenes (&'a self) -> impl Content<TuiOut> + 'a {
let Self {
width, width_side, width_mid,
scenes_height, scene_last, scene_selected,
track_selected, is_editing, app: Tek { editor, .. }, ..
} = self;
Tryptich::center(*scenes_height)
.left(*width_side, Map::new(||self.scenes_with_scene_colors(),
move|(index, scene, y1, y2, previous): SceneWithColor, _|view_scene_name(
*width,
(1 + y2 - y1) as u16,
y1 as u16,
index,
scene,
previous,
*scene_last == index,
*scene_selected
)))
.middle(*width_mid, per_track(*width_mid, ||self.tracks_with_sizes_scrolled(),
move|track_index, track|Map::new(||self.scenes_with_track_colors(),
move|(scene_index, scene, y1, y2, prev_scene): SceneWithColor<'a>, _|
view_scene_clip(
*width_mid,
(1 + y2 - y1) as u16,
y1 as u16,
scene,
prev_scene,
scene_index,
track_index,
*is_editing,
*track_selected == Some(track_index),
*scene_selected,
*scene_last == scene_index,
editor
))))
}
fn scene_add (&'a self) -> impl Content<TuiOut> + 'a {
ViewCache::scene_add(
&self.app.view_cache,
self.scene_selected.unwrap_or(0),
self.scene_count,
self.is_editing,
)
}
fn track_counter (&'a self) -> Arc<RwLock<String>> {
ViewCache::track_counter(
&self.app.view_cache,
self.track_selected.unwrap_or(0),
self.track_count,
)
}
}
fn view_track_header <'a> (
index: usize, track: &'a Track, active: bool
) -> impl Content<TuiOut> + use<'a> {
let fg = track.color.lightest.rgb;
let bg = if active { track.color.light.rgb } else { track.color.base.rgb };
let bg2 = Reset;//if index > 0 { self.tracks()[index - 1].color.base.rgb } else { Reset };
wrap(bg, fg, Tui::bold(true, Fill::x(Align::nw(&track.name))))
}
pub(crate) fn view_scene_name (
width: u16,
height: u16,
offset: u16,
index: usize,
scene: &Scene,
prev: Option<ItemPalette>,
last: bool,
select: Option<usize>,
) -> impl Content<TuiOut> {
Fill::x(map_south(offset, height, Fixed::y(height, view_scene_cell(
"", Some(scene.name.clone()), &scene.color, prev, last, select, true, index,
))))
}
pub(crate) fn view_scene_clip <'a> (
width: u16,
height: u16,
offset: u16,
scene: &'a Scene,
prev_bg: Option<ItemPalette>,
scene_index: usize,
track_index: usize,
editing: bool,
same_track: bool,
scene_selected: Option<usize>,
scene_is_last: bool,
editor: &'a Option<MidiEditor>,
) -> impl Content<TuiOut> + use<'a> {
let (name, _fg, bg) = if let Some(clip) = &scene.clips[track_index] {
let clip = clip.read().unwrap();
(Some(clip.name.clone()), clip.color.lightest.rgb, clip.color)
} else {
(None, Tui::g(96), ItemPalette::G[32])
};
let active = editing && same_track && scene_selected == Some(scene_index);
let with_editor = |x|Bsp::b(x, When(active, editor));
map_south(offset, height, with_editor(Fixed::y(height, view_scene_cell(
"", name, &bg, prev_bg,
scene_is_last, scene_selected, same_track, scene_index,
))))
}
pub(crate) fn view_scene_cell <'a> (
icon: &'a str,
name: Option<Arc<str>>,
color: &ItemPalette,
prev_color: Option<ItemPalette>,
is_last: bool,
selected: Option<usize>,
same_track: bool,
scene: usize,
) -> impl Content<TuiOut> + use<'a> {
Phat {
width: 0,
height: 0,
content: Fill::x(Align::w(Tui::bold(true, Bsp::e(icon, name)))),
colors: Tek::colors(
color,
prev_color,
same_track && selected == Some(scene),
same_track && scene > 0 && selected == Some(scene - 1),
is_last
)
}
}

12
crates/cli/Cargo.toml Normal file
View file

@ -0,0 +1,12 @@
[package]
name = "tek_cli"
edition = "2021"
version = "0.2.0"
[dependencies]
tek = { workspace = true }
clap = { workspace = true }
[[bin]]
name = "tek"
path = "./tek.rs"

View file

@ -0,0 +1,5 @@
(bsp/s (fixed/y 1 :transport)
(bsp/n (fixed/y 1 :status)
(fill/xy (bsp/a
(fill/xy (align/e :pool))
:arranger))))

View file

@ -0,0 +1,21 @@
(@u undo 1)
(@shift-u redo 1)
(@space clock toggle)
(@shift-space clock toggle 0)
(@t select :track 0)
(@tab edit :clip)
(@c color)
(@q launch)
(@shift-I input add)
(@shift-O output add)
(@shift-S scene add)
(@shift-T track add)
(@up select :scene-prev)
(@w select :scene-prev)
(@down select :scene-next)
(@s select :scene-next)
(@left select :track-prev)
(@a select :track-prev)
(@right select :track-next)
(@d select :track-next)

View file

@ -0,0 +1,8 @@
(@g clip get)
(@p clip put)
(@delete clip del)
(@comma clip prev)
(@period clip next)
(@lt clip swap-prev)
(@gt clip swap-next)
(@l clip loop-toggle)

View file

View file

@ -0,0 +1,7 @@
(@q scene launch :scene)
(@c scene color :scene)
(@comma scene prev)
(@period scene next)
(@lt scene swap-prev)
(@gt scene swap-next)
(@delete scene delete)

View file

@ -0,0 +1,12 @@
(@q track launch :track)
(@c track color :track)
(@comma track prev)
(@period track next)
(@lt track swap-prev)
(@gt track swap-next)
(@delete track delete)
(@r track rec)
(@m track mon)
(@p track play)
(@P track solo)

View file

@ -0,0 +1,6 @@
(bsp/s (fixed/y 1 :transport)
(bsp/s :sample
(bsp/n (fixed/y 1 :status)
(bsp/w (fixed/x :w-sidebar :pool)
(bsp/e :sampler
(fill/y :editor))))))

View file

@ -0,0 +1,4 @@
(bsp/s (fixed/y 1 :transport)
(bsp/n (fixed/y 1 :status)
(bsp/w (fixed/x :w-sidebar :pool)
(fill/y :editor))))

View file

@ -0,0 +1 @@
:transport

184
crates/cli/tek.rs Normal file
View file

@ -0,0 +1,184 @@
pub(crate) use tek::*;
pub(crate) use std::sync::{Arc, RwLock};
pub(crate) use clap::{self, Parser, Subcommand};
/// Application entrypoint.
pub fn main () -> Usually<()> {
Cli::parse().run()
}
#[derive(Debug, Parser)]
#[command(version, about = Some(HEADER), long_about = Some(HEADER))]
pub struct Cli {
/// Which app to initialize
#[command(subcommand)] mode: Mode,
/// Name of JACK client
#[arg(short='n', 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>,
/// 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 outs to connect to (multiple instances accepted)
#[arg(short='i', long)] midi_from_re: Vec<String>,
/// MIDI ins to connect to (multiple instances accepted)
#[arg(short='O', long)] midi_to: Vec<String>,
/// MIDI ins to connect to (multiple instances accepted)
#[arg(short='o', long)] midi_to_re: Vec<String>,
/// Audio outs to connect to left input
#[arg(short='l', long)] left_from: Vec<String>,
/// Audio outs to connect to right input
#[arg(short='r', long)] right_from: Vec<String>,
/// Audio ins to connect from left output
#[arg(short='L', long)] left_to: Vec<String>,
/// Audio ins to connect from right output
#[arg(short='R', long)] right_to: Vec<String>,
}
#[derive(Debug, Clone, Subcommand)] pub enum Mode {
/// A standalone transport clock.
Clock,
/// A MIDI sequencer.
Sequencer,
/// A MIDI-controlled audio sampler.
Sampler,
/// Sequencer and sampler together.12
Groovebox,
/// Multi-track MIDI sequencer.
Arranger {
/// Number of scenes
#[arg(short = 'y', long, default_value_t = 4)] scenes: usize,
/// Number of tracks
#[arg(short = 'x', long, default_value_t = 4)] tracks: usize,
/// Width of tracks
#[arg(short = 'w', long, default_value_t = 12)] track_width: usize,
},
/// TODO: A MIDI-controlled audio mixer
Mixer,
/// TODO: A customizable channel strip
Track,
/// TODO: An audio plugin host
Plugin,
}
impl Cli {
pub fn run (&self) -> Usually<()> {
let name = self.name.as_ref().map_or("tek", |x|x.as_str());
let mode = &self.mode;
let empty = &[] as &[&str];
let midi_froms = PortConnect::collect(&self.midi_from, empty, &self.midi_from_re);
let midi_tos = PortConnect::collect(&self.midi_to, empty, &self.midi_to_re);
let left_froms = PortConnect::collect(&self.left_from, empty, empty);
let left_tos = PortConnect::collect(&self.left_to, empty, empty);
let right_froms = PortConnect::collect(&self.right_from, empty, empty);
let right_tos = PortConnect::collect(&self.right_to, empty, empty);
let audio_froms = &[left_froms.as_slice(), right_froms.as_slice()];
let audio_tos = &[left_tos.as_slice(), right_tos.as_slice()];
let clip = match mode {
Mode::Sequencer | Mode::Groovebox => Some(Arc::new(RwLock::new(MidiClip::new(
"Clip", true, 384usize, None, Some(ItemColor::random().into())),
))),
_ => None,
};
let scenes = vec![];
Tui::new()?.run(&Jack::new(name)?.run(|jack|{
let mut midi_ins = vec![];
let mut midi_outs = vec![];
for (index, connect) in midi_froms.iter().enumerate() {
let port = JackMidiIn::new(jack, &format!("M/{index}"), &[connect.clone()])?;
midi_ins.push(port);
}
for (index, connect) in midi_tos.iter().enumerate() {
let port = JackMidiOut::new(jack, &format!("{index}/M"), &[connect.clone()])?;
midi_outs.push(port);
}
let mut app = Tek {
jack: jack.clone(),
view: SourceIter(match mode {
Mode::Clock => include_str!("./edn/transport.edn"),
Mode::Sequencer => include_str!("./edn/sequencer.edn"),
Mode::Groovebox => include_str!("./edn/groovebox.edn"),
Mode::Arranger { .. } => include_str!("./edn/arranger.edn"),
_ => todo!("{mode:?}"),
}),
pool: match mode {
Mode::Sequencer | Mode::Groovebox => clip.as_ref().map(Into::into),
Mode::Arranger { .. } => Some(Default::default()),
_ => None,
},
editor: match mode {
Mode::Sequencer | Mode::Groovebox => clip.as_ref().map(Into::into),
Mode::Arranger { .. } => Some(Default::default()),
_ => None
},
midi_ins,
midi_outs,
midi_buf: match mode {
Mode::Clock => vec![],
Mode::Sequencer | Mode::Groovebox | Mode::Arranger {..} => vec![vec![];65536],
_ => todo!("{mode:?}"),
},
color: ItemPalette::random(),
clock: Clock::new(jack, self.bpm)?,
keys: SourceIter(include_str!("./edn/arranger_keys.edn")),
keys_clip: SourceIter(include_str!("./edn/arranger_keys_clip.edn")),
keys_track: SourceIter(include_str!("./edn/arranger_keys_track.edn")),
keys_scene: SourceIter(include_str!("./edn/arranger_keys_scene.edn")),
keys_mix: SourceIter(include_str!("./edn/arranger_keys_mix.edn")),
tracks: match mode {
Mode::Sequencer => vec![Track::default()],
Mode::Groovebox => vec![Track {
devices: vec![
Sampler::new(
jack,
&"sampler",
midi_froms.as_slice(),
audio_froms,
audio_tos
)?.boxed()
],
..Track::default()
}],
_ => vec![]
},
scenes,
..Default::default()
};
if let &Mode::Arranger { scenes, tracks, track_width, .. } = mode {
app.arranger = Default::default();
app.selected = Selection::Clip(1, 1);
app.scenes_add(scenes)?;
app.tracks_add(tracks, Some(track_width), &[], &[])?;
}
jack.sync_lead(self.sync_lead, |mut state|{
let clock = app.clock();
clock.playhead.update_from_sample(state.position.frame() as f64);
state.position.bbt = Some(clock.bbt());
state.position
})?;
jack.sync_follow(self.sync_follow)?;
Ok(app)
})?)
}
}
const HEADER: &'static str = r#"
"#;
#[cfg(test)] #[test] fn test_cli () {
use clap::CommandFactory;
Cli::command().debug_assert();
let jack = Jack::default();
//TODO:
//let _ = Tek::new_clock(&jack, None, false, false, &[], &[]);
//let _ = Tek::new_sequencer(&jack, None, false, false, &[], &[]);
//let _ = Tek::new_groovebox(&jack, None, false, false, &[], &[], &[&[], &[]], &[&[], &[]]);
//let _ = Tek::new_arranger(&jack, None, false, false, &[], &[], &[&[], &[]], &[&[], &[]], 0, 0, 0);
}

7
crates/jack/Cargo.toml Normal file
View file

@ -0,0 +1,7 @@
[package]
name = "tek_jack"
edition = "2021"
version = "0.2.0"
[dependencies]
jack = { workspace = true }

View file

@ -0,0 +1,152 @@
use crate::*;
use ::jack::contrib::*;
use self::JackState::*;
/// Things that can provide a [jack::Client] reference.
pub trait HasJack {
/// Return the internal [jack::Client] handle
/// that lets you call the JACK API.
fn jack (&self) -> &Jack;
/// Run something with the client.
fn with_client <T> (&self, op: impl FnOnce(&Client)->T) -> T {
match &*self.jack().state.read().unwrap() {
Inert => panic!("jack client not activated"),
Inactive(ref client) => op(client),
Activating => panic!("jack client has not finished activation"),
Active(ref client) => op(client.as_client()),
}
}
fn port_by_name (&self, name: &str) -> Option<Port<Unowned>> {
self.with_client(|client|client.port_by_name(name))
}
fn port_by_id (&self, id: u32) -> Option<Port<Unowned>> {
self.with_client(|c|c.port_by_id(id))
}
fn register_port <PS: PortSpec + Default> (&self, name: impl AsRef<str>) -> Usually<Port<PS>> {
self.with_client(|client|Ok(client.register_port(name.as_ref(), PS::default())?))
}
fn sync_lead (&self, enable: bool, cb: impl Fn(TimebaseInfo)->Position) -> Usually<()> {
if enable {
self.with_client(|client|match client.register_timebase_callback(false, cb) {
Ok(_) => Ok(()),
Err(e) => Err(e)
})?
}
Ok(())
}
fn sync_follow (&self, _enable: bool) -> Usually<()> {
// TODO: sync follow
Ok(())
}
}
impl HasJack for Jack { fn jack (&self) -> &Jack { self } }
impl HasJack for &Jack { fn jack (&self) -> &Jack { self } }
/// Wraps [JackState] and through it [jack::Client].
#[derive(Clone, Debug, Default)]
pub struct Jack {
state: Arc<RwLock<JackState>>
}
impl Jack {
pub fn new (name: &str) -> Usually<Self> {
Ok(Self {
state: JackState::new(Client::new(name, ClientOptions::NO_START_SERVER)?.0)
})
}
pub fn run <'j: 'static, T: Audio + 'j> (
&self, cb: impl FnOnce(&Jack)->Usually<T>
) -> Usually<Arc<RwLock<T>>> {
let app = Arc::new(RwLock::new(cb(self)?));
let mut state = Activating;
std::mem::swap(&mut*self.state.write().unwrap(), &mut state);
if let Inactive(client) = state {
let client = client.activate_async(
// This is the misc notifications handler. It's a struct that wraps a [Box]
// which performs type erasure on a callback that takes [JackEvent], which is
// one of the available misc notifications.
Notifications(Box::new({
let app = app.clone();
move|event|app.write().unwrap().handle(event)
}) as BoxedJackEventHandler),
// This is the main processing handler. It's a struct that wraps a [Box]
// which performs type erasure on a callback that takes [Client] and [ProcessScope]
// and passes them down to the `app`'s `process` callback, which in turn
// implements audio and MIDI input and output on a realtime basis.
ClosureProcessHandler::new(Box::new({
let app = app.clone();
move|c: &_, s: &_|app.write().unwrap().process(c, s)
}) as BoxedAudioHandler<'j>),
)?;
*self.state.write().unwrap() = Active(client);
} else {
unreachable!();
}
Ok(app)
}
}
/// This is a connection which may be [Inactive], [Activating], or [Active].
/// In the [Active] and [Inactive] states, [JackState::client] returns a
/// [jack::Client], which you can use to talk to the JACK API.
#[derive(Debug, Default)] enum JackState {
/// Unused
#[default] Inert,
/// Before activation.
Inactive(Client),
/// During activation.
Activating,
/// After activation. Must not be dropped for JACK thread to persist.
Active(DynamicAsyncClient<'static>),
}
impl JackState {
fn new (client: Client) -> Arc<RwLock<Self>> { Arc::new(RwLock::new(Self::Inactive(client))) }
}
//has_jack_client!(|self: JackState|match self {
//Inert => panic!("jack client not activated"),
//Inactive(ref client) => client,
//Activating => panic!("jack client has not finished activation"),
//Active(ref client) => client.as_client(),
//});
/// This is a boxed realtime callback.
pub type BoxedAudioHandler<'j> =
Box<dyn FnMut(&Client, &ProcessScope) -> Control + Send + 'j>;
/// This is the notification handler wrapper for a boxed realtime callback.
pub type DynamicAudioHandler<'j> =
ClosureProcessHandler<(), BoxedAudioHandler<'j>>;
/// This is a boxed [JackEvent] callback.
pub type BoxedJackEventHandler<'j> =
Box<dyn Fn(JackEvent) + Send + Sync + 'j>;
/// This is the notification handler wrapper for a boxed [JackEvent] callback.
pub type DynamicNotifications<'j> =
Notifications<BoxedJackEventHandler<'j>>;
/// This is a running JACK [AsyncClient] with maximum type erasure.
/// It has one [Box] containing a function that handles [JackEvent]s,
/// and another [Box] containing a function that handles realtime IO,
/// and that's all it knows about them.
pub type DynamicAsyncClient<'j>
= AsyncClient<DynamicNotifications<'j>, DynamicAudioHandler<'j>>;
/// Implement [Audio]: provide JACK callbacks.
#[macro_export] macro_rules! audio {
(|
$self1:ident:
$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?,$c:ident,$s:ident
|$cb:expr$(;|$self2:ident,$e:ident|$cb2:expr)?) => {
impl $(<$($L),*$($T $(: $U)?),*>)? Audio for $Struct $(<$($L),*$($T),*>)? {
#[inline] fn process (&mut $self1, $c: &Client, $s: &ProcessScope) -> Control { $cb }
$(#[inline] fn handle (&mut $self2, $e: JackEvent) { $cb2 })?
}
}
}
/// Trait for thing that has a JACK process callback.
pub trait Audio: Send + Sync {
fn handle (&mut self, _event: JackEvent) {}
fn process (&mut self, _: &Client, _: &ProcessScope) -> Control {
Control::Continue
}
fn callback (
state: &Arc<RwLock<Self>>, client: &Client, scope: &ProcessScope
) -> Control where Self: Sized {
if let Ok(mut state) = state.write() {
state.process(client, scope)
} else {
Control::Quit
}
}
}

View file

@ -0,0 +1,197 @@
use crate::*
/// A [AudioComponent] bound to a JACK client and a set of ports.
pub struct JackDevice<E: Engine> {
/// The active JACK client of this device.
pub client: DynamicAsyncClient,
/// The device state, encapsulated for sharing between threads.
pub state: Arc<RwLock<Box<dyn AudioComponent<E>>>>,
/// Unowned copies of the device's JACK ports, for connecting to the device.
/// The "real" readable/writable `Port`s are owned by the `state`.
pub ports: UnownedJackPorts,
}
impl<E: Engine> std::fmt::Debug for JackDevice<E> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("JackDevice")
.field("ports", &self.ports)
.finish()
}
}
impl<E: Engine> Render for JackDevice<E> {
type Engine = E;
fn min_size(&self, to: E::Size) -> Perhaps<E::Size> {
self.state.read().unwrap().layout(to)
}
fn render(&self, to: &mut E::Output) -> Usually<()> {
self.state.read().unwrap().render(to)
}
}
impl<E: Engine> Handle<E> for JackDevice<E> {
fn handle(&mut self, from: &E::Input) -> Perhaps<E::Handled> {
self.state.write().unwrap().handle(from)
}
}
impl<E: Engine> Ports for JackDevice<E> {
fn audio_ins(&self) -> Usually<Vec<&Port<Unowned>>> {
Ok(self.ports.audio_ins.values().collect())
}
fn audio_outs(&self) -> Usually<Vec<&Port<Unowned>>> {
Ok(self.ports.audio_outs.values().collect())
}
fn midi_ins(&self) -> Usually<Vec<&Port<Unowned>>> {
Ok(self.ports.midi_ins.values().collect())
}
fn midi_outs(&self) -> Usually<Vec<&Port<Unowned>>> {
Ok(self.ports.midi_outs.values().collect())
}
}
impl<E: Engine> JackDevice<E> {
/// Returns a locked mutex of the state's contents.
pub fn state(&self) -> LockResult<RwLockReadGuard<Box<dyn AudioComponent<E>>>> {
self.state.read()
}
/// Returns a locked mutex of the state's contents.
pub fn state_mut(&self) -> LockResult<RwLockWriteGuard<Box<dyn AudioComponent<E>>>> {
self.state.write()
}
pub fn connect_midi_in(&self, index: usize, port: &Port<Unowned>) -> Usually<()> {
Ok(self
.client
.as_client()
.connect_ports(port, self.midi_ins()?[index])?)
}
pub fn connect_midi_out(&self, index: usize, port: &Port<Unowned>) -> Usually<()> {
Ok(self
.client
.as_client()
.connect_ports(self.midi_outs()?[index], port)?)
}
pub fn connect_audio_in(&self, index: usize, port: &Port<Unowned>) -> Usually<()> {
Ok(self
.client
.as_client()
.connect_ports(port, self.audio_ins()?[index])?)
}
pub fn connect_audio_out(&self, index: usize, port: &Port<Unowned>) -> Usually<()> {
Ok(self
.client
.as_client()
.connect_ports(self.audio_outs()?[index], port)?)
}
}
////////////////////////////////////////////////////////////////////////////////////
///// `JackDevice` factory. Creates JACK `Client`s, performs port registration
///// and activation, and encapsulates a `AudioComponent` into a `JackDevice`.
//pub struct Jack {
//pub client: Client,
//pub midi_ins: Vec<String>,
//pub audio_ins: Vec<String>,
//pub midi_outs: Vec<String>,
//pub audio_outs: Vec<String>,
//}
//impl Jack {
//pub fn new(name: &str) -> Usually<Self> {
//Ok(Self {
//midi_ins: vec![],
//audio_ins: vec![],
//midi_outs: vec![],
//audio_outs: vec![],
//client: Client::new(name, ClientOptions::NO_START_SERVER)?.0,
//})
//}
//pub fn run<'a: 'static, D, E>(
//self,
//state: impl FnOnce(JackPorts) -> Box<D>,
//) -> Usually<JackDevice<E>>
//where
//D: AudioComponent<E> + Sized + 'static,
//E: Engine + 'static,
//{
//let owned_ports = JackPorts {
//audio_ins: register_ports(&self.client, self.audio_ins, AudioIn::default())?,
//audio_outs: register_ports(&self.client, self.audio_outs, AudioOut::default())?,
//midi_ins: register_ports(&self.client, self.midi_ins, MidiIn::default())?,
//midi_outs: register_ports(&self.client, self.midi_outs, MidiOut::default())?,
//};
//let midi_outs = owned_ports
//.midi_outs
//.values()
//.map(|p| Ok(p.name()?))
//.collect::<Usually<Vec<_>>>()?;
//let midi_ins = owned_ports
//.midi_ins
//.values()
//.map(|p| Ok(p.name()?))
//.collect::<Usually<Vec<_>>>()?;
//let audio_outs = owned_ports
//.audio_outs
//.values()
//.map(|p| Ok(p.name()?))
//.collect::<Usually<Vec<_>>>()?;
//let audio_ins = owned_ports
//.audio_ins
//.values()
//.map(|p| Ok(p.name()?))
//.collect::<Usually<Vec<_>>>()?;
//let state = Arc::new(RwLock::new(state(owned_ports) as Box<dyn AudioComponent<E>>));
//let client = self.client.activate_async(
//Notifications(Box::new({
//let _state = state.clone();
//move |_event| {
//// FIXME: this deadlocks
////state.lock().unwrap().handle(&event).unwrap();
//}
//}) as Box<dyn Fn(JackEvent) + Send + Sync>),
//ClosureProcessHandler::new(Box::new({
//let state = state.clone();
//move |c: &Client, s: &ProcessScope| state.write().unwrap().process(c, s)
//}) as BoxedAudioHandler),
//)?;
//Ok(JackDevice {
//ports: UnownedJackPorts {
//audio_ins: query_ports(&client.as_client(), audio_ins),
//audio_outs: query_ports(&client.as_client(), audio_outs),
//midi_ins: query_ports(&client.as_client(), midi_ins),
//midi_outs: query_ports(&client.as_client(), midi_outs),
//},
//client,
//state,
//})
//}
//pub fn audio_in(mut self, name: &str) -> Self {
//self.audio_ins.push(name.to_string());
//self
//}
//pub fn audio_out(mut self, name: &str) -> Self {
//self.audio_outs.push(name.to_string());
//self
//}
//pub fn midi_in(mut self, name: &str) -> Self {
//self.midi_ins.push(name.to_string());
//self
//}
//pub fn midi_out(mut self, name: &str) -> Self {
//self.midi_outs.push(name.to_string());
//self
//}
//}
///// A UI component that may be associated with a JACK client by the `Jack` factory.
//pub trait AudioComponent<E: Engine>: Component<E> + Audio {
///// Perform type erasure for collecting heterogeneous devices.
//fn boxed(self) -> Box<dyn AudioComponent<E>>
//where
//Self: Sized + 'static,
//{
//Box::new(self)
//}
//}
///// All things that implement the required traits can be treated as `AudioComponent`.
//impl<E: Engine, W: Component<E> + Audio> AudioComponent<E> for W {}
/////////
/*
*/

View file

@ -0,0 +1,52 @@
use crate::*;
/// Event enum for JACK events.
#[derive(Debug, Clone, PartialEq)] pub enum JackEvent {
ThreadInit,
Shutdown(ClientStatus, Arc<str>),
Freewheel(bool),
SampleRate(Frames),
ClientRegistration(Arc<str>, bool),
PortRegistration(PortId, bool),
PortRename(PortId, Arc<str>, Arc<str>),
PortsConnected(PortId, PortId, bool),
GraphReorder,
XRun,
}
/// Generic notification handler that emits [JackEvent]
pub struct Notifications<T: Fn(JackEvent) + Send>(pub T);
impl<T: Fn(JackEvent) + Send> NotificationHandler for Notifications<T> {
fn thread_init(&self, _: &Client) {
self.0(JackEvent::ThreadInit);
}
unsafe fn shutdown(&mut self, status: ClientStatus, reason: &str) {
self.0(JackEvent::Shutdown(status, reason.into()));
}
fn freewheel(&mut self, _: &Client, enabled: bool) {
self.0(JackEvent::Freewheel(enabled));
}
fn sample_rate(&mut self, _: &Client, frames: Frames) -> Control {
self.0(JackEvent::SampleRate(frames));
Control::Quit
}
fn client_registration(&mut self, _: &Client, name: &str, reg: bool) {
self.0(JackEvent::ClientRegistration(name.into(), reg));
}
fn port_registration(&mut self, _: &Client, id: PortId, reg: bool) {
self.0(JackEvent::PortRegistration(id, reg));
}
fn port_rename(&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control {
self.0(JackEvent::PortRename(id, old.into(), new.into()));
Control::Continue
}
fn ports_connected(&mut self, _: &Client, a: PortId, b: PortId, are: bool) {
self.0(JackEvent::PortsConnected(a, b, are));
}
fn graph_reorder(&mut self, _: &Client) -> Control {
self.0(JackEvent::GraphReorder);
Control::Continue
}
fn xrun(&mut self, _: &Client) -> Control {
self.0(JackEvent::XRun);
Control::Continue
}
}

View file

@ -0,0 +1,217 @@
use crate::*;
macro_rules! impl_port {
($Name:ident : $Spec:ident -> $Pair:ident |$jack:ident, $name:ident|$port:expr) => {
#[derive(Debug)] pub struct $Name {
/// Handle to JACK client, for receiving reconnect events.
jack: Jack,
/// Port name
name: Arc<str>,
/// Port handle.
port: Port<$Spec>,
/// List of ports to connect to.
conn: Vec<PortConnect>
}
impl AsRef<Port<$Spec>> for $Name { fn as_ref (&self) -> &Port<$Spec> { &self.port } }
impl $Name {
pub fn name (&self) -> &Arc<str> { &self.name }
pub fn port (&self) -> &Port<$Spec> { &self.port }
pub fn port_mut (&mut self) -> &mut Port<$Spec> { &mut self.port }
pub fn new ($jack: &Jack, name: impl AsRef<str>, connect: &[PortConnect])
-> Usually<Self>
{
let $name = name.as_ref();
let jack = $jack.clone();
let port = $port?;
let name = $name.into();
let conn = connect.to_vec();
let port = Self { jack, port, name, conn };
port.connect_to_matching()?;
Ok(port)
}
}
impl HasJack for $Name { fn jack (&self) -> &Jack { &self.jack } }
impl JackPort for $Name {
type Port = $Spec;
type Pair = $Pair;
fn port (&self) -> &Port<$Spec> { &self.port }
}
impl JackPortConnect<&str> for $Name {
fn connect_to (&self, to: &str) -> Usually<PortConnectStatus> {
self.with_client(|c|if let Some(ref port) = c.port_by_name(to.as_ref()) {
self.connect_to(port)
} else {
Ok(Missing)
})
}
}
impl JackPortConnect<&Port<Unowned>> for $Name {
fn connect_to (&self, port: &Port<Unowned>) -> Usually<PortConnectStatus> {
self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(&self.port, port) {
Connected
} else if let Ok(_) = c.connect_ports(port, &self.port) {
Connected
} else {
Mismatch
}))
}
}
impl JackPortConnect<&Port<$Pair>> for $Name {
fn connect_to (&self, port: &Port<$Pair>) -> Usually<PortConnectStatus> {
self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(&self.port, port) {
Connected
} else if let Ok(_) = c.connect_ports(port, &self.port) {
Connected
} else {
Mismatch
}))
}
}
impl JackPortAutoconnect for $Name {
fn conn (&self) -> &[PortConnect] {
&self.conn
}
}
};
}
impl_port!(JackAudioIn: AudioIn -> AudioOut |j, n|j.register_port::<AudioIn>(n));
impl_port!(JackAudioOut: AudioOut -> AudioIn |j, n|j.register_port::<AudioOut>(n));
impl_port!(JackMidiIn: MidiIn -> MidiOut |j, n|j.register_port::<MidiIn>(n));
impl_port!(JackMidiOut: MidiOut -> MidiIn |j, n|j.register_port::<MidiOut>(n));
pub trait JackPort: HasJack {
type Port: PortSpec;
type Pair: PortSpec;
fn port (&self) -> &Port<Self::Port>;
}
pub trait JackPortConnect<T>: JackPort {
fn connect_to (&self, to: T) -> Usually<PortConnectStatus>;
}
pub trait JackPortAutoconnect: JackPort + for<'a>JackPortConnect<&'a Port<Unowned>> {
fn conn (&self) -> &[PortConnect];
fn ports (&self, re_name: Option<&str>, re_type: Option<&str>, flags: PortFlags) -> Vec<String> {
self.with_client(|c|c.ports(re_name, re_type, flags))
}
fn port_by_id (&self, id: u32) -> Option<Port<Unowned>> {
self.with_client(|c|c.port_by_id(id))
}
fn port_by_name (&self, name: impl AsRef<str>) -> Option<Port<Unowned>> {
self.with_client(|c|c.port_by_name(name.as_ref()))
}
fn connect_to_matching (&self) -> Usually<()> {
for connect in self.conn().iter() {
let status = match &connect.name {
Exact(name) => self.connect_exact(name),
RegExp(re) => self.connect_regexp(re, connect.scope),
}?;
*connect.status.write().unwrap() = status;
}
Ok(())
}
fn connect_exact (
&self, name: &str
) -> Usually<Vec<(Port<Unowned>, Arc<str>, PortConnectStatus)>> {
self.with_client(|c|{
let mut status = vec![];
for port in c.ports(None, None, PortFlags::empty()).iter() {
if port.as_str() == &*name {
if let Some(port) = c.port_by_name(port.as_str()) {
let port_status = self.connect_to(&port)?;
let name = port.name()?.into();
status.push((port, name, port_status));
if port_status == Connected {
break
}
}
}
}
Ok(status)
})
}
fn connect_regexp (
&self, re: &str, scope: PortConnectScope
) -> Usually<Vec<(Port<Unowned>, Arc<str>, PortConnectStatus)>> {
self.with_client(|c|{
let mut status = vec![];
let ports = c.ports(Some(&re), None, PortFlags::empty());
for port in ports.iter() {
if let Some(port) = c.port_by_name(port.as_str()) {
let port_status = self.connect_to(&port)?;
let name = port.name()?.into();
status.push((port, name, port_status));
if port_status == Connected && scope == One {
break
}
}
}
Ok(status)
})
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum PortConnectName {
/** Exact match */
Exact(Arc<str>),
/** Match regular expression */
RegExp(Arc<str>),
}
#[derive(Clone, Copy, Debug, PartialEq)] pub enum PortConnectScope {
One,
All
}
#[derive(Clone, Copy, Debug, PartialEq)] pub enum PortConnectStatus {
Missing,
Disconnected,
Connected,
Mismatch,
}
#[derive(Clone, Debug)] pub struct PortConnect {
pub name: PortConnectName,
pub scope: PortConnectScope,
pub status: Arc<RwLock<Vec<(Port<Unowned>, Arc<str>, PortConnectStatus)>>>,
pub info: Arc<String>,
}
impl PortConnect {
pub fn collect (exact: &[impl AsRef<str>], re: &[impl AsRef<str>], re_all: &[impl AsRef<str>])
-> Vec<Self>
{
let mut connections = vec![];
for port in exact.iter() { connections.push(Self::exact(port)) }
for port in re.iter() { connections.push(Self::regexp(port)) }
for port in re_all.iter() { connections.push(Self::regexp_all(port)) }
connections
}
/// Connect to this exact port
pub fn exact (name: impl AsRef<str>) -> Self {
let info = format!("=:{}", name.as_ref()).into();
let name = Exact(name.as_ref().into());
Self { name, scope: One, status: Arc::new(RwLock::new(vec![])), info }
}
pub fn regexp (name: impl AsRef<str>) -> Self {
let info = format!("~:{}", name.as_ref()).into();
let name = RegExp(name.as_ref().into());
Self { name, scope: One, status: Arc::new(RwLock::new(vec![])), info }
}
pub fn regexp_all (name: impl AsRef<str>) -> Self {
let info = format!("+:{}", name.as_ref()).into();
let name = RegExp(name.as_ref().into());
Self { name, scope: All, status: Arc::new(RwLock::new(vec![])), info }
}
pub fn info (&self) -> Arc<str> {
let status = {
let status = self.status.read().unwrap();
let mut ok = 0;
for (_, _, state) in status.iter() {
if *state == Connected {
ok += 1
}
}
format!("{ok}/{}", status.len())
};
let scope = match self.scope {
One => " ", All => "*",
};
let name = match &self.name {
Exact(name) => format!("= {name}"), RegExp(name) => format!("~ {name}"),
};
format!(" ({}) {} {}", status, scope, name).into()
}
}

17
crates/jack/src/lib.rs Normal file
View file

@ -0,0 +1,17 @@
#![feature(type_alias_impl_trait)]
mod jack_client; pub use self::jack_client::*;
mod jack_event; pub use self::jack_event::*;
mod jack_port; pub use self::jack_port::*;
pub(crate) use PortConnectName::*;
pub(crate) use PortConnectScope::*;
pub(crate) use PortConnectStatus::*;
pub(crate) use std::sync::{Arc, RwLock};
pub use ::jack; pub(crate) use ::jack::{
//contrib::ClosureProcessHandler,
NotificationHandler,
Client, AsyncClient, ClientOptions, ClientStatus,
ProcessScope, Control, Frames,
Port, PortId, PortSpec, PortFlags,
Unowned, MidiIn, MidiOut, AudioIn, AudioOut,
};
pub(crate) type Usually<T> = Result<T, Box<dyn std::error::Error>>;

13
crates/midi/Cargo.toml Normal file
View file

@ -0,0 +1,13 @@
[package]
name = "tek_midi"
edition = "2021"
version = "0.2.0"
[dependencies]
tengri = { workspace = true }
tek_jack = { workspace = true }
tek_time = { workspace = true }
midly = { workspace = true }
uuid = { workspace = true }

View file

@ -0,0 +1,6 @@
(@up inc)
(@down dec)
(@right next)
(@left prev)
(@return set :length)
(@escape cancel)

View file

@ -0,0 +1,4 @@
(:char append :char)
(@backspace delete :last)
(@return confirm)
(@escape cancel)

View file

@ -0,0 +1,21 @@
(@up note/pos :note-pos-next)
(@w note/pos :note-pos-next)
(@down note/pos :note-pos-prev)
(@s note/pos :note-pos-prev)
(@comma note/len :note-len-prev)
(@period note/len :note-len-next)
(@plus note/range :note-range-next-)
(@underscore note/range :note-range-prev-)
(@left time/pos :time-pos-prev)
(@a time/pos :time-pos-prev)
(@right time/pos :time-pos-next)
(@d time/pos :time-pos-next)
(@equal time/zoom :time-zoom-prev)
(@minus time/zoom :time-zoom-next)
(@z time/lock)
(@enter note/put)
(@shift-enter note/append)
(@del note/del)
(@shift-del note/del)

View file

@ -0,0 +1,12 @@
(@n rename begin)
(@t length begin)
(@m import begin)
(@x export begin)
(@c clip color :current :random-color)
(@openbracket select :previous)
(@closebracket select :next)
(@lt swap :current :previous)
(@gt swap :current :next)
(@delete clip/delete :current)
(@shift-A clip/add :after :new-clip)
(@shift-D clip/add :after :cloned-clip)

View file

@ -0,0 +1,8 @@
(@up select :prev)
(@down select :next)
(@right chdir :selected)
(@left chdir :parent)
(@return confirm)
(@escape cancel)
(:char append :char)
(@backspace delete :last)

View file

View file

View file

View file

@ -0,0 +1,17 @@
use tek_midi::*;
use tengri::input::*;
use std::sync::*;
struct ExampleClips(Arc<RwLock<Vec<Arc<RwLock<MidiClip>>>>>);
impl HasClips for ExampleClips {
fn clips (&self) -> RwLockReadGuard<'_, Vec<Arc<RwLock<MidiClip>>>> {
self.0.read().unwrap()
}
fn clips_mut (&self) -> RwLockWriteGuard<'_, Vec<Arc<RwLock<MidiClip>>>> {
self.0.write().unwrap()
}
}
fn main () -> Result<(), Box<dyn std::error::Error>> {
let mut clips = ExampleClips(Arc::new(vec![].into()));
PoolClipCommand::Import(0, std::path::PathBuf::from("./example.mid")).execute(&mut clips)?;
Ok(())
}

86
crates/midi/src/lib.rs Normal file
View file

@ -0,0 +1,86 @@
mod midi_clip; pub use midi_clip::*;
mod midi_edit; pub use midi_edit::*;
mod midi_in; pub use midi_in::*;
mod midi_launch; pub use midi_launch::*;
mod midi_out; pub use midi_out::*;
mod midi_pitch; pub use midi_pitch::*;
mod midi_player; pub use midi_player::*;
mod midi_point; pub use midi_point::*;
mod midi_pool; pub use midi_pool::*;
mod midi_range; pub use midi_range::*;
mod midi_view; pub use midi_view::*;
mod piano_h; pub use self::piano_h::*;
mod piano_v; pub use self::piano_v::*;
pub(crate) use ::tek_time::*;
pub(crate) use ::tek_jack::{*, jack::*};
pub(crate) use ::tengri::{
input::*,
output::*,
dsl::*,
tui::{
*,
ratatui::style::{Style, Stylize, Color}
}
};
pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicUsize, AtomicBool, Ordering::Relaxed}};
pub(crate) use std::path::PathBuf;
pub(crate) use std::fmt::Debug;
pub use ::midly; pub(crate) use ::midly::{*, num::*, live::*};
pub(crate) const KEYS_EDIT: &str = include_str!("../edn/keys_edit.edn");
pub(crate) const KEYS_POOL: &str = include_str!("../edn/keys_pool.edn");
pub(crate) const KEYS_FILE: &str = include_str!("../edn/keys_pool_file.edn");
pub(crate) const KEYS_LENGTH: &str = include_str!("../edn/keys_clip_length.edn");
pub(crate) const KEYS_RENAME: &str = include_str!("../edn/keys_clip_rename.edn");
/// Add "all notes off" to the start of a buffer.
pub fn all_notes_off (output: &mut [Vec<Vec<u8>>]) {
let mut buf = vec![];
let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() };
let evt = LiveEvent::Midi { channel: 0.into(), message: msg };
evt.write(&mut buf).unwrap();
output[0].push(buf);
}
/// Return boxed iterator of MIDI events
pub fn parse_midi_input <'a> (input: MidiIter<'a>) -> Box<dyn Iterator<Item=(usize, LiveEvent<'a>, &'a [u8])> + 'a> {
Box::new(input.map(|RawMidi { time, bytes }|(
time as usize,
LiveEvent::parse(bytes).unwrap(),
bytes
)))
}
/// Update notes_in array
pub fn update_keys (keys: &mut[bool;128], message: &MidiMessage) {
match message {
MidiMessage::NoteOn { key, .. } => { keys[key.as_int() as usize] = true; }
MidiMessage::NoteOff { key, .. } => { keys[key.as_int() as usize] = false; },
_ => {}
}
}
#[cfg(test)] #[test] pub fn test_midi_clip () {
let clip = MidiClip::stop_all();
println!("{clip:?}");
let clip = MidiClip::default();
let mut clip = MidiClip::new("clip", true, 1, None, None);
clip.set_length(96);
clip.toggle_loop();
clip.record_event(12, midly::MidiMessage::NoteOn { key: 36.into(), vel: 100.into() });
assert!(clip.contains_note_on(36.into(), 6, 18));
assert_eq!(&clip.notes, &clip.duplicate().notes);
let clip = std::sync::Arc::new(clip);
assert_eq!(clip.clone(), clip);
}
#[cfg(test)] #[test] pub fn test_midi_edit () {
let editor = MidiEditor::default();
println!("{editor:?}");
}
#[cfg(test)] #[test] pub fn test_midi_player () {
let player = MidiPlayer::default();
println!("{player:?}");
}

View file

@ -0,0 +1,107 @@
//! MIDI clip data.
use crate::*;
pub trait HasMidiClip {
fn clip (&self) -> Option<Arc<RwLock<MidiClip>>>;
}
#[macro_export] macro_rules! has_clip {
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? HasMidiClip for $Struct $(<$($L),*$($T),*>)? {
fn clip (&$self) -> Option<Arc<RwLock<MidiClip>>> { $cb }
}
}
}
/// A MIDI sequence.
#[derive(Debug, Clone, Default)]
pub struct MidiClip {
pub uuid: uuid::Uuid,
/// Name of clip
pub name: Arc<str>,
/// Temporal resolution in pulses per quarter note
pub ppq: usize,
/// Length of clip in pulses
pub length: usize,
/// Notes in clip
pub notes: MidiData,
/// Whether to loop the clip or play it once
pub looped: bool,
/// Start of loop
pub loop_start: usize,
/// Length of loop
pub loop_length: usize,
/// All notes are displayed with minimum length
pub percussive: bool,
/// Identifying color of clip
pub color: ItemPalette,
}
/// MIDI message structural
pub type MidiData = Vec<Vec<MidiMessage>>;
impl MidiClip {
pub fn new (
name: impl AsRef<str>,
looped: bool,
length: usize,
notes: Option<MidiData>,
color: Option<ItemPalette>,
) -> Self {
Self {
uuid: uuid::Uuid::new_v4(),
name: name.as_ref().into(),
ppq: PPQ,
length,
notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]),
looped,
loop_start: 0,
loop_length: length,
percussive: true,
color: color.unwrap_or_else(ItemPalette::random)
}
}
pub fn set_length (&mut self, length: usize) {
self.length = length;
self.notes = vec![Vec::with_capacity(16);length];
}
pub fn duplicate (&self) -> Self {
let mut clone = self.clone();
clone.uuid = uuid::Uuid::new_v4();
clone
}
pub fn toggle_loop (&mut self) { self.looped = !self.looped; }
pub fn record_event (&mut self, pulse: usize, message: MidiMessage) {
if pulse >= self.length { panic!("extend clip first") }
self.notes[pulse].push(message);
}
/// Check if a range `start..end` contains MIDI Note On `k`
pub fn contains_note_on (&self, k: u7, start: usize, end: usize) -> bool {
for events in self.notes[start.max(0)..end.min(self.notes.len())].iter() {
for event in events.iter() {
if let MidiMessage::NoteOn {key,..} = event { if *key == k { return true } }
}
}
false
}
pub fn stop_all () -> Self {
Self::new(
"Stop",
false,
1,
Some(vec![vec![MidiMessage::Controller {
controller: 123.into(),
value: 0.into()
}]]),
Some(ItemColor::from_rgb(Color::Rgb(32, 32, 32)).into())
)
}
}
impl PartialEq for MidiClip {
fn eq (&self, other: &Self) -> bool {
self.uuid == other.uuid
}
}
impl Eq for MidiClip {}

View file

@ -0,0 +1,306 @@
//! MIDI editor.
use crate::*;
pub trait HasEditor {
fn editor (&self) -> &Option<MidiEditor>;
fn editor_mut (&mut self) -> &Option<MidiEditor>;
fn is_editing (&self) -> bool { true }
fn editor_w (&self) -> usize { 0 }
fn editor_h (&self) -> usize { 0 }
}
#[macro_export] macro_rules! has_editor {
(|$self:ident: $Struct:ident|{
editor = $e0:expr;
editor_w = $e1:expr;
editor_h = $e2:expr;
is_editing = $e3:expr;
}) => {
impl HasEditor for $Struct {
fn editor (&$self) -> &Option<MidiEditor> { &$e0 }
fn editor_mut (&mut $self) -> &Option<MidiEditor> { &mut $e0 }
fn editor_w (&$self) -> usize { $e1 }
fn editor_h (&$self) -> usize { $e2 }
fn is_editing (&$self) -> bool { $e3 }
}
};
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? HasEditor for $Struct $(<$($L),*$($T),*>)? {
fn editor (&$self) -> &MidiEditor { &$cb }
}
};
}
/// Contains state for viewing and editing a clip
pub struct MidiEditor {
pub mode: PianoHorizontal,
pub size: Measure<TuiOut>,
keys: SourceIter<'static>
}
has_size!(<TuiOut>|self: MidiEditor|&self.size);
content!(TuiOut: |self: MidiEditor| {
self.autoscroll();
//self.autozoom();
self.size.of(&self.mode)
});
from!(|clip: &Arc<RwLock<MidiClip>>|MidiEditor = {
let model = Self::from(Some(clip.clone()));
model.redraw();
model
});
from!(|clip: Option<Arc<RwLock<MidiClip>>>|MidiEditor = {
let mut model = Self::default();
*model.clip_mut() = clip;
model.redraw();
model
});
provide!(bool: |self: MidiEditor| {
":true" => true,
":false" => false,
":time-lock" => self.time_lock().get(),
":time-lock-toggle" => !self.time_lock().get(),
});
provide!(usize: |self: MidiEditor| {
":note-length" => self.note_len(),
":note-pos" => self.note_pos(),
":note-pos-next" => self.note_pos() + 1,
":note-pos-prev" => self.note_pos().saturating_sub(1),
":note-len" => self.note_len(),
":note-len-next" => self.note_len() + 1,
":note-len-prev" => self.note_len().saturating_sub(1),
":note-range" => self.note_axis().get(),
":note-range-prev" => self.note_axis().get() + 1,
":note-range-next" => self.note_axis().get().saturating_sub(1),
":time-pos" => self.time_pos(),
":time-pos-next" => self.time_pos() + self.time_zoom().get(),
":time-pos-prev" => self.time_pos().saturating_sub(self.time_zoom().get()),
":time-zoom" => self.time_zoom().get(),
":time-zoom-next" => self.time_zoom().get() + 1,
":time-zoom-prev" => self.time_zoom().get().saturating_sub(1).max(1),
});
impl std::fmt::Debug for MidiEditor {
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
f.debug_struct("MidiEditor")
.field("mode", &self.mode)
.finish()
}
}
impl Default for MidiEditor {
fn default () -> Self {
Self {
mode: PianoHorizontal::new(None),
size: Measure::new(),
keys: SourceIter(KEYS_EDIT),
}
}
}
impl MidiEditor {
//fn clip_length (&self) -> usize {
//self.clip().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1)
//}
/// Put note at current position
fn put_note (&mut self, advance: bool) {
let mut redraw = false;
if let Some(clip) = self.clip() {
let mut clip = clip.write().unwrap();
let note_start = self.time_pos();
let note_pos = self.note_pos();
let note_len = self.note_len();
let note_end = note_start + (note_len.saturating_sub(1));
let key: u7 = u7::from(note_pos as u8);
let vel: u7 = 100.into();
let length = clip.length;
let note_end = note_end % length;
let note_on = MidiMessage::NoteOn { key, vel };
if !clip.notes[note_start].iter().any(|msg|*msg == note_on) {
clip.notes[note_start].push(note_on);
}
let note_off = MidiMessage::NoteOff { key, vel };
if !clip.notes[note_end].iter().any(|msg|*msg == note_off) {
clip.notes[note_end].push(note_off);
}
if advance {
self.set_time_pos(note_end);
}
redraw = true;
}
if redraw {
self.mode.redraw();
}
}
pub fn clip_status (&self) -> impl Content<TuiOut> + '_ {
let (color, name, length, looped) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) {
(clip.color, clip.name.clone(), clip.length, clip.looped)
} else { (ItemPalette::G[64], String::new().into(), 0, false) };
Bsp::e(
FieldH(color, "Edit", format!("{name} ({length})")),
FieldH(color, "Loop", looped.to_string())
)
}
pub fn edit_status (&self) -> impl Content<TuiOut> + '_ {
let (color, length) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) {
(clip.color, clip.length)
} else { (ItemPalette::G[64], 0) };
let time_pos = self.time_pos();
let time_zoom = self.time_zoom().get();
let time_lock = if self.time_lock().get() { "[lock]" } else { " " };
let note_pos = format!("{:>3}", self.note_pos());
let note_name = format!("{:4}", Note::pitch_to_name(self.note_pos()));
let note_len = format!("{:>4}", self.note_len());
Bsp::e(
FieldH(color, "Time", format!("{length}/{time_zoom}+{time_pos} {time_lock}")),
FieldH(color, "Note", format!("{note_name} {note_pos} {note_len}")),
)
}
}
impl TimeRange for MidiEditor {
fn time_len (&self) -> &AtomicUsize { self.mode.time_len() }
fn time_zoom (&self) -> &AtomicUsize { self.mode.time_zoom() }
fn time_lock (&self) -> &AtomicBool { self.mode.time_lock() }
fn time_start (&self) -> &AtomicUsize { self.mode.time_start() }
fn time_axis (&self) -> &AtomicUsize { self.mode.time_axis() }
}
impl NoteRange for MidiEditor {
fn note_lo (&self) -> &AtomicUsize { self.mode.note_lo() }
fn note_axis (&self) -> &AtomicUsize { self.mode.note_axis() }
}
impl NotePoint for MidiEditor {
fn note_len (&self) -> usize { self.mode.note_len() }
fn set_note_len (&self, x: usize) { self.mode.set_note_len(x) }
fn note_pos (&self) -> usize { self.mode.note_pos() }
fn set_note_pos (&self, x: usize) { self.mode.set_note_pos(x) }
}
impl TimePoint for MidiEditor {
fn time_pos (&self) -> usize { self.mode.time_pos() }
fn set_time_pos (&self, x: usize) { self.mode.set_time_pos(x) }
}
impl MidiViewer for MidiEditor {
fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { self.mode.buffer_size(clip) }
fn redraw (&self) { self.mode.redraw() }
fn clip (&self) -> &Option<Arc<RwLock<MidiClip>>> { self.mode.clip() }
fn clip_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>> { self.mode.clip_mut() }
fn set_clip (&mut self, p: Option<&Arc<RwLock<MidiClip>>>) { self.mode.set_clip(p) }
}
atom_command!(MidiEditCommand: |state: MidiEditor| {
("note/append" [] Some(Self::AppendNote))
("note/put" [] Some(Self::PutNote))
("note/del" [] Some(Self::DelNote))
("note/pos" [a: usize] Some(Self::SetNoteCursor(a.expect("no note cursor"))))
("note/len" [a: usize] Some(Self::SetNoteLength(a.expect("no note length"))))
("time/pos" [a: usize] Some(Self::SetTimeCursor(a.expect("no time cursor"))))
("time/zoom" [a: usize] Some(Self::SetTimeZoom(a.expect("no time zoom"))))
("time/lock" [a: bool] Some(Self::SetTimeLock(a.expect("no time lock"))))
("time/lock" [] Some(Self::SetTimeLock(!state.time_lock().get())))
});
#[derive(Clone, Debug)] pub enum MidiEditCommand {
// TODO: 1-9 seek markers that by default start every 8th of the clip
AppendNote,
PutNote,
DelNote,
SetNoteCursor(usize),
SetNoteLength(usize),
SetNoteScroll(usize),
SetTimeCursor(usize),
SetTimeScroll(usize),
SetTimeZoom(usize),
SetTimeLock(bool),
Show(Option<Arc<RwLock<MidiClip>>>),
}
handle!(TuiIn: |self: MidiEditor, input|{
Ok(if let Some(command) = self.keys.command::<_, MidiEditCommand, _>(self, input) {
let _undo = command.execute(self)?;
Some(true)
} else {
None
})
});
impl Command<MidiEditor> for MidiEditCommand {
fn execute (self, state: &mut MidiEditor) -> Perhaps<Self> {
use MidiEditCommand::*;
match self {
Show(clip) => { state.set_clip(clip.as_ref()); },
DelNote => {},
PutNote => { state.put_note(false); },
AppendNote => { state.put_note(true); },
SetTimeZoom(x) => { state.time_zoom().set(x); state.redraw(); },
SetTimeLock(x) => { state.time_lock().set(x); },
SetTimeScroll(x) => { state.time_start().set(x); },
SetNoteScroll(x) => { state.note_lo().set(x.min(127)); },
SetNoteLength(x) => {
let note_len = state.note_len();
let time_zoom = state.time_zoom().get();
state.set_note_len(x);
if note_len / time_zoom != x / time_zoom {
state.redraw();
}
},
SetTimeCursor(x) => { state.set_time_pos(x); },
SetNoteCursor(note) => { state.set_note_pos(note.min(127)); },
//_ => todo!("{:?}", self)
}
Ok(None)
}
}
//keymap!(KEYS_MIDI_EDITOR = |s: MidiEditor, _input: Event| MidiEditCommand {
//key(Up) => SetNoteCursor(s.note_pos() + 1),
//key(Char('w')) => SetNoteCursor(s.note_pos() + 1),
//key(Down) => SetNoteCursor(s.note_pos().saturating_sub(1)),
//key(Char('s')) => SetNoteCursor(s.note_pos().saturating_sub(1)),
//key(Left) => SetTimeCursor(s.time_pos().saturating_sub(s.note_len())),
//key(Char('a')) => SetTimeCursor(s.time_pos().saturating_sub(s.note_len())),
//key(Right) => SetTimeCursor((s.time_pos() + s.note_len()) % s.clip_length()),
//ctrl(alt(key(Up))) => SetNoteScroll(s.note_pos() + 3),
//ctrl(alt(key(Down))) => SetNoteScroll(s.note_pos().saturating_sub(3)),
//ctrl(alt(key(Left))) => SetTimeScroll(s.time_pos().saturating_sub(s.time_zoom().get())),
//ctrl(alt(key(Right))) => SetTimeScroll((s.time_pos() + s.time_zoom().get()) % s.clip_length()),
//ctrl(key(Up)) => SetNoteScroll(s.note_lo().get() + 1),
//ctrl(key(Down)) => SetNoteScroll(s.note_lo().get().saturating_sub(1)),
//ctrl(key(Left)) => SetTimeScroll(s.time_start().get().saturating_sub(s.note_len())),
//ctrl(key(Right)) => SetTimeScroll(s.time_start().get() + s.note_len()),
//alt(key(Up)) => SetNoteCursor(s.note_pos() + 3),
//alt(key(Down)) => SetNoteCursor(s.note_pos().saturating_sub(3)),
//alt(key(Left)) => SetTimeCursor(s.time_pos().saturating_sub(s.time_zoom().get())),
//alt(key(Right)) => SetTimeCursor((s.time_pos() + s.time_zoom().get()) % s.clip_length()),
//key(Char('d')) => SetTimeCursor((s.time_pos() + s.note_len()) % s.clip_length()),
//key(Char('z')) => SetTimeLock(!s.time_lock().get()),
//key(Char('-')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::next(s.time_zoom().get()) }),
//key(Char('_')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::next(s.time_zoom().get()) }),
//key(Char('=')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::prev(s.time_zoom().get()) }),
//key(Char('+')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::prev(s.time_zoom().get()) }),
//key(Enter) => PutNote,
//ctrl(key(Enter)) => AppendNote,
//key(Char(',')) => SetNoteLength(NoteDuration::prev(s.note_len())),
//key(Char('.')) => SetNoteLength(NoteDuration::next(s.note_len())),
//key(Char('<')) => SetNoteLength(NoteDuration::prev(s.note_len())),
//key(Char('>')) => SetNoteLength(NoteDuration::next(s.note_len())),
////// TODO: kpat!(Char('/')) => // toggle 3plet
////// TODO: kpat!(Char('?')) => // toggle dotted
//});
#[cfg(test)] #[test] fn test_midi_edit () {
let mut editor = MidiEditor {
mode: PianoHorizontal::new(Some(&Arc::new(RwLock::new(MidiClip::stop_all())))),
size: Default::default(),
keys: Default::default(),
};
let _ = editor.put_note(true);
let _ = editor.put_note(false);
let _ = editor.clip_status();
let _ = editor.edit_status();
struct TestEditorHost(Option<MidiEditor>);
has_editor!(|self: TestEditorHost|{
editor = self.0;
editor_w = 0;
editor_h = 0;
is_editing = false;
});
let mut host = TestEditorHost(Some(editor));
let _ = host.editor();
let _ = host.editor_mut();
let _ = host.is_editing();
let _ = host.editor_w();
let _ = host.editor_h();
}

View file

@ -0,0 +1,91 @@
use crate::*;
/// Trait for thing that may receive MIDI.
pub trait HasMidiIns {
fn midi_ins (&self) -> &Vec<JackMidiIn>;
fn midi_ins_mut (&mut self) -> &mut Vec<JackMidiIn>;
fn has_midi_ins (&self) -> bool {
!self.midi_ins().is_empty()
}
}
pub trait MidiRecordApi: HasClock + HasPlayClip + HasMidiIns {
fn notes_in (&self) -> &Arc<RwLock<[bool;128]>>;
fn recording (&self) -> bool;
fn recording_mut (&mut self) -> &mut bool;
fn toggle_record (&mut self) {
*self.recording_mut() = !self.recording();
}
fn monitoring (&self) -> bool;
fn monitoring_mut (&mut self) -> &mut bool;
fn toggle_monitor (&mut self) {
*self.monitoring_mut() = !self.monitoring();
}
fn monitor (&mut self, scope: &ProcessScope, midi_buf: &mut Vec<Vec<Vec<u8>>>) {
// For highlighting keys and note repeat
let notes_in = self.notes_in().clone();
let monitoring = self.monitoring();
for input in self.midi_ins_mut().iter() {
for (sample, event, bytes) in parse_midi_input(input.port().iter(scope)) {
if let LiveEvent::Midi { message, .. } = event {
if monitoring {
midi_buf[sample].push(bytes.to_vec());
}
update_keys(&mut notes_in.write().unwrap(), &message);
}
}
}
}
fn record (&mut self, scope: &ProcessScope, midi_buf: &mut Vec<Vec<Vec<u8>>>) {
if self.monitoring() {
self.monitor(scope, midi_buf);
}
if !self.clock().is_rolling() {
return
}
if let Some((started, ref clip)) = self.play_clip().clone() {
self.record_clip(scope, started, clip, midi_buf);
}
if let Some((_start_at, _clip)) = &self.next_clip() {
self.record_next();
}
}
fn record_clip (
&mut self,
scope: &ProcessScope,
started: Moment,
clip: &Option<Arc<RwLock<MidiClip>>>,
_midi_buf: &mut Vec<Vec<Vec<u8>>>
) {
if let Some(clip) = clip {
let sample0 = scope.last_frame_time() as usize;
let start = started.sample.get() as usize;
let _recording = self.recording();
let timebase = self.clock().timebase().clone();
let quant = self.clock().quant.get();
let mut clip = clip.write().unwrap();
let length = clip.length;
for input in self.midi_ins_mut().iter() {
for (sample, event, _bytes) in parse_midi_input(input.port().iter(scope)) {
if let LiveEvent::Midi { message, .. } = event {
clip.record_event({
let sample = (sample0 + sample - start) as f64;
let pulse = timebase.samples_to_pulse(sample);
let quantized = (pulse / quant).round() * quant;
quantized as usize % length
}, message);
}
}
}
}
}
fn record_next (&mut self) {
// TODO switch to next clip and record into it
}
fn overdub (&self) -> bool;
fn overdub_mut (&mut self) -> &mut bool;
fn toggle_overdub (&mut self) {
*self.overdub_mut() = !self.overdub();
}
}

View file

@ -0,0 +1,84 @@
use crate::*;
pub trait HasPlayClip: HasClock {
fn reset (&self) -> bool;
fn reset_mut (&mut self) -> &mut bool;
fn play_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
fn next_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
fn pulses_since_start (&self) -> Option<f64> {
if let Some((started, Some(_))) = self.play_clip().as_ref() {
let elapsed = self.clock().playhead.pulse.get() - started.pulse.get();
Some(elapsed)
} else {
None
}
}
fn pulses_since_start_looped (&self) -> Option<(f64, f64)> {
if let Some((started, Some(clip))) = self.play_clip().as_ref() {
let elapsed = self.clock().playhead.pulse.get() - started.pulse.get();
let length = clip.read().unwrap().length.max(1); // prevent div0 on empty clip
let times = (elapsed as usize / length) as f64;
let elapsed = (elapsed as usize % length) as f64;
Some((times, elapsed))
} else {
None
}
}
fn enqueue_next (&mut self, clip: Option<&Arc<RwLock<MidiClip>>>) {
let start = self.clock().next_launch_pulse() as f64;
let instant = Moment::from_pulse(self.clock().timebase(), start);
*self.next_clip_mut() = Some((instant, clip.cloned()));
*self.reset_mut() = true;
}
fn play_status (&self) -> impl Content<TuiOut> {
let (name, color): (Arc<str>, ItemPalette) = if let Some((_, Some(clip))) = self.play_clip() {
let MidiClip { ref name, color, .. } = *clip.read().unwrap();
(name.clone(), color)
} else {
("".into(), Tui::g(64).into())
};
let time: String = self.pulses_since_start_looped()
.map(|(times, time)|format!("{:>3}x {:>}", times+1.0, self.clock().timebase.format_beats_1(time)))
.unwrap_or_else(||String::from(" ")).into();
FieldV(color, "Now:", format!("{} {}", time, name))
}
fn next_status (&self) -> impl Content<TuiOut> {
let mut time: Arc<str> = String::from("--.-.--").into();
let mut name: Arc<str> = String::from("").into();
let mut color = ItemPalette::G[64];
let clock = self.clock();
if let Some((t, Some(clip))) = self.next_clip() {
let clip = clip.read().unwrap();
name = clip.name.clone();
color = clip.color.clone();
time = {
let target = t.pulse.get();
let current = clock.playhead.pulse.get();
if target > current {
let remaining = target - current;
format!("-{:>}", clock.timebase.format_beats_1(remaining))
} else {
String::new()
}
}.into()
} else if let Some((t, Some(clip))) = self.play_clip() {
let clip = clip.read().unwrap();
if clip.looped {
name = clip.name.clone();
color = clip.color.clone();
let target = t.pulse.get() + clip.length as f64;
let current = clock.playhead.pulse.get();
if target > current {
time = format!("-{:>}", clock.timebase.format_beats_0(
target - current
)).into()
}
} else {
name = "Stop".to_string().into();
}
};
FieldV(color, "Next:", format!("{} {}", time, name))
}
}

160
crates/midi/src/midi_out.rs Normal file
View file

@ -0,0 +1,160 @@
use crate::*;
/// Trait for thing that may output MIDI.
pub trait HasMidiOuts {
fn midi_outs (&self) -> &Vec<JackMidiOut>;
fn midi_outs_mut (&mut self) -> &mut Vec<JackMidiOut>;
fn has_midi_outs (&self) -> bool {
!self.midi_outs().is_empty()
}
/// Buffer for serializing a MIDI event. FIXME rename
fn midi_note (&mut self) -> &mut Vec<u8>;
}
pub trait MidiPlaybackApi: HasPlayClip + HasClock + HasMidiOuts {
fn notes_out (&self) -> &Arc<RwLock<[bool;128]>>;
/// Clear the section of the output buffer that we will be using,
/// emitting "all notes off" at start of buffer if requested.
fn clear (
&mut self, scope: &ProcessScope, out: &mut [Vec<Vec<u8>>], reset: bool
) {
for frame in &mut out[0..scope.n_frames() as usize] {
frame.clear();
}
if reset {
all_notes_off(out);
}
}
/// Output notes from clip to MIDI output ports.
fn play (
&mut self, scope: &ProcessScope, note_buf: &mut Vec<u8>, out: &mut [Vec<Vec<u8>>]
) -> bool {
if !self.clock().is_rolling() {
return false
}
// If a clip is playing, write a chunk of MIDI events from it to the output buffer.
// If no clip is playing, prepare for switchover immediately.
self.play_clip().as_ref().map_or(true, |(started, clip)|{
self.play_chunk(scope, note_buf, out, started, clip)
})
}
/// Handle switchover from current to next playing clip.
fn switchover (
&mut self, scope: &ProcessScope, note_buf: &mut Vec<u8>, out: &mut [Vec<Vec<u8>>]
) {
if !self.clock().is_rolling() {
return
}
let sample0 = scope.last_frame_time() as usize;
//let samples = scope.n_frames() as usize;
if let Some((start_at, clip)) = &self.next_clip() {
let start = start_at.sample.get() as usize;
let sample = self.clock().started.read().unwrap()
.as_ref().unwrap().sample.get() as usize;
// If it's time to switch to the next clip:
if start <= sample0.saturating_sub(sample) {
// Samples elapsed since clip was supposed to start
let _skipped = sample0 - start;
// Switch over to enqueued clip
let started = Moment::from_sample(self.clock().timebase(), start as f64);
// Launch enqueued clip
*self.play_clip_mut() = Some((started, clip.clone()));
// Unset enqueuement (TODO: where to implement looping?)
*self.next_clip_mut() = None;
// Fill in remaining ticks of chunk from next clip.
self.play(scope, note_buf, out);
}
}
}
fn play_chunk (
&self,
scope: &ProcessScope,
note_buf: &mut Vec<u8>,
out: &mut [Vec<Vec<u8>>],
started: &Moment,
clip: &Option<Arc<RwLock<MidiClip>>>
) -> bool {
// First sample to populate. Greater than 0 means that the first
// pulse of the clip falls somewhere in the middle of the chunk.
let sample = (scope.last_frame_time() as usize).saturating_sub(
started.sample.get() as usize +
self.clock().started.read().unwrap().as_ref().unwrap().sample.get() as usize
);
// Iterator that emits sample (index into output buffer at which to write MIDI event)
// paired with pulse (index into clip from which to take the MIDI event) for each
// sample of the output buffer that corresponds to a MIDI pulse.
let pulses = self.clock().timebase().pulses_between_samples(sample, sample + scope.n_frames() as usize);
// Notes active during current chunk.
let notes = &mut self.notes_out().write().unwrap();
let length = clip.as_ref().map_or(0, |p|p.read().unwrap().length);
for (sample, pulse) in pulses {
// If a next clip is enqueued, and we're past the end of the current one,
// break the loop here (FIXME count pulse correctly)
let past_end = if clip.is_some() { pulse >= length } else { true };
if self.next_clip().is_some() && past_end {
return true
}
// If there's a currently playing clip, output notes from it to buffer:
if let Some(ref clip) = clip {
Self::play_pulse(clip, pulse, sample, note_buf, out, notes)
}
}
false
}
fn play_pulse (
clip: &RwLock<MidiClip>,
pulse: usize,
sample: usize,
note_buf: &mut Vec<u8>,
out: &mut [Vec<Vec<u8>>],
notes: &mut [bool;128]
) {
// Source clip from which the MIDI events will be taken.
let clip = clip.read().unwrap();
// Clip with zero length is not processed
if clip.length > 0 {
// Current pulse index in source clip
let pulse = pulse % clip.length;
// Output each MIDI event from clip at appropriate frames of output buffer:
for message in clip.notes[pulse].iter() {
// Clear output buffer for this MIDI event.
note_buf.clear();
// TODO: support MIDI channels other than CH1.
let channel = 0.into();
// Serialize MIDI event into message buffer.
LiveEvent::Midi { channel, message: *message }
.write(note_buf)
.unwrap();
// Append serialized message to output buffer.
out[sample].push(note_buf.clone());
// Update the list of currently held notes.
update_keys(&mut*notes, message);
}
}
}
/// Write a chunk of MIDI data from the output buffer to all assigned output ports.
fn write (&mut self, scope: &ProcessScope, out: &[Vec<Vec<u8>>]) {
let samples = scope.n_frames() as usize;
for port in self.midi_outs_mut().iter_mut() {
Self::write_port(&mut port.port_mut().writer(scope), samples, out)
}
}
/// Write a chunk of MIDI data from the output buffer to an output port.
fn write_port (writer: &mut MidiWriter, samples: usize, out: &[Vec<Vec<u8>>]) {
for (time, events) in out.iter().enumerate().take(samples) {
for bytes in events.iter() {
writer.write(&RawMidi { time: time as u32, bytes }).unwrap_or_else(|_|{
panic!("Failed to write MIDI data: {bytes:?}");
});
}
}
}
}

View file

@ -0,0 +1,23 @@
pub struct Note;
impl Note {
pub const NAMES: [&str; 128] = [
"C0", "C#0", "D0", "D#0", "E0", "F0", "F#0", "G0", "G#0", "A0", "A#0", "B0",
"C1", "C#1", "D1", "D#1", "E1", "F1", "F#1", "G1", "G#1", "A1", "A#1", "B1",
"C2", "C#2", "D2", "D#2", "E2", "F2", "F#2", "G2", "G#2", "A2", "A#2", "B2",
"C3", "C#3", "D3", "D#3", "E3", "F3", "F#3", "G3", "G#3", "A3", "A#3", "B3",
"C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4",
"C5", "C#5", "D5", "D#5", "E5", "F5", "F#5", "G5", "G#5", "A5", "A#5", "B5",
"C6", "C#6", "D6", "D#6", "E6", "F6", "F#6", "G6", "G#6", "A6", "A#6", "B6",
"C7", "C#7", "D7", "D#7", "E7", "F7", "F#7", "G7", "G#7", "A7", "A#7", "B7",
"C8", "C#8", "D8", "D#8", "E8", "F8", "F#8", "G8", "G#8", "A8", "A#8", "B8",
"C9", "C#9", "D9", "D#9", "E9", "F9", "F#9", "G9", "G#9", "A9", "A#9", "B9",
"C10", "C#10", "D10", "D#10", "E10", "F10", "F#10", "G10",
];
pub fn pitch_to_name (n: usize) -> &'static str {
if n > 127 {
panic!("to_note_name({n}): must be 0-127");
}
Self::NAMES[n]
}
}

View file

@ -0,0 +1,192 @@
//! MIDI player
use crate::*;
pub trait HasPlayer {
fn player (&self) -> &impl MidiPlayerApi;
fn player_mut (&mut self) -> &mut impl MidiPlayerApi;
}
#[macro_export] macro_rules! has_player {
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? HasPlayer for $Struct $(<$($L),*$($T),*>)? {
fn player (&$self) -> &impl MidiPlayerApi { &$cb }
fn player_mut (&mut $self) -> &mut impl MidiPlayerApi { &mut$cb }
}
}
}
pub trait MidiPlayerApi: MidiRecordApi + MidiPlaybackApi + Send + Sync {}
impl MidiPlayerApi for MidiPlayer {}
/// Contains state for playing a clip
pub struct MidiPlayer {
/// State of clock and playhead
pub clock: Clock,
/// Start time and clip being played
pub play_clip: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
/// Start time and next clip
pub next_clip: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
/// Play input through output.
pub monitoring: bool,
/// Write input to sequence.
pub recording: bool,
/// Overdub input to sequence.
pub overdub: bool,
/// Send all notes off
pub reset: bool, // TODO?: after Some(nframes)
/// Record from MIDI ports to current sequence.
pub midi_ins: Vec<JackMidiIn>,
/// Play from current sequence to MIDI ports
pub midi_outs: Vec<JackMidiOut>,
/// Notes currently held at input
pub notes_in: Arc<RwLock<[bool; 128]>>,
/// Notes currently held at output
pub notes_out: Arc<RwLock<[bool; 128]>>,
/// MIDI output buffer
pub note_buf: Vec<u8>,
}
impl Default for MidiPlayer {
fn default () -> Self {
Self {
play_clip: None,
next_clip: None,
recording: false,
monitoring: false,
overdub: false,
notes_in: RwLock::new([false;128]).into(),
notes_out: RwLock::new([false;128]).into(),
note_buf: vec![0;8],
reset: true,
midi_ins: vec![],
midi_outs: vec![],
clock: Clock::default(),
}
}
}
impl MidiPlayer {
pub fn new (
name: impl AsRef<str>,
jack: &Jack,
clock: Option<&Clock>,
clip: Option<&Arc<RwLock<MidiClip>>>,
midi_from: &[PortConnect],
midi_to: &[PortConnect],
) -> Usually<Self> {
let _name = name.as_ref();
let clock = clock.cloned().unwrap_or_default();
Ok(Self {
midi_ins: vec![JackMidiIn::new(jack, format!("M/{}", name.as_ref()), midi_from)?,],
midi_outs: vec![JackMidiOut::new(jack, format!("{}/M", name.as_ref()), midi_to)?, ],
play_clip: clip.map(|clip|(Moment::zero(&clock.timebase), Some(clip.clone()))),
clock,
note_buf: vec![0;8],
reset: true,
recording: false,
monitoring: false,
overdub: false,
next_clip: None,
notes_in: RwLock::new([false;128]).into(),
notes_out: RwLock::new([false;128]).into(),
})
}
}
impl std::fmt::Debug for MidiPlayer {
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
f.debug_struct("MidiPlayer")
.field("clock", &self.clock)
.field("play_clip", &self.play_clip)
.field("next_clip", &self.next_clip)
.finish()
}
}
has_clock!(|self: MidiPlayer|self.clock);
impl HasMidiIns for MidiPlayer {
fn midi_ins (&self) -> &Vec<JackMidiIn> { &self.midi_ins }
fn midi_ins_mut (&mut self) -> &mut Vec<JackMidiIn> { &mut self.midi_ins }
}
impl HasMidiOuts for MidiPlayer {
fn midi_outs (&self) -> &Vec<JackMidiOut> { &self.midi_outs }
fn midi_outs_mut (&mut self) -> &mut Vec<JackMidiOut> { &mut self.midi_outs }
fn midi_note (&mut self) -> &mut Vec<u8> { &mut self.note_buf }
}
/// Hosts the JACK callback for a single MIDI player
pub struct PlayerAudio<'a, T: MidiPlayerApi>(
/// Player
pub &'a mut T,
/// Note buffer
pub &'a mut Vec<u8>,
/// Note chunk buffer
pub &'a mut Vec<Vec<Vec<u8>>>,
);
/// JACK process callback for a sequencer's clip player/recorder.
impl<T: MidiPlayerApi> Audio for PlayerAudio<'_, T> {
fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
let model = &mut self.0;
let note_buf = &mut self.1;
let midi_buf = &mut self.2;
// Clear output buffer(s)
model.clear(scope, midi_buf, false);
// Write chunk of clip to output, handle switchover
if model.play(scope, note_buf, midi_buf) {
model.switchover(scope, note_buf, midi_buf);
}
if model.has_midi_ins() {
if model.recording() || model.monitoring() {
// Record and/or monitor input
model.record(scope, midi_buf)
} else if model.has_midi_outs() && model.monitoring() {
// Monitor input to output
model.monitor(scope, midi_buf)
}
}
// Write to output port(s)
model.write(scope, midi_buf);
Control::Continue
}
}
impl MidiRecordApi for MidiPlayer {
fn recording (&self) -> bool {
self.recording
}
fn recording_mut (&mut self) -> &mut bool {
&mut self.recording
}
fn monitoring (&self) -> bool {
self.monitoring
}
fn monitoring_mut (&mut self) -> &mut bool {
&mut self.monitoring
}
fn overdub (&self) -> bool {
self.overdub
}
fn overdub_mut (&mut self) -> &mut bool {
&mut self.overdub
}
fn notes_in (&self) -> &Arc<RwLock<[bool; 128]>> {
&self.notes_in
}
}
impl MidiPlaybackApi for MidiPlayer {
fn notes_out (&self) -> &Arc<RwLock<[bool; 128]>> {
&self.notes_out
}
}
impl HasPlayClip for MidiPlayer {
fn reset (&self) -> bool {
self.reset
}
fn reset_mut (&mut self) -> &mut bool {
&mut self.reset
}
fn play_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
&self.play_clip
}
fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
&mut self.play_clip
}
fn next_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
&self.next_clip
}
fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
&mut self.next_clip
}
}

View file

@ -0,0 +1,50 @@
use crate::*;
#[derive(Debug, Clone)]
pub struct MidiPointModel {
/// Time coordinate of cursor
pub time_pos: Arc<AtomicUsize>,
/// Note coordinate of cursor
pub note_pos: Arc<AtomicUsize>,
/// Length of note that will be inserted, in pulses
pub note_len: Arc<AtomicUsize>,
}
impl Default for MidiPointModel {
fn default () -> Self {
Self {
time_pos: Arc::new(0.into()),
note_pos: Arc::new(36.into()),
note_len: Arc::new(24.into()),
}
}
}
pub trait NotePoint {
fn note_len (&self) -> usize;
fn set_note_len (&self, x: usize);
fn note_pos (&self) -> usize;
fn set_note_pos (&self, x: usize);
fn note_end (&self) -> usize { self.note_pos() + self.note_len() }
}
pub trait TimePoint {
fn time_pos (&self) -> usize;
fn set_time_pos (&self, x: usize);
}
pub trait MidiPoint: NotePoint + TimePoint {}
impl<T: NotePoint + TimePoint> MidiPoint for T {}
impl NotePoint for MidiPointModel {
fn note_len (&self) -> usize { self.note_len.load(Relaxed)}
fn set_note_len (&self, x: usize) { self.note_len.store(x, Relaxed) }
fn note_pos (&self) -> usize { self.note_pos.load(Relaxed).min(127) }
fn set_note_pos (&self, x: usize) { self.note_pos.store(x.min(127), Relaxed) }
}
impl TimePoint for MidiPointModel {
fn time_pos (&self) -> usize { self.time_pos.load(Relaxed) }
fn set_time_pos (&self, x: usize) { self.time_pos.store(x, Relaxed) }
}

View file

@ -0,0 +1,561 @@
use crate::*;
pub type ClipPool = Vec<Arc<RwLock<MidiClip>>>;
pub trait HasClips {
fn clips <'a> (&'a self) -> std::sync::RwLockReadGuard<'a, ClipPool>;
fn clips_mut <'a> (&'a self) -> std::sync::RwLockWriteGuard<'a, ClipPool>;
fn add_clip (&self) -> (usize, Arc<RwLock<MidiClip>>) {
let clip = Arc::new(RwLock::new(MidiClip::new("Clip", true, 384, None, None)));
self.clips_mut().push(clip.clone());
(self.clips().len() - 1, clip)
}
}
#[macro_export] macro_rules! has_clips {
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? {
fn clips <'a> (&'a $self) -> std::sync::RwLockReadGuard<'a, ClipPool> {
$cb.read().unwrap()
}
fn clips_mut <'a> (&'a $self) -> std::sync::RwLockWriteGuard<'a, ClipPool> {
$cb.write().unwrap()
}
}
}
}
#[derive(Debug)]
pub struct MidiPool {
pub visible: bool,
/// Collection of clips
pub clips: Arc<RwLock<Vec<Arc<RwLock<MidiClip>>>>>,
/// Selected clip
pub clip: AtomicUsize,
/// Mode switch
pub mode: Option<PoolMode>,
keys: SourceIter<'static>,
keys_rename: SourceIter<'static>,
keys_length: SourceIter<'static>,
keys_file: SourceIter<'static>,
}
/// Modes for clip pool
#[derive(Debug, Clone)]
pub enum PoolMode {
/// Renaming a pattern
Rename(usize, Arc<str>),
/// Editing the length of a pattern
Length(usize, usize, ClipLengthFocus),
/// Load clip from disk
Import(usize, FileBrowser),
/// Save clip to disk
Export(usize, FileBrowser),
}
impl Default for MidiPool {
fn default () -> Self {
Self {
visible: true,
clips: Arc::from(RwLock::from(vec![])),
clip: 0.into(),
mode: None,
keys: SourceIter(KEYS_POOL),
keys_rename: SourceIter(KEYS_RENAME),
keys_length: SourceIter(KEYS_LENGTH),
keys_file: SourceIter(KEYS_FILE),
}
}
}
from!(|clip:&Arc<RwLock<MidiClip>>|MidiPool = {
let model = Self::default();
model.clips.write().unwrap().push(clip.clone());
model.clip.store(1, Relaxed);
model
});
has_clips!(|self: MidiPool|self.clips);
has_clip!(|self: MidiPool|self.clips().get(self.clip_index()).map(|c|c.clone()));
impl MidiPool {
fn clip_index (&self) -> usize { self.clip.load(Relaxed) }
fn set_clip_index (&self, value: usize) { self.clip.store(value, Relaxed); }
fn mode (&self) -> &Option<PoolMode> { &self.mode }
fn mode_mut (&mut self) -> &mut Option<PoolMode> { &mut self.mode }
fn begin_clip_length (&mut self) {
let length = self.clips()[self.clip_index()].read().unwrap().length;
*self.mode_mut() = Some(PoolMode::Length(
self.clip_index(),
length,
ClipLengthFocus::Bar
));
}
fn begin_clip_rename (&mut self) {
let name = self.clips()[self.clip_index()].read().unwrap().name.clone();
*self.mode_mut() = Some(PoolMode::Rename(
self.clip_index(),
name
));
}
fn begin_import (&mut self) -> Usually<()> {
*self.mode_mut() = Some(PoolMode::Import(
self.clip_index(),
FileBrowser::new(None)?
));
Ok(())
}
fn begin_export (&mut self) -> Usually<()> {
*self.mode_mut() = Some(PoolMode::Export(
self.clip_index(),
FileBrowser::new(None)?
));
Ok(())
}
}
/// Displays and edits clip length.
#[derive(Clone)]
pub struct ClipLength {
/// Pulses per beat (quaver)
ppq: usize,
/// Beats per bar
bpb: usize,
/// Length of clip in pulses
pulses: usize,
/// Selected subdivision
focus: Option<ClipLengthFocus>,
}
impl ClipLength {
fn _new (pulses: usize, focus: Option<ClipLengthFocus>) -> Self {
Self { ppq: PPQ, bpb: 4, pulses, focus }
}
fn bars (&self) -> usize {
self.pulses / (self.bpb * self.ppq)
}
fn beats (&self) -> usize {
(self.pulses % (self.bpb * self.ppq)) / self.ppq
}
fn ticks (&self) -> usize {
self.pulses % self.ppq
}
fn bars_string (&self) -> Arc<str> {
format!("{}", self.bars()).into()
}
fn beats_string (&self) -> Arc<str> {
format!("{}", self.beats()).into()
}
fn ticks_string (&self) -> Arc<str> {
format!("{:>02}", self.ticks()).into()
}
}
/// Focused field of `ClipLength`
#[derive(Copy, Clone, Debug)]
pub enum ClipLengthFocus {
/// Editing the number of bars
Bar,
/// Editing the number of beats
Beat,
/// Editing the number of ticks
Tick,
}
impl ClipLengthFocus {
fn next (&mut self) {
*self = match self { Self::Bar => Self::Beat, Self::Beat => Self::Tick, Self::Tick => Self::Bar, }
}
fn prev (&mut self) {
*self = match self { Self::Bar => Self::Tick, Self::Beat => Self::Bar, Self::Tick => Self::Beat, }
}
}
pub struct PoolView<'a>(pub bool, pub &'a MidiPool);
content!(TuiOut: |self: PoolView<'a>| {
let Self(compact, model) = self;
let MidiPool { clips, .. } = self.1;
//let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||Tui::g(32).into());
let on_bg = |x|x;//Bsp::b(Repeat(" "), Tui::bg(color.darkest.rgb, x));
let border = |x|x;//Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)).enclose(x);
let iter = | |model.clips().clone().into_iter();
let height = clips.read().unwrap().len() as u16;
Tui::bg(Color::Reset, Fixed::y(height, on_bg(border(Map::new(iter, move|clip: Arc<RwLock<MidiClip>>, i|{
let item_height = 1;
let item_offset = i as u16 * item_height;
let selected = i == model.clip_index();
let MidiClip { ref name, color, length, .. } = *clip.read().unwrap();
let bg = if selected { color.light.rgb } else { color.base.rgb };
let fg = color.lightest.rgb;
let name = if *compact { format!(" {i:>3}") } else { format!(" {i:>3} {name}") };
let length = if *compact { String::default() } else { format!("{length} ") };
Fixed::y(1, map_south(item_offset, item_height, Tui::bg(bg, lay!(
Fill::x(Align::w(Tui::fg(fg, Tui::bold(selected, name)))),
Fill::x(Align::e(Tui::fg(fg, Tui::bold(selected, length)))),
Fill::x(Align::w(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), ""))))),
Fill::x(Align::e(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), ""))))),
))))
})))))
});
content!(TuiOut: |self: ClipLength| {
let bars = ||self.bars_string();
let beats = ||self.beats_string();
let ticks = ||self.ticks_string();
match self.focus {
None =>
row!(" ", bars(), ".", beats(), ".", ticks()),
Some(ClipLengthFocus::Bar) =>
row!("[", bars(), "]", beats(), ".", ticks()),
Some(ClipLengthFocus::Beat) =>
row!(" ", bars(), "[", beats(), "]", ticks()),
Some(ClipLengthFocus::Tick) =>
row!(" ", bars(), ".", beats(), "[", ticks()),
}
});
handle!(TuiIn: |self: MidiPool, input|{
Ok(if let Some(command) = match self.mode() {
Some(PoolMode::Rename(..)) => self.keys_rename,
Some(PoolMode::Length(..)) => self.keys_length,
Some(PoolMode::Import(..)) | Some(PoolMode::Export(..)) => self.keys_file,
_ => self.keys
}.command::<Self, PoolCommand, TuiIn>(self, input) {
let _undo = command.execute(self)?;
Some(true)
} else {
None
})
});
provide!(bool: |self: MidiPool| {});
impl MidiPool {
pub fn new_clip (&self) -> MidiClip {
MidiClip::new("Clip", true, 4 * PPQ, None, Some(ItemPalette::random()))
}
pub fn cloned_clip (&self) -> MidiClip {
let index = self.clip_index();
let mut clip = self.clips()[index].read().unwrap().duplicate();
clip.color = ItemPalette::random_near(clip.color, 0.25);
clip
}
pub fn add_new_clip (&self) -> (usize, Arc<RwLock<MidiClip>>) {
let clip = Arc::new(RwLock::new(self.new_clip()));
let index = {
let mut clips = self.clips.write().unwrap();
clips.push(clip.clone());
clips.len().saturating_sub(1)
};
self.clip.store(index, Relaxed);
(index, clip)
}
}
provide!(MidiClip: |self: MidiPool| {
":new-clip" => self.new_clip(),
":cloned-clip" => self.cloned_clip(),
});
provide!(PathBuf: |self: MidiPool| {});
provide!(Arc<str>: |self: MidiPool| {});
provide!(usize: |self: MidiPool| {
":current" => 0,
":after" => 0,
":previous" => 0,
":next" => 0
});
provide!(ItemColor: |self: MidiPool| {
":random-color" => ItemColor::random()
});
#[derive(Clone, PartialEq, Debug)] pub enum PoolCommand {
/// Toggle visibility of pool
Show(bool),
/// Select a clip from the clip pool
Select(usize),
/// Rename a clip
Rename(ClipRenameCommand),
/// Change the length of a clip
Length(ClipLengthCommand),
/// Import from file
Import(FileBrowserCommand),
/// Export to file
Export(FileBrowserCommand),
/// Update the contents of the clip pool
Clip(PoolClipCommand),
}
atom_command!(PoolCommand: |state: MidiPool| {
("show" [a: bool] Some(Self::Show(a.expect("no flag"))))
("select" [i: usize] Some(Self::Select(i.expect("no index"))))
("rename" [,..a] ClipRenameCommand::try_from_expr(state, a).map(Self::Rename))
("length" [,..a] ClipLengthCommand::try_from_expr(state, a).map(Self::Length))
("import" [,..a] FileBrowserCommand::try_from_expr(state, a).map(Self::Import))
("export" [,..a] FileBrowserCommand::try_from_expr(state, a).map(Self::Export))
("clip" [,..a] PoolClipCommand::try_from_expr(state, a).map(Self::Clip))
});
command!(|self: PoolCommand, state: MidiPool|{
use PoolCommand::*;
match self {
Rename(ClipRenameCommand::Begin) => { state.begin_clip_rename(); None }
Rename(command) => command.delegate(state, Rename)?,
Length(ClipLengthCommand::Begin) => { state.begin_clip_length(); None },
Length(command) => command.delegate(state, Length)?,
Import(FileBrowserCommand::Begin) => { state.begin_import()?; None },
Import(command) => command.delegate(state, Import)?,
Export(FileBrowserCommand::Begin) => { state.begin_export()?; None },
Export(command) => command.delegate(state, Export)?,
Clip(command) => command.execute(state)?.map(Clip),
Show(visible) => { state.visible = visible; Some(Self::Show(!visible)) },
Select(clip) => { state.set_clip_index(clip); None },
}
});
#[derive(Clone, Debug, PartialEq)] pub enum PoolClipCommand {
Add(usize, MidiClip),
Delete(usize),
Swap(usize, usize),
Import(usize, PathBuf),
Export(usize, PathBuf),
SetName(usize, Arc<str>),
SetLength(usize, usize),
SetColor(usize, ItemColor),
}
atom_command!(PoolClipCommand: |state: MidiPool| {
("add" [i: usize, c: MidiClip]
Some(Self::Add(i.expect("no index"), c.expect("no clip"))))
("delete" [i: usize]
Some(Self::Delete(i.expect("no index"))))
("swap" [a: usize, b: usize]
Some(Self::Swap(a.expect("no index"), b.expect("no index"))))
("import" [i: usize, p: PathBuf]
Some(Self::Import(i.expect("no index"), p.expect("no path"))))
("export" [i: usize, p: PathBuf]
Some(Self::Export(i.expect("no index"), p.expect("no path"))))
("set-name" [i: usize, n: Arc<str>]
Some(Self::SetName(i.expect("no index"), n.expect("no name"))))
("set-length" [i: usize, l: usize]
Some(Self::SetLength(i.expect("no index"), l.expect("no length"))))
("set-color" [i: usize, c: ItemColor]
Some(Self::SetColor(i.expect("no index"), c.expect("no color"))))
});
impl<T: HasClips> Command<T> for PoolClipCommand {
fn execute (self, model: &mut T) -> Perhaps<Self> {
use PoolClipCommand::*;
Ok(match self {
Add(mut index, clip) => {
let clip = Arc::new(RwLock::new(clip));
let mut clips = model.clips_mut();
if index >= clips.len() {
index = clips.len();
clips.push(clip)
} else {
clips.insert(index, clip);
}
Some(Self::Delete(index))
},
Delete(index) => {
let clip = model.clips_mut().remove(index).read().unwrap().clone();
Some(Self::Add(index, clip))
},
Swap(index, other) => {
model.clips_mut().swap(index, other);
Some(Self::Swap(index, other))
},
Import(index, path) => {
let bytes = std::fs::read(&path)?;
let smf = Smf::parse(bytes.as_slice())?;
let mut t = 0u32;
let mut events = vec![];
for track in smf.tracks.iter() {
for event in track.iter() {
t += event.delta.as_int();
if let TrackEventKind::Midi { channel, message } = event.kind {
events.push((t, channel.as_int(), message));
}
}
}
let mut clip = MidiClip::new("imported", true, t as usize + 1, None, None);
for event in events.iter() {
clip.notes[event.0 as usize].push(event.2);
}
Self::Add(index, clip).execute(model)?
},
Export(_index, _path) => {
todo!("export clip to midi file");
},
SetName(index, name) => {
let clip = &mut model.clips_mut()[index];
let old_name = clip.read().unwrap().name.clone();
clip.write().unwrap().name = name;
Some(Self::SetName(index, old_name))
},
SetLength(index, length) => {
let clip = &mut model.clips_mut()[index];
let old_len = clip.read().unwrap().length;
clip.write().unwrap().length = length;
Some(Self::SetLength(index, old_len))
},
SetColor(index, color) => {
let mut color = ItemPalette::from(color);
std::mem::swap(&mut color, &mut model.clips()[index].write().unwrap().color);
Some(Self::SetColor(index, color.base))
},
})
}
}
#[derive(Clone, Debug, PartialEq)] pub enum ClipRenameCommand {
Begin,
Cancel,
Confirm,
Set(Arc<str>),
}
atom_command!(ClipRenameCommand: |state: MidiPool| {
("begin" [] Some(Self::Begin))
("cancel" [] Some(Self::Cancel))
("confirm" [] Some(Self::Confirm))
("set" [n: Arc<str>] Some(Self::Set(n.expect("no name"))))
});
command!(|self: ClipRenameCommand, state: MidiPool|if let Some(
PoolMode::Rename(clip, ref mut old_name)
) = state.mode_mut().clone() {
match self {
Self::Set(s) => {
state.clips()[clip].write().unwrap().name = s;
return Ok(Some(Self::Set(old_name.clone().into())))
},
Self::Confirm => {
let old_name = old_name.clone();
*state.mode_mut() = None;
return Ok(Some(Self::Set(old_name)))
},
Self::Cancel => {
state.clips()[clip].write().unwrap().name = old_name.clone().into();
return Ok(None)
},
_ => unreachable!()
}
} else {
unreachable!()
});
#[derive(Copy, Clone, Debug, PartialEq)] pub enum ClipLengthCommand {
Begin,
Cancel,
Set(usize),
Next,
Prev,
Inc,
Dec,
}
atom_command!(ClipLengthCommand: |state: MidiPool| {
("begin" [] Some(Self::Begin))
("cancel" [] Some(Self::Cancel))
("next" [] Some(Self::Next))
("prev" [] Some(Self::Prev))
("inc" [] Some(Self::Inc))
("dec" [] Some(Self::Dec))
("set" [l: usize] Some(Self::Set(l.expect("no length"))))
});
command!(|self: ClipLengthCommand, state: MidiPool|{
use ClipLengthCommand::*;
use ClipLengthFocus::*;
if let Some(
PoolMode::Length(clip, ref mut length, ref mut focus)
) = state.mode_mut().clone() {
match self {
Cancel => { *state.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 old_length;
{
let clip = state.clips()[clip].clone();//.write().unwrap();
old_length = Some(clip.read().unwrap().length);
clip.write().unwrap().length = length;
}
*state.mode_mut() = None;
return Ok(old_length.map(Self::Set))
},
_ => unreachable!()
}
} else {
unreachable!();
}
None
});
atom_command!(FileBrowserCommand: |state: MidiPool| {
("begin" [] Some(Self::Begin))
("cancel" [] Some(Self::Cancel))
("confirm" [] Some(Self::Confirm))
("select" [i: usize] Some(Self::Select(i.expect("no index"))))
("chdir" [p: PathBuf] Some(Self::Chdir(p.expect("no path"))))
("filter" [f: Arc<str>] Some(Self::Filter(f.expect("no filter"))))
});
command!(|self: FileBrowserCommand, state: MidiPool|{
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;
PoolClipCommand::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
});
///////////////////////////////////////////////////////////////////////////////////////////////////
//fn to_clips_command (state: &MidiPool, input: &Event) -> Option<PoolCommand> {
//use KeyCode::{Up, Down, Delete, Char};
//use PoolCommand as Cmd;
//let index = state.clip_index();
//let count = state.clips().len();
//Some(match input {
//kpat!(Char('n')) => Cmd::Rename(ClipRenameCommand::Begin),
//kpat!(Char('t')) => Cmd::Length(ClipLengthCommand::Begin),
//kpat!(Char('m')) => Cmd::Import(FileBrowserCommand::Begin),
//kpat!(Char('x')) => Cmd::Export(FileBrowserCommand::Begin),
//kpat!(Char('c')) => Cmd::Clip(PoolClipCommand::SetColor(index, ItemColor::random())),
//kpat!(Char('[')) | kpat!(Up) => Cmd::Select(
//index.overflowing_sub(1).0.min(state.clips().len() - 1)
//),
//kpat!(Char(']')) | kpat!(Down) => Cmd::Select(
//index.saturating_add(1) % state.clips().len()
//),
//kpat!(Char('<')) => if index > 1 {
//state.set_clip_index(state.clip_index().saturating_sub(1));
//Cmd::Clip(PoolClipCommand::Swap(index - 1, index))
//} else {
//return None
//},
//kpat!(Char('>')) => if index < count.saturating_sub(1) {
//state.set_clip_index(state.clip_index() + 1);
//Cmd::Clip(PoolClipCommand::Swap(index + 1, index))
//} else {
//return None
//},
//kpat!(Delete) => if index > 0 {
//state.set_clip_index(index.min(count.saturating_sub(1)));
//Cmd::Clip(PoolClipCommand::Delete(index))
//} else {
//return None
//},
//kpat!(Char('a')) | kpat!(Shift-Char('A')) => Cmd::Clip(PoolClipCommand::Add(count, MidiClip::new(
//"Clip", true, 4 * PPQ, None, Some(ItemPalette::random())
//))),
//kpat!(Char('i')) => Cmd::Clip(PoolClipCommand::Add(index + 1, MidiClip::new(
//"Clip", true, 4 * PPQ, None, Some(ItemPalette::random())
//))),
//kpat!(Char('d')) | kpat!(Shift-Char('D')) => {
//let mut clip = state.clips()[index].read().unwrap().duplicate();
//clip.color = ItemPalette::random_near(clip.color, 0.25);
//Cmd::Clip(PoolClipCommand::Add(index + 1, clip))
//},
//_ => return None
//})
//}

View file

@ -0,0 +1,79 @@
use crate::*;
#[derive(Debug, Clone)]
pub struct MidiRangeModel {
pub time_len: Arc<AtomicUsize>,
/// Length of visible time axis
pub time_axis: Arc<AtomicUsize>,
/// Earliest time displayed
pub time_start: Arc<AtomicUsize>,
/// Time step
pub time_zoom: Arc<AtomicUsize>,
/// Auto rezoom to fit in time axis
pub time_lock: Arc<AtomicBool>,
/// Length of visible note axis
pub note_axis: Arc<AtomicUsize>,
// Lowest note displayed
pub note_lo: Arc<AtomicUsize>,
}
from!(|data:(usize, bool)|MidiRangeModel = Self {
time_len: Arc::new(0.into()),
note_axis: Arc::new(0.into()),
note_lo: Arc::new(0.into()),
time_axis: Arc::new(0.into()),
time_start: Arc::new(0.into()),
time_zoom: Arc::new(data.0.into()),
time_lock: Arc::new(data.1.into()),
});
pub trait TimeRange {
fn time_len (&self) -> &AtomicUsize;
fn time_zoom (&self) -> &AtomicUsize;
fn time_lock (&self) -> &AtomicBool;
fn time_start (&self) -> &AtomicUsize;
fn time_axis (&self) -> &AtomicUsize;
fn time_end (&self) -> usize {
self.time_start().get() + self.time_axis().get() * self.time_zoom().get()
}
}
pub trait NoteRange {
fn note_lo (&self) -> &AtomicUsize;
fn note_axis (&self) -> &AtomicUsize;
fn note_hi (&self) -> usize {
(self.note_lo().get() + self.note_axis().get().saturating_sub(1)).min(127)
}
}
pub trait MidiRange: TimeRange + NoteRange {}
impl<T: TimeRange + NoteRange> MidiRange for T {}
impl TimeRange for MidiRangeModel {
fn time_len (&self) -> &AtomicUsize { &self.time_len }
fn time_zoom (&self) -> &AtomicUsize { &self.time_zoom }
fn time_lock (&self) -> &AtomicBool { &self.time_lock }
fn time_start (&self) -> &AtomicUsize { &self.time_start }
fn time_axis (&self) -> &AtomicUsize { &self.time_axis }
}
impl NoteRange for MidiRangeModel {
fn note_lo (&self) -> &AtomicUsize { &self.note_lo }
fn note_axis (&self) -> &AtomicUsize { &self.note_axis }
}
#[cfg(test)] #[test] fn test_midi_range () {
let model = MidiRangeModel::from((1, false));
let _ = model.time_len();
let _ = model.time_zoom();
let _ = model.time_lock();
let _ = model.time_start();
let _ = model.time_axis();
let _ = model.time_end();
let _ = model.note_lo();
let _ = model.note_axis();
let _ = model.note_hi();
}

View file

@ -0,0 +1,66 @@
use crate::*;
pub trait MidiViewer: HasSize<TuiOut> + MidiRange + MidiPoint + Debug + Send + Sync {
fn buffer_size (&self, clip: &MidiClip) -> (usize, usize);
fn redraw (&self);
fn clip (&self) -> &Option<Arc<RwLock<MidiClip>>>;
fn clip_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>>;
fn set_clip (&mut self, clip: Option<&Arc<RwLock<MidiClip>>>) {
*self.clip_mut() = clip.cloned();
self.redraw();
}
/// Make sure cursor is within note range
fn autoscroll (&self) {
let note_pos = self.note_pos().min(127);
let note_lo = self.note_lo().get();
let note_hi = self.note_hi();
if note_pos < note_lo {
self.note_lo().set(note_pos);
} else if note_pos > note_hi {
self.note_lo().set((note_lo + note_pos).saturating_sub(note_hi));
}
}
/// Make sure time range is within display
fn autozoom (&self) {
if self.time_lock().get() {
let time_len = self.time_len().get();
let time_axis = self.time_axis().get();
let time_zoom = self.time_zoom().get();
loop {
let time_zoom = self.time_zoom().get();
let time_area = time_axis * time_zoom;
if time_area > time_len {
let next_time_zoom = NoteDuration::prev(time_zoom);
if next_time_zoom <= 1 {
break
}
let next_time_area = time_axis * next_time_zoom;
if next_time_area >= time_len {
self.time_zoom().set(next_time_zoom);
} else {
break
}
} else if time_area < time_len {
let prev_time_zoom = NoteDuration::next(time_zoom);
if prev_time_zoom > 384 {
break
}
let prev_time_area = time_axis * prev_time_zoom;
if prev_time_area <= time_len {
self.time_zoom().set(prev_time_zoom);
} else {
break
}
}
}
if time_zoom != self.time_zoom().get() {
self.redraw()
}
}
//while time_len.div_ceil(time_zoom) > time_axis {
//println!("\r{time_len} {time_zoom} {time_axis}");
//time_zoom = Note::next(time_zoom);
//}
//self.time_zoom().set(time_zoom);
}
}

318
crates/midi/src/piano_h.rs Normal file
View file

@ -0,0 +1,318 @@
use crate::*;
use Color::*;
/// A clip, rendered as a horizontal piano roll.
pub struct PianoHorizontal {
pub clip: Option<Arc<RwLock<MidiClip>>>,
/// Buffer where the whole clip is rerendered on change
pub buffer: Arc<RwLock<BigBuffer>>,
/// Size of actual notes area
pub size: Measure<TuiOut>,
/// The display window
pub range: MidiRangeModel,
/// The note cursor
pub point: MidiPointModel,
/// The highlight color palette
pub color: ItemPalette,
/// Width of the keyboard
pub keys_width: u16,
}
impl PianoHorizontal {
pub fn new (clip: Option<&Arc<RwLock<MidiClip>>>) -> Self {
let size = Measure::new();
let mut range = MidiRangeModel::from((12, true));
range.time_axis = size.x.clone();
range.note_axis = size.y.clone();
let piano = Self {
keys_width: 5,
size,
range,
buffer: RwLock::new(Default::default()).into(),
point: MidiPointModel::default(),
clip: clip.cloned(),
color: clip.as_ref().map(|p|p.read().unwrap().color).unwrap_or(ItemPalette::G[64]),
};
piano.redraw();
piano
}
}
pub(crate) fn note_y_iter (note_lo: usize, note_hi: usize, y0: u16) -> impl Iterator<Item=(usize, u16, usize)> {
(note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n))
}
content!(TuiOut:|self: PianoHorizontal| Tui::bg(Tui::g(40), Bsp::s(
Bsp::e(
Fixed::x(5, format!("{}x{}", self.size.w(), self.size.h())),
self.timeline()
),
Bsp::e(
self.keys(),
self.size.of(Tui::bg(Tui::g(32), Bsp::b(
Fill::xy(self.notes()),
Fill::xy(self.cursor()),
)))
),
)));
impl PianoHorizontal {
/// Draw the piano roll background.
///
/// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄
fn draw_bg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize, note_len: usize) {
for (y, note) in (0..=127).rev().enumerate() {
for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) {
let cell = buf.get_mut(x, y).unwrap();
cell.set_bg(clip.color.darkest.rgb);
if time % 384 == 0 {
cell.set_fg(clip.color.darker.rgb);
cell.set_char('│');
} else if time % 96 == 0 {
cell.set_fg(clip.color.dark.rgb);
cell.set_char('╎');
} else if time % note_len == 0 {
cell.set_fg(clip.color.darker.rgb);
cell.set_char('┊');
} else if (127 - note) % 12 == 0 {
cell.set_fg(clip.color.darker.rgb);
cell.set_char('=');
} else if (127 - note) % 6 == 0 {
cell.set_fg(clip.color.darker.rgb);
cell.set_char('—');
} else {
cell.set_fg(clip.color.darker.rgb);
cell.set_char('·');
}
}
}
}
/// Draw the piano roll foreground.
///
/// This mode uses full blocks on note on and half blocks on legato: █▄ █▄ █▄
fn draw_fg (buf: &mut BigBuffer, clip: &MidiClip, zoom: usize) {
let style = Style::default().fg(clip.color.base.rgb);//.bg(Rgb(0, 0, 0));
let mut notes_on = [false;128];
for (x, time_start) in (0..clip.length).step_by(zoom).enumerate() {
for (_y, note) in (0..=127).rev().enumerate() {
if let Some(cell) = buf.get_mut(x, note) {
if notes_on[note] {
cell.set_char('▂');
cell.set_style(style);
}
}
}
let time_end = time_start + zoom;
for time in time_start..time_end.min(clip.length) {
for event in clip.notes[time].iter() {
match event {
MidiMessage::NoteOn { key, .. } => {
let note = key.as_int() as usize;
if let Some(cell) = buf.get_mut(x, note) {
cell.set_char('█');
cell.set_style(style);
}
notes_on[note] = true
},
MidiMessage::NoteOff { key, .. } => {
notes_on[key.as_int() as usize] = false
},
_ => {}
}
}
}
}
}
fn notes (&self) -> impl Content<TuiOut> {
let time_start = self.time_start().get();
let note_lo = self.note_lo().get();
let note_hi = self.note_hi();
let buffer = self.buffer.clone();
ThunkRender::new(move|to: &mut TuiOut|{
let source = buffer.read().unwrap();
let [x0, y0, w, _h] = to.area().xywh();
//if h as usize != note_axis {
//panic!("area height mismatch: {h} <> {note_axis}");
//}
for (area_x, screen_x) in (x0..x0+w).enumerate() {
for (area_y, screen_y, _note) in note_y_iter(note_lo, note_hi, y0) {
let source_x = time_start + area_x;
let source_y = note_hi - area_y;
// TODO: enable loop rollover:
//let source_x = (time_start + area_x) % source.width.max(1);
//let source_y = (note_hi - area_y) % source.height.max(1);
let is_in_x = source_x < source.width;
let is_in_y = source_y < source.height;
if is_in_x && is_in_y {
if let Some(source_cell) = source.get(source_x, source_y) {
if let Some(cell) = to.buffer.cell_mut(ratatui::prelude::Position::from((screen_x, screen_y))) {
*cell = source_cell.clone();
}
}
}
}
}
})
}
fn cursor (&self) -> impl Content<TuiOut> {
let style = Some(Style::default().fg(self.color.lightest.rgb));
let note_hi = self.note_hi();
let note_lo = self.note_lo().get();
let note_pos = self.note_pos();
let note_len = self.note_len();
let time_pos = self.time_pos();
let time_start = self.time_start().get();
let time_zoom = self.time_zoom().get();
ThunkRender::new(move|to: &mut TuiOut|{
let [x0, y0, w, _] = to.area().xywh();
for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) {
if note == note_pos {
for x in 0..w {
let screen_x = x0 + x;
let time_1 = time_start + x as usize * time_zoom;
let time_2 = time_1 + time_zoom;
if time_1 <= time_pos && time_pos < time_2 {
to.blit(&"", screen_x, screen_y, style);
let tail = note_len as u16 / time_zoom as u16;
for x_tail in (screen_x + 1)..(screen_x + tail) {
to.blit(&"", x_tail, screen_y, style);
}
break
}
}
break
}
}
})
}
fn keys (&self) -> impl Content<TuiOut> {
let state = self;
let color = state.color;
let note_lo = state.note_lo().get();
let note_hi = state.note_hi();
let note_pos = state.note_pos();
let key_style = Some(Style::default().fg(Rgb(192, 192, 192)).bg(Rgb(0, 0, 0)));
let off_style = Some(Style::default().fg(Tui::g(255)));
let on_style = Some(Style::default().fg(Rgb(255,0,0)).bg(color.base.rgb).bold());
Fill::y(Fixed::x(self.keys_width, ThunkRender::new(move|to: &mut TuiOut|{
let [x, y0, _w, _h] = to.area().xywh();
for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) {
to.blit(&to_key(note), x, screen_y, key_style);
if note > 127 {
continue
}
if note == note_pos {
to.blit(&format!("{:<5}", Note::pitch_to_name(note)), x, screen_y, on_style)
} else {
to.blit(&Note::pitch_to_name(note), x, screen_y, off_style)
};
}
})))
}
fn timeline (&self) -> impl Content<TuiOut> + '_ {
Fill::x(Fixed::y(1, ThunkRender::new(move|to: &mut TuiOut|{
let [x, y, w, _h] = to.area();
let style = Some(Style::default().dim());
let length = self.clip.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1);
for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) {
let t = area_x as usize * self.time_zoom().get();
if t < length {
to.blit(&"|", screen_x, y, style);
}
}
})))
}
}
has_size!(<TuiOut>|self:PianoHorizontal|&self.size);
impl TimeRange for PianoHorizontal {
fn time_len (&self) -> &AtomicUsize { self.range.time_len() }
fn time_zoom (&self) -> &AtomicUsize { self.range.time_zoom() }
fn time_lock (&self) -> &AtomicBool { self.range.time_lock() }
fn time_start (&self) -> &AtomicUsize { self.range.time_start() }
fn time_axis (&self) -> &AtomicUsize { self.range.time_axis() }
}
impl NoteRange for PianoHorizontal {
fn note_lo (&self) -> &AtomicUsize { self.range.note_lo() }
fn note_axis (&self) -> &AtomicUsize { self.range.note_axis() }
}
impl NotePoint for PianoHorizontal {
fn note_len (&self) -> usize { self.point.note_len() }
fn set_note_len (&self, x: usize) { self.point.set_note_len(x) }
fn note_pos (&self) -> usize { self.point.note_pos() }
fn set_note_pos (&self, x: usize) { self.point.set_note_pos(x) }
}
impl TimePoint for PianoHorizontal {
fn time_pos (&self) -> usize { self.point.time_pos() }
fn set_time_pos (&self, x: usize) { self.point.set_time_pos(x) }
}
impl MidiViewer for PianoHorizontal {
fn clip (&self) -> &Option<Arc<RwLock<MidiClip>>> {
&self.clip
}
fn clip_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>> {
&mut self.clip
}
/// Determine the required space to render the clip.
fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) {
(clip.length / self.range.time_zoom().get(), 128)
}
fn redraw (&self) {
*self.buffer.write().unwrap() = if let Some(clip) = self.clip.as_ref() {
let clip = clip.read().unwrap();
let buf_size = self.buffer_size(&clip);
let mut buffer = BigBuffer::from(buf_size);
let note_len = self.note_len();
let time_zoom = self.time_zoom().get();
self.time_len().set(clip.length);
PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom, note_len);
PianoHorizontal::draw_fg(&mut buffer, &clip, time_zoom);
buffer
} else {
Default::default()
}
}
fn set_clip (&mut self, clip: Option<&Arc<RwLock<MidiClip>>>) {
*self.clip_mut() = clip.cloned();
self.color = clip.map(|p|p.read().unwrap().color)
.unwrap_or(ItemPalette::G[64]);
self.redraw();
}
}
impl std::fmt::Debug for PianoHorizontal {
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
let buffer = self.buffer.read().unwrap();
f.debug_struct("PianoHorizontal")
.field("time_zoom", &self.range.time_zoom)
.field("buffer", &format!("{}x{}", buffer.width, buffer.height))
.finish()
}
}
// Update sequencer playhead indicator
//self.now().set(0.);
//if let Some((ref started_at, Some(ref playing))) = self.player.play_clip {
//let clip = clip.read().unwrap();
//if *playing.read().unwrap() == *clip {
//let pulse = self.current().pulse.get();
//let start = started_at.pulse.get();
//let now = (pulse - start) % clip.length as f64;
//self.now().set(now);
//}
//}
fn to_key (note: usize) -> &'static str {
match note % 12 {
11 => "████▌",
10 => " ",
9 => "████▌",
8 => " ",
7 => "████▌",
6 => " ",
5 => "████▌",
4 => "████▌",
3 => " ",
2 => "████▌",
1 => " ",
0 => "████▌",
_ => unreachable!(),
}
}

View file

@ -0,0 +1,34 @@
use crate::*;
use Color::*;
pub struct OctaveVertical {
on: [bool; 12],
colors: [Color; 3]
}
impl Default for OctaveVertical {
fn default () -> Self {
Self {
on: [false; 12],
colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)]
}
}
}
impl OctaveVertical {
fn color (&self, pitch: usize) -> Color {
let pitch = pitch % 12;
self.colors[if self.on[pitch] { 2 } else {
match pitch { 0 | 2 | 4 | 5 | 6 | 8 | 10 => 0, _ => 1 }
}]
}
}
impl Content<TuiOut> for OctaveVertical {
fn content (&self) -> impl Render<TuiOut> {
row!(
Tui::fg_bg(self.color(0), self.color(1), ""),
Tui::fg_bg(self.color(2), self.color(3), ""),
Tui::fg_bg(self.color(4), self.color(5), ""),
Tui::fg_bg(self.color(6), self.color(7), ""),
Tui::fg_bg(self.color(8), self.color(9), ""),
Tui::fg_bg(self.color(10), self.color(11), ""),
)
}
}

17
crates/plugin/Cargo.toml Normal file
View file

@ -0,0 +1,17 @@
[package]
name = "tek_plugin"
edition = "2021"
version = "0.2.0"
[dependencies]
tengri = { workspace = true }
tek_jack = { workspace = true }
tek_time = { workspace = true }
tek_midi = { workspace = true }
livi = { workspace = true, optional = true }
[features]
default = [ "lv2" ]
lv2 = [ "livi" ]

View file

View file

8
crates/plugin/src/lib.rs Normal file
View file

@ -0,0 +1,8 @@
mod plugin; pub use self::plugin::*;
mod lv2; pub use self::lv2::*;
pub(crate) use std::cmp::Ord;
pub(crate) use std::fmt::{Debug, Formatter};
pub(crate) use std::sync::{Arc, RwLock};
pub(crate) use std::thread::JoinHandle;
pub(crate) use ::tek_jack::{*, jack::*};
pub(crate) use ::tengri::{output::*, tui::{*, ratatui::prelude::*}};

40
crates/plugin/src/lv2.rs Normal file
View file

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

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

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

275
crates/plugin/src/plugin.rs Normal file
View file

@ -0,0 +1,275 @@
use crate::*;
/// A plugin device.
#[derive(Debug)]
pub struct Plugin {
/// JACK client handle (needs to not be dropped for standalone mode to work).
pub jack: Jack,
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: &Jack,
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: &Jack,
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_atom!("plugin/lv2" => |jack: &Jack, args| -> Plugin {
//let mut name = String::new();
//let mut path = String::new();
//atom!(atom in args {
//Atom::Map(map) => {
//if let Some(Atom::Str(n)) = map.get(&Atom::Key(":name")) {
//name = String::from(*n);
//}
//if let Some(Atom::Str(p)) = map.get(&Atom::Key(":path")) {
//path = String::from(*p);
//}
//},
//_ => panic!("unexpected in lv2 '{name}'"),
//});
//Plugin::new_lv2(jack, &name, &path)
//});

View file

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

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

View file

@ -0,0 +1,33 @@
name: Deploy
on:
release:
types: [published]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
# Installs the latest stable rust, and all components needed for the rest of the CI pipeline.
- name: Set up CI environment
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
# Sanity check: make sure the release builds
- name: Build
run: cargo build --verbose
# Sanity check: make sure all tests in the release pass
- name: Test
run: cargo test --verbose
# Deploy to crates.io
# Only works on github releases (tagged commits)
- name: Deploy to crates.io
env:
CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }}
run: cargo publish --token $CRATES_IO_TOKEN --manifest-path Cargo.toml

View file

@ -0,0 +1,46 @@
name: Docs
on:
push:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
persist-credentials: false
fetch-depth: 0
# Installs the latest stable rust, and all components needed for the rest of the CI pipeline.
- name: Set up CI environment
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
# Sanity check: make sure the release builds
- name: Build
run: cargo build --verbose
# Sanity check: make sure all tests in the release pass
- name: Test
run: cargo test --verbose
# Generate docs
# TODO: what does the last line here do?
- name: Generate docs
env:
GH_ENCRYPTED_TOKEN: ${{ secrets.GH_ENCRYPTED_TOKEN }}
run: |
cargo doc --all --no-deps
echo '<meta http-equiv=refresh content=0;url=vst/index.html>' > target/doc/index.html
# Push docs to github pages (branch `gh-pages`)
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: target/doc

View file

@ -0,0 +1,38 @@
name: Rust
on: [push, pull_request]
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
steps:
- uses: actions/checkout@v2
# Installs the latest stable rust, and all components needed for the rest of the CI pipeline.
- name: Set up CI environment
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
components: rustfmt, clippy
# Makes sure the code builds successfully.
- name: Build
run: cargo build --verbose
# Makes sure all of the tests pass.
- name: Test
run: cargo test --verbose
# Runs Clippy on the codebase, and makes sure there are no lint warnings.
# Disabled for now. Re-enable if you find it useful enough to deal with it constantly breaking.
# - name: Clippy
# run: cargo clippy --all-targets --all-features -- -D warnings -A clippy::unreadable_literal -A clippy::needless_range_loop -A clippy::float_cmp -A clippy::comparison-chain -A clippy::needless-doctest-main -A clippy::missing-safety-doc
# Makes sure the codebase is up to `cargo fmt` standards
- name: Format check
run: cargo fmt --all -- --check

21
crates/plugin/vst/.gitignore vendored Normal file
View file

@ -0,0 +1,21 @@
# Compiled files
*.o
*.so
*.rlib
*.dll
# Executables
*.exe
# Generated by Cargo
/target/
/examples/*/target/
Cargo.lock
# Vim
[._]*.s[a-w][a-z]
[._]s[a-w][a-z]
*.un~
Session.vim
.netrwhist
*~

View file

@ -0,0 +1,86 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 0.4.0
### Changed
- Added deprecation notice.
## 0.3.0
### Fixed
- `SysExEvent` no longer contains invalid data on 64-bit systems ([#170](https://github.com/RustAudio/vst-rs/pull/171)]
- Function pointers in `AEffect` marked as `extern` ([#141](https://github.com/RustAudio/vst-rs/pull/141))
- Key character fixes ([#152](https://github.com/RustAudio/vst-rs/pull/152))
- Doc and deploy actions fixes ([9eb1bef](https://github.com/RustAudio/vst-rs/commit/9eb1bef1826db1581b4162081de05c1090935afb))
- Various doc fixes ([#177](https://github.com/RustAudio/vst-rs/pull/177))
### Added
- `begin_edit` and `end_edit` now in `Host` trait ([#151](https://github.com/RustAudio/vst-rs/pull/151))
- Added a `prelude` for commonly used items when constructing a `Plugin` ([#161](https://github.com/RustAudio/vst-rs/pull/161))
- Various useful implementations for `AtomicFloat` ([#150](https://github.com/RustAudio/vst-rs/pull/150))
### Changed
- **Major breaking change:** New `Plugin` `Send` requirement ([#140](https://github.com/RustAudio/vst-rs/pull/140))
- No longer require `Plugin` to implement `Default` ([#154](https://github.com/RustAudio/vst-rs/pull/154))
- `impl_clicke` replaced with `num_enum` ([#168](https://github.com/RustAudio/vst-rs/pull/168))
- Reworked `SendEventBuffer` to make it useable in `Plugin::process_events` ([#160](https://github.com/RustAudio/vst-rs/pull/160))
- Updated dependencies and removed development dependency on `time` ([#179](https://github.com/RustAudio/vst-rs/pull/179))
## 0.2.1
### Fixed
- Introduced zero-valued `EventType` variant to enable zero-initialization of `Event`, fixing a panic on Rust 1.48 and newer ([#138](https://github.com/RustAudio/vst-rs/pull/138))
- `EditorGetRect` opcode returns `1` on success, ensuring that the provided dimensions are applied by the host ([#115](https://github.com/RustAudio/vst-rs/pull/115))
### Added
- Added `update_display()` method to `Host`, telling the host to update its display (after a parameter change) via the `UpdateDisplay` opcode ([#126](https://github.com/RustAudio/vst-rs/pull/126))
- Allow plug-in to return a custom value in `can_do()` via the `Supported::Custom` enum variant ([#130](https://github.com/RustAudio/vst-rs/pull/130))
- Added `PartialEq` and `Eq` for `Supported` ([#135](https://github.com/RustAudio/vst-rs/pull/135))
- Implemented `get_editor()` and `Editor` interface for `PluginInstance` to enable editor support on the host side ([#136](https://github.com/RustAudio/vst-rs/pull/136))
- Default value (`0.0`) for `AtomicFloat` ([#139](https://github.com/RustAudio/vst-rs/pull/139))
## 0.2.0
### Changed
- **Major breaking change:** Restructured `Plugin` API to make it thread safe ([#65](https://github.com/RustAudio/vst-rs/pull/65))
- Fixed a number of unsoundness issues in the `Outputs` API ([#67](https://github.com/RustAudio/vst-rs/pull/67), [#108](https://github.com/RustAudio/vst-rs/pull/108))
- Set parameters to be automatable by default ([#99](https://github.com/RustAudio/vst-rs/pull/99))
- Moved repository to the [RustAudio](https://github.com/RustAudio) organization and renamed it to `vst-rs` ([#90](https://github.com/RustAudio/vst-rs/pull/90), [#94](https://github.com/RustAudio/vst-rs/pull/94))
### Fixed
- Fixed a use-after-move bug in the event iterator ([#93](https://github.com/RustAudio/vst-rs/pull/93), [#111](https://github.com/RustAudio/vst-rs/pull/111))
### Added
- Handle `Opcode::GetEffectName` to resolve name display issues on some hosts ([#89](https://github.com/RustAudio/vst-rs/pull/89))
- More examples ([#65](https://github.com/RustAudio/vst-rs/pull/65), [#92](https://github.com/RustAudio/vst-rs/pull/92))
## 0.1.0
### Added
- Added initial changelog
- Initial project files
### Removed
- The `#[derive(Copy, Clone)]` attribute from `Outputs`.
### Changed
- The signature of the `Outputs::split_at_mut` now takes an `self` parameter instead of `&mut self`.
So calling `split_at_mut` will now move instead of "borrow".
- Now `&mut Outputs` (instead of `Outputs`) implements the `IntoIterator` trait.
- The return type of the `AudioBuffer::zip()` method (but it still implements the Iterator trait).

View file

@ -0,0 +1,75 @@
[package]
name = "vst"
version = "0.4.0"
edition = "2021"
authors = [
"Marko Mijalkovic <marko.mijalkovic97@gmail.com>",
"Boscop",
"Alex Zywicki <alexander.zywicki@gmail.com>",
"doomy <notdoomy@protonmail.com>",
"Ms2ger",
"Rob Saunders",
"David Lu",
"Aske Simon Christensen",
"kykc",
"Jordan Earls",
"xnor104",
"Nathaniel Theis",
"Colin Wallace",
"Henrik Nordvik",
"Charles Saracco",
"Frederik Halkjær" ]
description = "VST 2.4 API implementation in rust. Create plugins or hosts."
readme = "README.md"
repository = "https://github.com/rustaudio/vst-rs"
license = "MIT"
keywords = ["vst", "vst2", "plugin"]
autoexamples = false
[features]
default = []
disable_deprecation_warning = []
[dependencies]
log = "0.4"
num-traits = "0.2"
libc = "0.2"
bitflags = "1"
libloading = "0.7"
num_enum = "0.5.2"
[dev-dependencies]
rand = "0.8"
[[example]]
name = "dimension_expander"
crate-type = ["cdylib"]
[[example]]
name = "simple_host"
crate-type = ["bin"]
[[example]]
name = "sine_synth"
crate-type = ["cdylib"]
[[example]]
name = "fwd_midi"
crate-type = ["cdylib"]
[[example]]
name = "gain_effect"
crate-type = ["cdylib"]
[[example]]
name = "transfer_and_smooth"
crate-type = ["cdylib"]
[[example]]
name = "ladder_filter"
crate-type = ["cdylib"]

21
crates/plugin/vst/LICENSE Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Marko Mijalkovic
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

112
crates/plugin/vst/README.md Normal file
View file

@ -0,0 +1,112 @@
# vst-rs
[![crates.io][crates-img]][crates-url]
[![dependency status](https://deps.rs/repo/github/rustaudio/vst-rs/status.svg)](https://deps.rs/repo/github/rustaudio/vst-rs)
[![Discord Chat][discord-img]][discord-url]
[![Discourse topics][dc-img]][dc-url]
> **Notice**: `vst-rs` is deprecated.
>
> This crate is no longer actively developed or maintained. VST 2 has been [officially discontinued](http://web.archive.org/web/20210727141622/https://www.steinberg.net/en/newsandevents/news/newsdetail/article/vst-2-coming-to-an-end-4727.html) and it is [no longer possible](https://forum.juce.com/t/steinberg-closing-down-vst2-for-good/27722/25) to acquire a license to distribute VST 2 products. It is highly recommended that you make use of other libraries for developing audio plugins and plugin hosts in Rust.
>
> If you're looking for a high-level, multi-format framework for developing plugins in Rust, consider using [NIH-plug](https://github.com/robbert-vdh/nih-plug/) or [`baseplug`](https://github.com/wrl/baseplug/). If you're looking for bindings to specific plugin APIs, consider using [`vst3-sys`](https://github.com/RustAudio/vst3-sys/), [`clap-sys`](https://github.com/glowcoil/clap-sys), [`lv2(-sys)`](https://github.com/RustAudio/rust-lv2), or [`auv2-sys`](https://github.com/glowcoil/auv2-sys). If, despite the above warnings, you still have a need to use the VST 2 API from Rust, consider using [`vst2-sys`](https://github.com/RustAudio/vst2-sys) or generating bindings from the original VST 2 SDK using [`bindgen`](https://github.com/rust-lang/rust-bindgen).
`vst-rs` is a library for creating VST2 plugins in the Rust programming language.
This library is a work in progress, and as such it does not yet implement all
functionality. It can create basic VST plugins without an editor interface.
**Note:** If you are upgrading from a version prior to 0.2.0, you will need to update
your plugin code to be compatible with the new, thread-safe plugin API. See the
[`transfer_and_smooth`](examples/transfer_and_smooth.rs) example for a guide on how
to port your plugin.
## Library Documentation
Documentation for **released** versions can be found [here](https://docs.rs/vst/).
Development documentation (current `master` branch) can be found [here](https://rustaudio.github.io/vst-rs/vst/).
## Crate
This crate is available on [crates.io](https://crates.io/crates/vst). If you prefer the bleeding-edge, you can also
include the crate directly from the official [Github repository](https://github.com/rustaudio/vst-rs).
```toml
# get from crates.io.
vst = "0.3"
```
```toml
# get directly from Github. This might be unstable!
vst = { git = "https://github.com/rustaudio/vst-rs" }
```
## Usage
To create a plugin, simply create a type which implements the `Plugin` trait. Then call the `plugin_main` macro, which will export the necessary functions and handle dealing with the rest of the API.
## Example Plugin
A simple plugin that bears no functionality. The provided `Cargo.toml` has a
`crate-type` directive which builds a dynamic library, usable by any VST host.
`src/lib.rs`
```rust
#[macro_use]
extern crate vst;
use vst::prelude::*;
struct BasicPlugin;
impl Plugin for BasicPlugin {
fn new(_host: HostCallback) -> Self {
BasicPlugin
}
fn get_info(&self) -> Info {
Info {
name: "Basic Plugin".to_string(),
unique_id: 1357, // Used by hosts to differentiate between plugins.
..Default::default()
}
}
}
plugin_main!(BasicPlugin); // Important!
```
`Cargo.toml`
```toml
[package]
name = "basic_vst"
version = "0.0.1"
authors = ["Author <author@example.com>"]
[dependencies]
vst = { git = "https://github.com/rustaudio/vst-rs" }
[lib]
name = "basicvst"
crate-type = ["cdylib"]
```
[crates-img]: https://img.shields.io/crates/v/vst.svg
[crates-url]: https://crates.io/crates/vst
[discord-img]: https://img.shields.io/discord/590254806208217089.svg?label=Discord&logo=discord&color=blue
[discord-url]: https://discord.gg/QPdhk2u
[dc-img]: https://img.shields.io/discourse/https/rust-audio.discourse.group/topics.svg?logo=discourse&color=blue
[dc-url]: https://rust-audio.discourse.group
#### Packaging on OS X
On OS X VST plugins are packaged inside loadable bundles.
To package your VST as a loadable bundle you may use the `osx_vst_bundler.sh` script this library provides. 
Example: 
```
./osx_vst_bundler.sh Plugin target/release/plugin.dylib
Creates a Plugin.vst bundle
```
## Special Thanks
[Marko Mijalkovic](https://github.com/overdrivenpotato) for [initiating this project](https://github.com/overdrivenpotato/rust-vst2)

View file

@ -0,0 +1,222 @@
// author: Marko Mijalkovic <marko.mijalkovic97@gmail.com>
#[macro_use]
extern crate vst;
use std::collections::VecDeque;
use std::f64::consts::PI;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use vst::prelude::*;
/// Calculate the length in samples for a delay. Size ranges from 0.0 to 1.0.
fn delay(index: usize, mut size: f32) -> isize {
const SIZE_OFFSET: f32 = 0.06;
const SIZE_MULT: f32 = 1_000.0;
size += SIZE_OFFSET;
// Spread ratio between delays
const SPREAD: f32 = 0.3;
let base = size * SIZE_MULT;
let mult = (index as f32 * SPREAD) + 1.0;
let offset = if index > 2 { base * SPREAD / 2.0 } else { 0.0 };
(base * mult + offset) as isize
}
/// A left channel and right channel sample.
type SamplePair = (f32, f32);
/// The Dimension Expander.
struct DimensionExpander {
buffers: Vec<VecDeque<SamplePair>>,
params: Arc<DimensionExpanderParameters>,
old_size: f32,
}
struct DimensionExpanderParameters {
dry_wet: AtomicFloat,
size: AtomicFloat,
}
impl DimensionExpander {
fn new(size: f32, dry_wet: f32) -> DimensionExpander {
const NUM_DELAYS: usize = 4;
let mut buffers = Vec::new();
// Generate delay buffers
for i in 0..NUM_DELAYS {
let samples = delay(i, size);
let mut buffer = VecDeque::with_capacity(samples as usize);
// Fill in the delay buffers with empty samples
for _ in 0..samples {
buffer.push_back((0.0, 0.0));
}
buffers.push(buffer);
}
DimensionExpander {
buffers,
params: Arc::new(DimensionExpanderParameters {
dry_wet: AtomicFloat::new(dry_wet),
size: AtomicFloat::new(size),
}),
old_size: size,
}
}
/// Update the delay buffers with a new size value.
fn resize(&mut self, n: f32) {
let old_size = self.old_size;
for (i, buffer) in self.buffers.iter_mut().enumerate() {
// Calculate the size difference between delays
let old_delay = delay(i, old_size);
let new_delay = delay(i, n);
let diff = new_delay - old_delay;
// Add empty samples if the delay was increased, remove if decreased
if diff > 0 {
for _ in 0..diff {
buffer.push_back((0.0, 0.0));
}
} else if diff < 0 {
for _ in 0..-diff {
let _ = buffer.pop_front();
}
}
}
self.old_size = n;
}
}
impl Plugin for DimensionExpander {
fn new(_host: HostCallback) -> Self {
DimensionExpander::new(0.12, 0.66)
}
fn get_info(&self) -> Info {
Info {
name: "Dimension Expander".to_string(),
vendor: "overdrivenpotato".to_string(),
unique_id: 243723071,
version: 1,
inputs: 2,
outputs: 2,
parameters: 2,
category: Category::Effect,
..Default::default()
}
}
fn process(&mut self, buffer: &mut AudioBuffer<f32>) {
let (inputs, outputs) = buffer.split();
// Assume 2 channels
if inputs.len() < 2 || outputs.len() < 2 {
return;
}
// Resize if size changed
let size = self.params.size.get();
if size != self.old_size {
self.resize(size);
}
// Iterate over inputs as (&f32, &f32)
let (l, r) = inputs.split_at(1);
let stereo_in = l[0].iter().zip(r[0].iter());
// Iterate over outputs as (&mut f32, &mut f32)
let (mut l, mut r) = outputs.split_at_mut(1);
let stereo_out = l[0].iter_mut().zip(r[0].iter_mut());
// Zip and process
for ((left_in, right_in), (left_out, right_out)) in stereo_in.zip(stereo_out) {
// Push the new samples into the delay buffers.
for buffer in &mut self.buffers {
buffer.push_back((*left_in, *right_in));
}
let mut left_processed = 0.0;
let mut right_processed = 0.0;
// Recalculate time per sample
let time_s = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs_f64();
// Use buffer index to offset volume LFO
for (n, buffer) in self.buffers.iter_mut().enumerate() {
if let Some((left_old, right_old)) = buffer.pop_front() {
const LFO_FREQ: f64 = 0.5;
const WET_MULT: f32 = 0.66;
let offset = 0.25 * (n % 4) as f64;
// Sine wave volume LFO
let lfo = ((time_s * LFO_FREQ + offset) * PI * 2.0).sin() as f32;
let wet = self.params.dry_wet.get() * WET_MULT;
let mono = (left_old + right_old) / 2.0;
// Flip right channel and keep left mono so that the result is
// entirely stereo
left_processed += mono * wet * lfo;
right_processed += -mono * wet * lfo;
}
}
// By only adding to the input, the output value always remains the same in mono
*left_out = *left_in + left_processed;
*right_out = *right_in + right_processed;
}
}
fn get_parameter_object(&mut self) -> Arc<dyn PluginParameters> {
Arc::clone(&self.params) as Arc<dyn PluginParameters>
}
}
impl PluginParameters for DimensionExpanderParameters {
fn get_parameter(&self, index: i32) -> f32 {
match index {
0 => self.size.get(),
1 => self.dry_wet.get(),
_ => 0.0,
}
}
fn get_parameter_text(&self, index: i32) -> String {
match index {
0 => format!("{}", (self.size.get() * 1000.0) as isize),
1 => format!("{:.1}%", self.dry_wet.get() * 100.0),
_ => "".to_string(),
}
}
fn get_parameter_name(&self, index: i32) -> String {
match index {
0 => "Size",
1 => "Dry/Wet",
_ => "",
}
.to_string()
}
fn set_parameter(&self, index: i32, val: f32) {
match index {
0 => self.size.set(val),
1 => self.dry_wet.set(val),
_ => (),
}
}
}
plugin_main!(DimensionExpander);

View file

@ -0,0 +1,71 @@
#[macro_use]
extern crate vst;
use vst::api;
use vst::prelude::*;
plugin_main!(MyPlugin); // Important!
#[derive(Default)]
struct MyPlugin {
host: HostCallback,
recv_buffer: SendEventBuffer,
send_buffer: SendEventBuffer,
}
impl MyPlugin {
fn send_midi(&mut self) {
self.send_buffer
.send_events(self.recv_buffer.events().events(), &mut self.host);
self.recv_buffer.clear();
}
}
impl Plugin for MyPlugin {
fn new(host: HostCallback) -> Self {
MyPlugin {
host,
..Default::default()
}
}
fn get_info(&self) -> Info {
Info {
name: "fwd_midi".to_string(),
unique_id: 7357001, // Used by hosts to differentiate between plugins.
..Default::default()
}
}
fn process_events(&mut self, events: &api::Events) {
self.recv_buffer.store_events(events.events());
}
fn process(&mut self, buffer: &mut AudioBuffer<f32>) {
for (input, output) in buffer.zip() {
for (in_sample, out_sample) in input.iter().zip(output) {
*out_sample = *in_sample;
}
}
self.send_midi();
}
fn process_f64(&mut self, buffer: &mut AudioBuffer<f64>) {
for (input, output) in buffer.zip() {
for (in_sample, out_sample) in input.iter().zip(output) {
*out_sample = *in_sample;
}
}
self.send_midi();
}
fn can_do(&self, can_do: CanDo) -> vst::api::Supported {
use vst::api::Supported::*;
use vst::plugin::CanDo::*;
match can_do {
SendEvents | SendMidiEvent | ReceiveEvents | ReceiveMidiEvent => Yes,
_ => No,
}
}
}

View file

@ -0,0 +1,129 @@
// author: doomy <notdoomy@protonmail.com>
#[macro_use]
extern crate vst;
use std::sync::Arc;
use vst::prelude::*;
/// Simple Gain Effect.
/// Note that this does not use a proper scale for sound and shouldn't be used in
/// a production amplification effect! This is purely for demonstration purposes,
/// as well as to keep things simple as this is meant to be a starting point for
/// any effect.
struct GainEffect {
// Store a handle to the plugin's parameter object.
params: Arc<GainEffectParameters>,
}
/// The plugin's parameter object contains the values of parameters that can be
/// adjusted from the host. If we were creating an effect that didn't allow the
/// user to modify it at runtime or have any controls, we could omit this part.
///
/// The parameters object is shared between the processing and GUI threads.
/// For this reason, all mutable state in the object has to be represented
/// through thread-safe interior mutability. The easiest way to achieve this
/// is to store the parameters in atomic containers.
struct GainEffectParameters {
// The plugin's state consists of a single parameter: amplitude.
amplitude: AtomicFloat,
}
impl Default for GainEffectParameters {
fn default() -> GainEffectParameters {
GainEffectParameters {
amplitude: AtomicFloat::new(0.5),
}
}
}
// All plugins using `vst` also need to implement the `Plugin` trait. Here, we
// define functions that give necessary info to our host.
impl Plugin for GainEffect {
fn new(_host: HostCallback) -> Self {
// Note that controls will always return a value from 0 - 1.
// Setting a default to 0.5 means it's halfway up.
GainEffect {
params: Arc::new(GainEffectParameters::default()),
}
}
fn get_info(&self) -> Info {
Info {
name: "Gain Effect in Rust".to_string(),
vendor: "Rust DSP".to_string(),
unique_id: 243723072,
version: 1,
inputs: 2,
outputs: 2,
// This `parameters` bit is important; without it, none of our
// parameters will be shown!
parameters: 1,
category: Category::Effect,
..Default::default()
}
}
// Here is where the bulk of our audio processing code goes.
fn process(&mut self, buffer: &mut AudioBuffer<f32>) {
// Read the amplitude from the parameter object
let amplitude = self.params.amplitude.get();
// First, we destructure our audio buffer into an arbitrary number of
// input and output buffers. Usually, we'll be dealing with stereo (2 of each)
// but that might change.
for (input_buffer, output_buffer) in buffer.zip() {
// Next, we'll loop through each individual sample so we can apply the amplitude
// value to it.
for (input_sample, output_sample) in input_buffer.iter().zip(output_buffer) {
*output_sample = *input_sample * amplitude;
}
}
}
// Return the parameter object. This method can be omitted if the
// plugin has no parameters.
fn get_parameter_object(&mut self) -> Arc<dyn PluginParameters> {
Arc::clone(&self.params) as Arc<dyn PluginParameters>
}
}
impl PluginParameters for GainEffectParameters {
// the `get_parameter` function reads the value of a parameter.
fn get_parameter(&self, index: i32) -> f32 {
match index {
0 => self.amplitude.get(),
_ => 0.0,
}
}
// the `set_parameter` function sets the value of a parameter.
fn set_parameter(&self, index: i32, val: f32) {
#[allow(clippy::single_match)]
match index {
0 => self.amplitude.set(val),
_ => (),
}
}
// This is what will display underneath our control. We can
// format it into a string that makes the most since.
fn get_parameter_text(&self, index: i32) -> String {
match index {
0 => format!("{:.2}", (self.amplitude.get() - 0.5) * 2f32),
_ => "".to_string(),
}
}
// This shows the control's name.
fn get_parameter_name(&self, index: i32) -> String {
match index {
0 => "Amplitude",
_ => "",
}
.to_string()
}
}
// This part is important! Without it, our plugin won't work.
plugin_main!(GainEffect);

View file

@ -0,0 +1,248 @@
//! This zero-delay feedback filter is based on a 4-stage transistor ladder filter.
//! It follows the following equations:
//! x = input - tanh(self.res * self.vout[3])
//! vout[0] = self.params.g.get() * (tanh(x) - tanh(self.vout[0])) + self.s[0]
//! vout[1] = self.params.g.get() * (tanh(self.vout[0]) - tanh(self.vout[1])) + self.s[1]
//! vout[0] = self.params.g.get() * (tanh(self.vout[1]) - tanh(self.vout[2])) + self.s[2]
//! vout[0] = self.params.g.get() * (tanh(self.vout[2]) - tanh(self.vout[3])) + self.s[3]
//! since we can't easily solve a nonlinear equation,
//! Mystran's fixed-pivot method is used to approximate the tanh() parts.
//! Quality can be improved a lot by oversampling a bit.
//! Feedback is clipped independently of the input, so it doesn't disappear at high gains.
#[macro_use]
extern crate vst;
use std::f32::consts::PI;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use vst::prelude::*;
// this is a 4-pole filter with resonance, which is why there's 4 states and vouts
#[derive(Clone)]
struct LadderFilter {
// Store a handle to the plugin's parameter object.
params: Arc<LadderParameters>,
// the output of the different filter stages
vout: [f32; 4],
// s is the "state" parameter. In an IIR it would be the last value from the filter
// In this we find it by trapezoidal integration to avoid the unit delay
s: [f32; 4],
}
struct LadderParameters {
// the "cutoff" parameter. Determines how heavy filtering is
cutoff: AtomicFloat,
g: AtomicFloat,
// needed to calculate cutoff.
sample_rate: AtomicFloat,
// makes a peak at cutoff
res: AtomicFloat,
// used to choose where we want our output to be
poles: AtomicUsize,
// pole_value is just to be able to use get_parameter on poles
pole_value: AtomicFloat,
// a drive parameter. Just used to increase the volume, which results in heavier distortion
drive: AtomicFloat,
}
impl Default for LadderParameters {
fn default() -> LadderParameters {
LadderParameters {
cutoff: AtomicFloat::new(1000.),
res: AtomicFloat::new(2.),
poles: AtomicUsize::new(3),
pole_value: AtomicFloat::new(1.),
drive: AtomicFloat::new(0.),
sample_rate: AtomicFloat::new(44100.),
g: AtomicFloat::new(0.07135868),
}
}
}
// member methods for the struct
impl LadderFilter {
// the state needs to be updated after each process. Found by trapezoidal integration
fn update_state(&mut self) {
self.s[0] = 2. * self.vout[0] - self.s[0];
self.s[1] = 2. * self.vout[1] - self.s[1];
self.s[2] = 2. * self.vout[2] - self.s[2];
self.s[3] = 2. * self.vout[3] - self.s[3];
}
// performs a complete filter process (mystran's method)
fn tick_pivotal(&mut self, input: f32) {
if self.params.drive.get() > 0. {
self.run_ladder_nonlinear(input * (self.params.drive.get() + 0.7));
} else {
//
self.run_ladder_linear(input);
}
self.update_state();
}
// nonlinear ladder filter function with distortion.
fn run_ladder_nonlinear(&mut self, input: f32) {
let mut a = [1f32; 5];
let base = [input, self.s[0], self.s[1], self.s[2], self.s[3]];
// a[n] is the fixed-pivot approximation for tanh()
for n in 0..base.len() {
if base[n] != 0. {
a[n] = base[n].tanh() / base[n];
} else {
a[n] = 1.;
}
}
// denominators of solutions of individual stages. Simplifies the math a bit
let g0 = 1. / (1. + self.params.g.get() * a[1]);
let g1 = 1. / (1. + self.params.g.get() * a[2]);
let g2 = 1. / (1. + self.params.g.get() * a[3]);
let g3 = 1. / (1. + self.params.g.get() * a[4]);
// these are just factored out of the feedback solution. Makes the math way easier to read
let f3 = self.params.g.get() * a[3] * g3;
let f2 = self.params.g.get() * a[2] * g2 * f3;
let f1 = self.params.g.get() * a[1] * g1 * f2;
let f0 = self.params.g.get() * g0 * f1;
// outputs a 24db filter
self.vout[3] =
(f0 * input * a[0] + f1 * g0 * self.s[0] + f2 * g1 * self.s[1] + f3 * g2 * self.s[2] + g3 * self.s[3])
/ (f0 * self.params.res.get() * a[3] + 1.);
// since we know the feedback, we can solve the remaining outputs:
self.vout[0] = g0
* (self.params.g.get() * a[1] * (input * a[0] - self.params.res.get() * a[3] * self.vout[3]) + self.s[0]);
self.vout[1] = g1 * (self.params.g.get() * a[2] * self.vout[0] + self.s[1]);
self.vout[2] = g2 * (self.params.g.get() * a[3] * self.vout[1] + self.s[2]);
}
// linear version without distortion
pub fn run_ladder_linear(&mut self, input: f32) {
// denominators of solutions of individual stages. Simplifies the math a bit
let g0 = 1. / (1. + self.params.g.get());
let g1 = self.params.g.get() * g0 * g0;
let g2 = self.params.g.get() * g1 * g0;
let g3 = self.params.g.get() * g2 * g0;
// outputs a 24db filter
self.vout[3] =
(g3 * self.params.g.get() * input + g0 * self.s[3] + g1 * self.s[2] + g2 * self.s[1] + g3 * self.s[0])
/ (g3 * self.params.g.get() * self.params.res.get() + 1.);
// since we know the feedback, we can solve the remaining outputs:
self.vout[0] = g0 * (self.params.g.get() * (input - self.params.res.get() * self.vout[3]) + self.s[0]);
self.vout[1] = g0 * (self.params.g.get() * self.vout[0] + self.s[1]);
self.vout[2] = g0 * (self.params.g.get() * self.vout[1] + self.s[2]);
}
}
impl LadderParameters {
pub fn set_cutoff(&self, value: f32) {
// cutoff formula gives us a natural feeling cutoff knob that spends more time in the low frequencies
self.cutoff.set(20000. * (1.8f32.powf(10. * value - 10.)));
// bilinear transformation for g gives us a very accurate cutoff
self.g.set((PI * self.cutoff.get() / (self.sample_rate.get())).tan());
}
// returns the value used to set cutoff. for get_parameter function
pub fn get_cutoff(&self) -> f32 {
1. + 0.17012975 * (0.00005 * self.cutoff.get()).ln()
}
pub fn set_poles(&self, value: f32) {
self.pole_value.set(value);
self.poles.store(((value * 3.).round()) as usize, Ordering::Relaxed);
}
}
impl PluginParameters for LadderParameters {
// get_parameter has to return the value used in set_parameter
fn get_parameter(&self, index: i32) -> f32 {
match index {
0 => self.get_cutoff(),
1 => self.res.get() / 4.,
2 => self.pole_value.get(),
3 => self.drive.get() / 5.,
_ => 0.0,
}
}
fn set_parameter(&self, index: i32, value: f32) {
match index {
0 => self.set_cutoff(value),
1 => self.res.set(value * 4.),
2 => self.set_poles(value),
3 => self.drive.set(value * 5.),
_ => (),
}
}
fn get_parameter_name(&self, index: i32) -> String {
match index {
0 => "cutoff".to_string(),
1 => "resonance".to_string(),
2 => "filter order".to_string(),
3 => "drive".to_string(),
_ => "".to_string(),
}
}
fn get_parameter_label(&self, index: i32) -> String {
match index {
0 => "Hz".to_string(),
1 => "%".to_string(),
2 => "poles".to_string(),
3 => "%".to_string(),
_ => "".to_string(),
}
}
// This is what will display underneath our control. We can
// format it into a string that makes the most sense.
fn get_parameter_text(&self, index: i32) -> String {
match index {
0 => format!("{:.0}", self.cutoff.get()),
1 => format!("{:.3}", self.res.get()),
2 => format!("{}", self.poles.load(Ordering::Relaxed) + 1),
3 => format!("{:.3}", self.drive.get()),
_ => format!(""),
}
}
}
impl Plugin for LadderFilter {
fn new(_host: HostCallback) -> Self {
LadderFilter {
vout: [0f32; 4],
s: [0f32; 4],
params: Arc::new(LadderParameters::default()),
}
}
fn set_sample_rate(&mut self, rate: f32) {
self.params.sample_rate.set(rate);
}
fn get_info(&self) -> Info {
Info {
name: "LadderFilter".to_string(),
unique_id: 9263,
inputs: 1,
outputs: 1,
category: Category::Effect,
parameters: 4,
..Default::default()
}
}
fn process(&mut self, buffer: &mut AudioBuffer<f32>) {
for (input_buffer, output_buffer) in buffer.zip() {
for (input_sample, output_sample) in input_buffer.iter().zip(output_buffer) {
self.tick_pivotal(*input_sample);
// the poles parameter chooses which filter stage we take our output from.
*output_sample = self.vout[self.params.poles.load(Ordering::Relaxed)];
}
}
}
fn get_parameter_object(&mut self) -> Arc<dyn PluginParameters> {
Arc::clone(&self.params) as Arc<dyn PluginParameters>
}
}
plugin_main!(LadderFilter);

View file

@ -0,0 +1,63 @@
extern crate vst;
use std::env;
use std::path::Path;
use std::process;
use std::sync::{Arc, Mutex};
use vst::host::{Host, PluginLoader};
use vst::plugin::Plugin;
#[allow(dead_code)]
struct SampleHost;
impl Host for SampleHost {
fn automate(&self, index: i32, value: f32) {
println!("Parameter {} had its value changed to {}", index, value);
}
}
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
println!("usage: simple_host path/to/vst");
process::exit(1);
}
let path = Path::new(&args[1]);
// Create the host
let host = Arc::new(Mutex::new(SampleHost));
println!("Loading {}...", path.to_str().unwrap());
// Load the plugin
let mut loader =
PluginLoader::load(path, Arc::clone(&host)).unwrap_or_else(|e| panic!("Failed to load plugin: {}", e));
// Create an instance of the plugin
let mut instance = loader.instance().unwrap();
// Get the plugin information
let info = instance.get_info();
println!(
"Loaded '{}':\n\t\
Vendor: {}\n\t\
Presets: {}\n\t\
Parameters: {}\n\t\
VST ID: {}\n\t\
Version: {}\n\t\
Initial Delay: {} samples",
info.name, info.vendor, info.presets, info.parameters, info.unique_id, info.version, info.initial_delay
);
// Initialize the instance
instance.init();
println!("Initialized instance!");
println!("Closing instance...");
// Close the instance. This is not necessary as the instance is shut down when
// it is dropped as it goes out of scope.
// drop(instance);
}

View file

@ -0,0 +1,160 @@
// author: Rob Saunders <hello@robsaunders.io>
#[macro_use]
extern crate vst;
use vst::prelude::*;
use std::f64::consts::PI;
/// Convert the midi note's pitch into the equivalent frequency.
///
/// This function assumes A4 is 440hz.
fn midi_pitch_to_freq(pitch: u8) -> f64 {
const A4_PITCH: i8 = 69;
const A4_FREQ: f64 = 440.0;
// Midi notes can be 0-127
((f64::from(pitch as i8 - A4_PITCH)) / 12.).exp2() * A4_FREQ
}
struct SineSynth {
sample_rate: f64,
time: f64,
note_duration: f64,
note: Option<u8>,
}
impl SineSynth {
fn time_per_sample(&self) -> f64 {
1.0 / self.sample_rate
}
/// Process an incoming midi event.
///
/// The midi data is split up like so:
///
/// `data[0]`: Contains the status and the channel. Source: [source]
/// `data[1]`: Contains the supplemental data for the message - so, if this was a NoteOn then
/// this would contain the note.
/// `data[2]`: Further supplemental data. Would be velocity in the case of a NoteOn message.
///
/// [source]: http://www.midimountain.com/midi/midi_status.htm
fn process_midi_event(&mut self, data: [u8; 3]) {
match data[0] {
128 => self.note_off(data[1]),
144 => self.note_on(data[1]),
_ => (),
}
}
fn note_on(&mut self, note: u8) {
self.note_duration = 0.0;
self.note = Some(note)
}
fn note_off(&mut self, note: u8) {
if self.note == Some(note) {
self.note = None
}
}
}
pub const TAU: f64 = PI * 2.0;
impl Plugin for SineSynth {
fn new(_host: HostCallback) -> Self {
SineSynth {
sample_rate: 44100.0,
note_duration: 0.0,
time: 0.0,
note: None,
}
}
fn get_info(&self) -> Info {
Info {
name: "SineSynth".to_string(),
vendor: "DeathDisco".to_string(),
unique_id: 6667,
category: Category::Synth,
inputs: 2,
outputs: 2,
parameters: 0,
initial_delay: 0,
..Info::default()
}
}
#[allow(unused_variables)]
#[allow(clippy::single_match)]
fn process_events(&mut self, events: &Events) {
for event in events.events() {
match event {
Event::Midi(ev) => self.process_midi_event(ev.data),
// More events can be handled here.
_ => (),
}
}
}
fn set_sample_rate(&mut self, rate: f32) {
self.sample_rate = f64::from(rate);
}
fn process(&mut self, buffer: &mut AudioBuffer<f32>) {
let samples = buffer.samples();
let (_, mut outputs) = buffer.split();
let output_count = outputs.len();
let per_sample = self.time_per_sample();
let mut output_sample;
for sample_idx in 0..samples {
let time = self.time;
let note_duration = self.note_duration;
if let Some(current_note) = self.note {
let signal = (time * midi_pitch_to_freq(current_note) * TAU).sin();
// Apply a quick envelope to the attack of the signal to avoid popping.
let attack = 0.5;
let alpha = if note_duration < attack {
note_duration / attack
} else {
1.0
};
output_sample = (signal * alpha) as f32;
self.time += per_sample;
self.note_duration += per_sample;
} else {
output_sample = 0.0;
}
for buf_idx in 0..output_count {
let buff = outputs.get_mut(buf_idx);
buff[sample_idx] = output_sample;
}
}
}
fn can_do(&self, can_do: CanDo) -> Supported {
match can_do {
CanDo::ReceiveMidiEvent => Supported::Yes,
_ => Supported::Maybe,
}
}
}
plugin_main!(SineSynth);
#[cfg(test)]
mod tests {
use midi_pitch_to_freq;
#[test]
fn test_midi_pitch_to_freq() {
for i in 0..127 {
// expect no panics
midi_pitch_to_freq(i);
}
}
}

View file

@ -0,0 +1,136 @@
// This example illustrates how an existing plugin can be ported to the new,
// thread-safe API with the help of the ParameterTransfer struct.
// It shows how the parameter iteration feature of ParameterTransfer can be
// used to react explicitly to parameter changes in an efficient way (here,
// to implement smoothing of parameters).
#[macro_use]
extern crate vst;
use std::f32;
use std::sync::Arc;
use vst::prelude::*;
const PARAMETER_COUNT: usize = 100;
const BASE_FREQUENCY: f32 = 5.0;
const FILTER_FACTOR: f32 = 0.01; // Set this to 1.0 to disable smoothing.
const TWO_PI: f32 = 2.0 * f32::consts::PI;
// 1. Define a struct to hold parameters. Put a ParameterTransfer inside it,
// plus optionally a HostCallback.
struct MyPluginParameters {
#[allow(dead_code)]
host: HostCallback,
transfer: ParameterTransfer,
}
// 2. Put an Arc reference to your parameter struct in your main Plugin struct.
struct MyPlugin {
params: Arc<MyPluginParameters>,
states: Vec<Smoothed>,
sample_rate: f32,
phase: f32,
}
// 3. Implement PluginParameters for your parameter struct.
// The set_parameter and get_parameter just access the ParameterTransfer.
// The other methods can be implemented on top of this as well.
impl PluginParameters for MyPluginParameters {
fn set_parameter(&self, index: i32, value: f32) {
self.transfer.set_parameter(index as usize, value);
}
fn get_parameter(&self, index: i32) -> f32 {
self.transfer.get_parameter(index as usize)
}
}
impl Plugin for MyPlugin {
fn new(host: HostCallback) -> Self {
MyPlugin {
// 4. Initialize your main Plugin struct with a parameter struct
// wrapped in an Arc, and put the HostCallback inside it.
params: Arc::new(MyPluginParameters {
host,
transfer: ParameterTransfer::new(PARAMETER_COUNT),
}),
states: vec![Smoothed::default(); PARAMETER_COUNT],
sample_rate: 44100.0,
phase: 0.0,
}
}
fn get_info(&self) -> Info {
Info {
parameters: PARAMETER_COUNT as i32,
inputs: 0,
outputs: 2,
category: Category::Synth,
f64_precision: false,
name: "transfer_and_smooth".to_string(),
vendor: "Loonies".to_string(),
unique_id: 0x500007,
version: 100,
..Info::default()
}
}
// 5. Return a reference to the parameter struct from get_parameter_object.
fn get_parameter_object(&mut self) -> Arc<dyn PluginParameters> {
Arc::clone(&self.params) as Arc<dyn PluginParameters>
}
fn set_sample_rate(&mut self, sample_rate: f32) {
self.sample_rate = sample_rate;
}
fn process(&mut self, buffer: &mut AudioBuffer<f32>) {
// 6. In the process method, iterate over changed parameters and do
// for each what you would previously do in set_parameter. Since this
// runs in the processing thread, it has mutable access to the Plugin.
for (p, value) in self.params.transfer.iterate(true) {
// Example: Update filter state of changed parameter.
self.states[p].set(value);
}
// Example: Dummy synth adding together a bunch of sines.
let samples = buffer.samples();
let mut outputs = buffer.split().1;
for i in 0..samples {
let mut sum = 0.0;
for p in 0..PARAMETER_COUNT {
let amp = self.states[p].get();
if amp != 0.0 {
sum += (self.phase * p as f32 * TWO_PI).sin() * amp;
}
}
outputs[0][i] = sum;
outputs[1][i] = sum;
self.phase = (self.phase + BASE_FREQUENCY / self.sample_rate).fract();
}
}
}
// Example: Parameter smoothing as an example of non-trivial parameter handling
// that has to happen when a parameter changes.
#[derive(Clone, Default)]
struct Smoothed {
state: f32,
target: f32,
}
impl Smoothed {
fn set(&mut self, value: f32) {
self.target = value;
}
fn get(&mut self) -> f32 {
self.state += (self.target - self.state) * FILTER_FACTOR;
self.state
}
}
plugin_main!(MyPlugin);

View file

@ -0,0 +1,61 @@
#!/bin/bash
# Make sure we have the arguments we need
if [[ -z $1 || -z $2 ]]; then
echo "Generates a macOS bundle from a compiled dylib file"
echo "Example:"
echo -e "\t$0 Plugin target/release/plugin.dylib"
echo -e "\tCreates a Plugin.vst bundle"
else
# Make the bundle folder
mkdir -p "$1.vst/Contents/MacOS"
# Create the PkgInfo
echo "BNDL????" > "$1.vst/Contents/PkgInfo"
#build the Info.Plist
echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
<plist version=\"1.0\">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>$1</string>
<key>CFBundleGetInfoString</key>
<string>vst</string>
<key>CFBundleIconFile</key>
<string></string>
<key>CFBundleIdentifier</key>
<string>com.rust-vst.$1</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$1</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>$((RANDOM % 9999))</string>
<key>CSResourcesFileMapped</key>
<string></string>
</dict>
</plist>" > "$1.vst/Contents/Info.plist"
# move the provided library to the correct location
cp "$2" "$1.vst/Contents/MacOS/$1"
echo "Created bundle $1.vst"
fi

View file

@ -0,0 +1 @@
max_width = 120

View file

@ -0,0 +1,927 @@
//! Structures and types for interfacing with the VST 2.4 API.
use std::os::raw::c_void;
use std::sync::Arc;
use self::consts::*;
use crate::{
editor::Editor,
plugin::{Info, Plugin, PluginParameters},
};
/// Constant values
#[allow(missing_docs)] // For obvious constants
pub mod consts {
pub const MAX_PRESET_NAME_LEN: usize = 24;
pub const MAX_PARAM_STR_LEN: usize = 32;
pub const MAX_LABEL: usize = 64;
pub const MAX_SHORT_LABEL: usize = 8;
pub const MAX_PRODUCT_STR_LEN: usize = 64;
pub const MAX_VENDOR_STR_LEN: usize = 64;
/// VST plugins are identified by a magic number. This corresponds to 0x56737450.
pub const VST_MAGIC: i32 = ('V' as i32) << 24 | ('s' as i32) << 16 | ('t' as i32) << 8 | ('P' as i32);
}
/// `VSTPluginMain` function signature.
pub type PluginMain = fn(callback: HostCallbackProc) -> *mut AEffect;
/// Host callback function passed to plugin.
/// Can be used to query host information from plugin side.
pub type HostCallbackProc =
extern "C" fn(effect: *mut AEffect, opcode: i32, index: i32, value: isize, ptr: *mut c_void, opt: f32) -> isize;
/// Dispatcher function used to process opcodes. Called by host.
pub type DispatcherProc =
extern "C" fn(effect: *mut AEffect, opcode: i32, index: i32, value: isize, ptr: *mut c_void, opt: f32) -> isize;
/// Process function used to process 32 bit floating point samples. Called by host.
pub type ProcessProc =
extern "C" fn(effect: *mut AEffect, inputs: *const *const f32, outputs: *mut *mut f32, sample_frames: i32);
/// Process function used to process 64 bit floating point samples. Called by host.
pub type ProcessProcF64 =
extern "C" fn(effect: *mut AEffect, inputs: *const *const f64, outputs: *mut *mut f64, sample_frames: i32);
/// Callback function used to set parameter values. Called by host.
pub type SetParameterProc = extern "C" fn(effect: *mut AEffect, index: i32, parameter: f32);
/// Callback function used to get parameter values. Called by host.
pub type GetParameterProc = extern "C" fn(effect: *mut AEffect, index: i32) -> f32;
/// Used with the VST API to pass around plugin information.
#[allow(non_snake_case)]
#[repr(C)]
pub struct AEffect {
/// Magic number. Must be `['V', 'S', 'T', 'P']`.
pub magic: i32,
/// Host to plug-in dispatcher.
pub dispatcher: DispatcherProc,
/// Accumulating process mode is deprecated in VST 2.4! Use `processReplacing` instead!
pub _process: ProcessProc,
/// Set value of automatable parameter.
pub setParameter: SetParameterProc,
/// Get value of automatable parameter.
pub getParameter: GetParameterProc,
/// Number of programs (Presets).
pub numPrograms: i32,
/// Number of parameters. All programs are assumed to have this many parameters.
pub numParams: i32,
/// Number of audio inputs.
pub numInputs: i32,
/// Number of audio outputs.
pub numOutputs: i32,
/// Bitmask made of values from `api::PluginFlags`.
///
/// ```no_run
/// use vst::api::PluginFlags;
/// let flags = PluginFlags::CAN_REPLACING | PluginFlags::CAN_DOUBLE_REPLACING;
/// // ...
/// ```
pub flags: i32,
/// Reserved for host, must be 0.
pub reserved1: isize,
/// Reserved for host, must be 0.
pub reserved2: isize,
/// For algorithms which need input in the first place (Group delay or latency in samples).
///
/// This value should be initially in a resume state.
pub initialDelay: i32,
/// Deprecated unused member.
pub _realQualities: i32,
/// Deprecated unused member.
pub _offQualities: i32,
/// Deprecated unused member.
pub _ioRatio: f32,
/// Void pointer usable by api to store object data.
pub object: *mut c_void,
/// User defined pointer.
pub user: *mut c_void,
/// Registered unique identifier (register it at Steinberg 3rd party support Web).
/// This is used to identify a plug-in during save+load of preset and project.
pub uniqueId: i32,
/// Plug-in version (e.g. 1100 for v1.1.0.0).
pub version: i32,
/// Process audio samples in replacing mode.
pub processReplacing: ProcessProc,
/// Process double-precision audio samples in replacing mode.
pub processReplacingF64: ProcessProcF64,
/// Reserved for future use (please zero).
pub future: [u8; 56],
}
impl AEffect {
/// Return handle to Plugin object. Only works for plugins created using this library.
/// Caller is responsible for not calling this function concurrently.
// Suppresses warning about returning a reference to a box
#[allow(clippy::borrowed_box)]
pub unsafe fn get_plugin(&self) -> &mut Box<dyn Plugin> {
//FIXME: find a way to do this without resorting to transmuting via a box
&mut *(self.object as *mut Box<dyn Plugin>)
}
/// Return handle to Info object. Only works for plugins created using this library.
pub unsafe fn get_info(&self) -> &Info {
&(*(self.user as *mut super::PluginCache)).info
}
/// Return handle to PluginParameters object. Only works for plugins created using this library.
pub unsafe fn get_params(&self) -> &Arc<dyn PluginParameters> {
&(*(self.user as *mut super::PluginCache)).params
}
/// Return handle to Editor object. Only works for plugins created using this library.
/// Caller is responsible for not calling this function concurrently.
pub unsafe fn get_editor(&self) -> &mut Option<Box<dyn Editor>> {
&mut (*(self.user as *mut super::PluginCache)).editor
}
/// Drop the Plugin object. Only works for plugins created using this library.
pub unsafe fn drop_plugin(&mut self) {
drop(Box::from_raw(self.object as *mut Box<dyn Plugin>));
drop(Box::from_raw(self.user as *mut super::PluginCache));
}
}
/// Information about a channel. Only some hosts use this information.
#[repr(C)]
pub struct ChannelProperties {
/// Channel name.
pub name: [u8; MAX_LABEL as usize],
/// Flags found in `ChannelFlags`.
pub flags: i32,
/// Type of speaker arrangement this channel is a part of.
pub arrangement_type: SpeakerArrangementType,
/// Name of channel (recommended: 6 characters + delimiter).
pub short_name: [u8; MAX_SHORT_LABEL as usize],
/// Reserved for future use.
pub future: [u8; 48],
}
/// Tells the host how the channels are intended to be used in the plugin. Only useful for some
/// hosts.
#[repr(i32)]
#[derive(Clone, Copy)]
pub enum SpeakerArrangementType {
/// User defined arrangement.
Custom = -2,
/// Empty arrangement.
Empty = -1,
/// Mono.
Mono = 0,
/// L R
Stereo,
/// Ls Rs
StereoSurround,
/// Lc Rc
StereoCenter,
/// Sl Sr
StereoSide,
/// C Lfe
StereoCLfe,
/// L R C
Cinema30,
/// L R S
Music30,
/// L R C Lfe
Cinema31,
/// L R Lfe S
Music31,
/// L R C S (LCRS)
Cinema40,
/// L R Ls Rs (Quadro)
Music40,
/// L R C Lfe S (LCRS + Lfe)
Cinema41,
/// L R Lfe Ls Rs (Quadro + Lfe)
Music41,
/// L R C Ls Rs
Surround50,
/// L R C Lfe Ls Rs
Surround51,
/// L R C Ls Rs Cs
Cinema60,
/// L R Ls Rs Sl Sr
Music60,
/// L R C Lfe Ls Rs Cs
Cinema61,
/// L R Lfe Ls Rs Sl Sr
Music61,
/// L R C Ls Rs Lc Rc
Cinema70,
/// L R C Ls Rs Sl Sr
Music70,
/// L R C Lfe Ls Rs Lc Rc
Cinema71,
/// L R C Lfe Ls Rs Sl Sr
Music71,
/// L R C Ls Rs Lc Rc Cs
Cinema80,
/// L R C Ls Rs Cs Sl Sr
Music80,
/// L R C Lfe Ls Rs Lc Rc Cs
Cinema81,
/// L R C Lfe Ls Rs Cs Sl Sr
Music81,
/// L R C Lfe Ls Rs Tfl Tfc Tfr Trl Trr Lfe2
Surround102,
}
/// Used to specify whether functionality is supported.
#[allow(missing_docs)]
#[derive(PartialEq, Eq)]
pub enum Supported {
Yes,
Maybe,
No,
Custom(isize),
}
impl Supported {
/// Create a `Supported` value from an integer if possible.
pub fn from(val: isize) -> Option<Supported> {
use self::Supported::*;
match val {
1 => Some(Yes),
0 => Some(Maybe),
-1 => Some(No),
_ => None,
}
}
}
impl Into<isize> for Supported {
/// Convert to integer ordinal for interop with VST api.
fn into(self) -> isize {
use self::Supported::*;
match self {
Yes => 1,
Maybe => 0,
No => -1,
Custom(i) => i,
}
}
}
/// Denotes in which thread the host is in.
#[repr(i32)]
pub enum ProcessLevel {
/// Unsupported by host.
Unknown = 0,
/// GUI thread.
User,
/// Audio process thread.
Realtime,
/// Sequence thread (MIDI, etc).
Prefetch,
/// Offline processing thread (therefore GUI/user thread).
Offline,
}
/// Language that the host is using.
#[repr(i32)]
#[allow(missing_docs)]
pub enum HostLanguage {
English = 1,
German,
French,
Italian,
Spanish,
Japanese,
}
/// The file operation to perform.
#[repr(i32)]
pub enum FileSelectCommand {
/// Load a file.
Load = 0,
/// Save a file.
Save,
/// Load multiple files simultaneously.
LoadMultipleFiles,
/// Choose a directory.
SelectDirectory,
}
// TODO: investigate removing this.
/// Format to select files.
pub enum FileSelectType {
/// Regular file selector.
Regular,
}
/// File type descriptor.
#[repr(C)]
pub struct FileType {
/// Display name of file type.
pub name: [u8; 128],
/// OS X file type.
pub osx_type: [u8; 8],
/// Windows file type.
pub win_type: [u8; 8],
/// Unix file type.
pub nix_type: [u8; 8],
/// MIME type.
pub mime_type_1: [u8; 128],
/// Additional MIME type.
pub mime_type_2: [u8; 128],
}
/// File selector descriptor used in `host::OpCode::OpenFileSelector`.
#[repr(C)]
pub struct FileSelect {
/// The type of file selection to perform.
pub command: FileSelectCommand,
/// The file selector to open.
pub select_type: FileSelectType,
/// Unknown. 0 = no creator.
pub mac_creator: i32,
/// Number of file types.
pub num_types: i32,
/// List of file types to show.
pub file_types: *mut FileType,
/// File selector's title.
pub title: [u8; 1024],
/// Initial path.
pub initial_path: *mut u8,
/// Used when operation returns a single path.
pub return_path: *mut u8,
/// Size of the path buffer in bytes.
pub size_return_path: i32,
/// Used when operation returns multiple paths.
pub return_multiple_paths: *mut *mut u8,
/// Number of paths returned.
pub num_paths: i32,
/// Reserved by host.
pub reserved: isize,
/// Reserved for future use.
pub future: [u8; 116],
}
/// A struct which contains events.
#[repr(C)]
pub struct Events {
/// Number of events.
pub num_events: i32,
/// Reserved for future use. Should be 0.
pub _reserved: isize,
/// Variable-length array of pointers to `api::Event` objects.
///
/// The VST standard specifies a variable length array of initial size 2. If there are more
/// than 2 elements a larger array must be stored in this structure.
pub events: [*mut Event; 2],
}
impl Events {
#[inline]
pub(crate) fn events_raw(&self) -> &[*const Event] {
use std::slice;
unsafe {
slice::from_raw_parts(
&self.events[0] as *const *mut _ as *const *const _,
self.num_events as usize,
)
}
}
#[inline]
pub(crate) fn events_raw_mut(&mut self) -> &mut [*const SysExEvent] {
use std::slice;
unsafe {
slice::from_raw_parts_mut(
&mut self.events[0] as *mut *mut _ as *mut *const _,
self.num_events as usize,
)
}
}
/// Use this in your impl of process_events() to process the incoming midi events.
///
/// # Example
/// ```no_run
/// # use vst::plugin::{Info, Plugin, HostCallback};
/// # use vst::buffer::{AudioBuffer, SendEventBuffer};
/// # use vst::host::Host;
/// # use vst::api;
/// # use vst::event::{Event, MidiEvent};
/// # struct ExamplePlugin { host: HostCallback, send_buf: SendEventBuffer }
/// # impl Plugin for ExamplePlugin {
/// # fn new(host: HostCallback) -> Self { Self { host, send_buf: Default::default() } }
/// #
/// # fn get_info(&self) -> Info { Default::default() }
/// #
/// fn process_events(&mut self, events: &api::Events) {
/// for e in events.events() {
/// match e {
/// Event::Midi(MidiEvent { data, .. }) => {
/// // ...
/// }
/// _ => ()
/// }
/// }
/// }
/// # }
/// ```
#[inline]
#[allow(clippy::needless_lifetimes)]
pub fn events<'a>(&'a self) -> impl Iterator<Item = crate::event::Event<'a>> {
self.events_raw()
.iter()
.map(|ptr| unsafe { crate::event::Event::from_raw_event(*ptr) })
}
}
/// The type of event that has occurred. See `api::Event.event_type`.
#[repr(i32)]
#[derive(Copy, Clone, Debug)]
pub enum EventType {
/// Value used for uninitialized placeholder events.
_Placeholder = 0,
/// Midi event. See `api::MidiEvent`.
Midi = 1,
/// Deprecated.
_Audio,
/// Deprecated.
_Video,
/// Deprecated.
_Parameter,
/// Deprecated.
_Trigger,
/// System exclusive event. See `api::SysExEvent`.
SysEx,
}
/// A VST event intended to be casted to a corresponding type.
///
/// The event types are not all guaranteed to be the same size,
/// so casting between them can be done
/// via `mem::transmute()` while leveraging pointers, e.g.
///
/// ```
/// # use vst::api::{Event, EventType, MidiEvent, SysExEvent};
/// # let mut event: *mut Event = &mut unsafe { std::mem::zeroed() };
/// // let event: *const Event = ...;
/// let midi_event: &MidiEvent = unsafe { std::mem::transmute(event) };
/// ```
#[repr(C)]
#[derive(Copy, Clone)]
pub struct Event {
/// The type of event. This lets you know which event this object should be casted to.
///
/// # Example
///
/// ```
/// # use vst::api::{Event, EventType, MidiEvent, SysExEvent};
/// #
/// # // Valid for test
/// # let mut event: *mut Event = &mut unsafe { std::mem::zeroed() };
/// #
/// // let mut event: *mut Event = ...
/// match unsafe { (*event).event_type } {
/// EventType::Midi => {
/// let midi_event: &MidiEvent = unsafe {
/// std::mem::transmute(event)
/// };
///
/// // ...
/// }
/// EventType::SysEx => {
/// let sys_event: &SysExEvent = unsafe {
/// std::mem::transmute(event)
/// };
///
/// // ...
/// }
/// // ...
/// # _ => {}
/// }
/// ```
pub event_type: EventType,
/// Size of this structure; `mem::sizeof::<Event>()`.
pub byte_size: i32,
/// Number of samples into the current processing block that this event occurs on.
///
/// E.g. if the block size is 512 and this value is 123, the event will occur on sample
/// `samples[123]`.
pub delta_frames: i32,
/// Generic flags, none defined in VST api yet.
pub _flags: i32,
/// The `Event` type is cast appropriately, so this acts as reserved space.
///
/// The actual size of the data may vary
///as this type is not guaranteed to be the same size as the other event types.
pub _reserved: [u8; 16],
}
/// A midi event.
#[repr(C)]
pub struct MidiEvent {
/// Should be `EventType::Midi`.
pub event_type: EventType,
/// Size of this structure; `mem::sizeof::<MidiEvent>()`.
pub byte_size: i32,
/// Number of samples into the current processing block that this event occurs on.
///
/// E.g. if the block size is 512 and this value is 123, the event will occur on sample
/// `samples[123]`.
pub delta_frames: i32,
/// See `MidiEventFlags`.
pub flags: i32,
/// Length in sample frames of entire note if available, otherwise 0.
pub note_length: i32,
/// Offset in samples into note from start if available, otherwise 0.
pub note_offset: i32,
/// 1 to 3 midi bytes. TODO: Doc
pub midi_data: [u8; 3],
/// Reserved midi byte (0).
pub _midi_reserved: u8,
/// Detuning between -63 and +64 cents,
/// for scales other than 'well-tempered'. e.g. 'microtuning'
pub detune: i8,
/// Note off velocity between 0 and 127.
pub note_off_velocity: u8,
/// Reserved for future use. Should be 0.
pub _reserved1: u8,
/// Reserved for future use. Should be 0.
pub _reserved2: u8,
}
/// A midi system exclusive event.
///
/// This event only contains raw byte data, and is up to the plugin to interpret it correctly.
/// `plugin::CanDo` has a `ReceiveSysExEvent` variant which lets the host query the plugin as to
/// whether this event is supported.
#[repr(C)]
#[derive(Clone)]
pub struct SysExEvent {
/// Should be `EventType::SysEx`.
pub event_type: EventType,
/// Size of this structure; `mem::sizeof::<SysExEvent>()`.
pub byte_size: i32,
/// Number of samples into the current processing block that this event occurs on.
///
/// E.g. if the block size is 512 and this value is 123, the event will occur on sample
/// `samples[123]`.
pub delta_frames: i32,
/// Generic flags, none defined in VST api yet.
pub _flags: i32,
/// Size of payload in bytes.
pub data_size: i32,
/// Reserved for future use. Should be 0.
pub _reserved1: isize,
/// Pointer to payload.
pub system_data: *mut u8,
/// Reserved for future use. Should be 0.
pub _reserved2: isize,
}
unsafe impl Send for SysExEvent {}
#[repr(C)]
#[derive(Clone, Default, Copy)]
/// Describes the time at the start of the block currently being processed
pub struct TimeInfo {
/// current Position in audio samples (always valid)
pub sample_pos: f64,
/// current Sample Rate in Hertz (always valid)
pub sample_rate: f64,
/// System Time in nanoseconds (10^-9 second)
pub nanoseconds: f64,
/// Musical Position, in Quarter Note (1.0 equals 1 Quarter Note)
pub ppq_pos: f64,
/// current Tempo in BPM (Beats Per Minute)
pub tempo: f64,
/// last Bar Start Position, in Quarter Note
pub bar_start_pos: f64,
/// Cycle Start (left locator), in Quarter Note
pub cycle_start_pos: f64,
/// Cycle End (right locator), in Quarter Note
pub cycle_end_pos: f64,
/// Time Signature Numerator (e.g. 3 for 3/4)
pub time_sig_numerator: i32,
/// Time Signature Denominator (e.g. 4 for 3/4)
pub time_sig_denominator: i32,
/// SMPTE offset in SMPTE subframes (bits; 1/80 of a frame).
/// The current SMPTE position can be calculated using `sample_pos`, `sample_rate`, and `smpte_frame_rate`.
pub smpte_offset: i32,
/// See `SmpteFrameRate`
pub smpte_frame_rate: SmpteFrameRate,
/// MIDI Clock Resolution (24 Per Quarter Note), can be negative (nearest clock)
pub samples_to_next_clock: i32,
/// See `TimeInfoFlags`
pub flags: i32,
}
#[repr(i32)]
#[derive(Copy, Clone, Debug)]
/// SMPTE Frame Rates.
pub enum SmpteFrameRate {
/// 24 fps
Smpte24fps = 0,
/// 25 fps
Smpte25fps = 1,
/// 29.97 fps
Smpte2997fps = 2,
/// 30 fps
Smpte30fps = 3,
/// 29.97 drop
Smpte2997dfps = 4,
/// 30 drop
Smpte30dfps = 5,
/// Film 16mm
SmpteFilm16mm = 6,
/// Film 35mm
SmpteFilm35mm = 7,
/// HDTV: 23.976 fps
Smpte239fps = 10,
/// HDTV: 24.976 fps
Smpte249fps = 11,
/// HDTV: 59.94 fps
Smpte599fps = 12,
/// HDTV: 60 fps
Smpte60fps = 13,
}
impl Default for SmpteFrameRate {
fn default() -> Self {
SmpteFrameRate::Smpte24fps
}
}
bitflags! {
/// Flags for VST channels.
pub struct ChannelFlags: i32 {
/// Indicates channel is active. Ignored by host.
const ACTIVE = 1;
/// Indicates channel is first of stereo pair.
const STEREO = 1 << 1;
/// Use channel's specified speaker_arrangement instead of stereo flag.
const SPEAKER = 1 << 2;
}
}
bitflags! {
/// Flags for VST plugins.
pub struct PluginFlags: i32 {
/// Plugin has an editor.
const HAS_EDITOR = 1;
/// Plugin can process 32 bit audio. (Mandatory in VST 2.4).
const CAN_REPLACING = 1 << 4;
/// Plugin preset data is handled in formatless chunks.
const PROGRAM_CHUNKS = 1 << 5;
/// Plugin is a synth.
const IS_SYNTH = 1 << 8;
/// Plugin does not produce sound when all input is silence.
const NO_SOUND_IN_STOP = 1 << 9;
/// Supports 64 bit audio processing.
const CAN_DOUBLE_REPLACING = 1 << 12;
}
}
bitflags! {
/// Cross platform modifier key flags.
pub struct ModifierKey: u8 {
/// Shift key.
const SHIFT = 1;
/// Alt key.
const ALT = 1 << 1;
/// Control on mac.
const COMMAND = 1 << 2;
/// Command on mac, ctrl on other.
const CONTROL = 1 << 3; // Ctrl on PC, Apple on Mac
}
}
bitflags! {
/// MIDI event flags.
pub struct MidiEventFlags: i32 {
/// This event is played live (not in playback from a sequencer track). This allows the
/// plugin to handle these flagged events with higher priority, especially when the
/// plugin has a big latency as per `plugin::Info::initial_delay`.
const REALTIME_EVENT = 1;
}
}
bitflags! {
/// Used in the `flags` field of `TimeInfo`, and for querying the host for specific values
pub struct TimeInfoFlags : i32 {
/// Indicates that play, cycle or record state has changed.
const TRANSPORT_CHANGED = 1;
/// Set if Host sequencer is currently playing.
const TRANSPORT_PLAYING = 1 << 1;
/// Set if Host sequencer is in cycle mode.
const TRANSPORT_CYCLE_ACTIVE = 1 << 2;
/// Set if Host sequencer is in record mode.
const TRANSPORT_RECORDING = 1 << 3;
/// Set if automation write mode active (record parameter changes).
const AUTOMATION_WRITING = 1 << 6;
/// Set if automation read mode active (play parameter changes).
const AUTOMATION_READING = 1 << 7;
/// Set if TimeInfo::nanoseconds is valid.
const NANOSECONDS_VALID = 1 << 8;
/// Set if TimeInfo::ppq_pos is valid.
const PPQ_POS_VALID = 1 << 9;
/// Set if TimeInfo::tempo is valid.
const TEMPO_VALID = 1 << 10;
/// Set if TimeInfo::bar_start_pos is valid.
const BARS_VALID = 1 << 11;
/// Set if both TimeInfo::cycle_start_pos and VstTimeInfo::cycle_end_pos are valid.
const CYCLE_POS_VALID = 1 << 12;
/// Set if both TimeInfo::time_sig_numerator and TimeInfo::time_sig_denominator are valid.
const TIME_SIG_VALID = 1 << 13;
/// Set if both TimeInfo::smpte_offset and VstTimeInfo::smpte_frame_rate are valid.
const SMPTE_VALID = 1 << 14;
/// Set if TimeInfo::samples_to_next_clock is valid.
const VST_CLOCK_VALID = 1 << 15;
}
}
#[cfg(test)]
mod tests {
use super::super::event;
use super::*;
use std::mem;
// This container is used because we have to store somewhere the events
// that are pointed to by raw pointers in the events object. We heap allocate
// the event so the pointer in events stays consistent when the container is moved.
pub struct EventContainer {
stored_event: Box<Event>,
pub events: Events,
}
// A convenience method which creates an api::Events object representing a midi event.
// This represents code that might be found in a VST host using this API.
fn encode_midi_message_as_events(message: [u8; 3]) -> EventContainer {
let midi_event: MidiEvent = MidiEvent {
event_type: EventType::Midi,
byte_size: mem::size_of::<MidiEvent>() as i32,
delta_frames: 0,
flags: 0,
note_length: 0,
note_offset: 0,
midi_data: [message[0], message[1], message[2]],
_midi_reserved: 0,
detune: 0,
note_off_velocity: 0,
_reserved1: 0,
_reserved2: 0,
};
let mut event: Event = unsafe { std::mem::transmute(midi_event) };
event.event_type = EventType::Midi;
let events = Events {
num_events: 1,
_reserved: 0,
events: [&mut event, &mut event], // Second one is a dummy
};
let mut ec = EventContainer {
stored_event: Box::new(event),
events,
};
ec.events.events[0] = &mut *(ec.stored_event); // Overwrite ptrs, since we moved the event into ec
ec
}
#[test]
fn encode_and_decode_gives_back_original_message() {
let message: [u8; 3] = [35, 16, 22];
let encoded = encode_midi_message_as_events(message);
assert_eq!(encoded.events.num_events, 1);
assert_eq!(encoded.events.events.len(), 2);
let e_vec: Vec<event::Event> = encoded.events.events().collect();
assert_eq!(e_vec.len(), 1);
match e_vec[0] {
event::Event::Midi(event::MidiEvent { data, .. }) => {
assert_eq!(data, message);
}
_ => {
panic!("Not a midi event!");
}
};
}
// This is a regression test for a bug fixed in PR #93
// We check here that calling events() on an api::Events object
// does not mutate the underlying events.
#[test]
fn message_survives_calling_events() {
let message: [u8; 3] = [35, 16, 22];
let encoded = encode_midi_message_as_events(message);
for e in encoded.events.events() {
match e {
event::Event::Midi(event::MidiEvent { data, .. }) => {
assert_eq!(data, message);
}
_ => {
panic!("Not a midi event!");
}
}
}
for e in encoded.events.events() {
match e {
event::Event::Midi(event::MidiEvent { data, .. }) => {
assert_eq!(data, message);
}
_ => {
panic!("Not a midi event!"); // FAILS here!
}
}
}
}
}

View file

@ -0,0 +1,606 @@
//! Buffers to safely work with audio samples.
use num_traits::Float;
use std::slice;
/// `AudioBuffer` contains references to the audio buffers for all input and output channels.
///
/// To create an `AudioBuffer` in a host, use a [`HostBuffer`](../host/struct.HostBuffer.html).
pub struct AudioBuffer<'a, T: 'a + Float> {
inputs: &'a [*const T],
outputs: &'a mut [*mut T],
samples: usize,
}
impl<'a, T: 'a + Float> AudioBuffer<'a, T> {
/// Create an `AudioBuffer` from raw pointers.
/// Only really useful for interacting with the VST API.
#[inline]
pub unsafe fn from_raw(
input_count: usize,
output_count: usize,
inputs_raw: *const *const T,
outputs_raw: *mut *mut T,
samples: usize,
) -> Self {
Self {
inputs: slice::from_raw_parts(inputs_raw, input_count),
outputs: slice::from_raw_parts_mut(outputs_raw, output_count),
samples,
}
}
/// The number of input channels that this buffer was created for
#[inline]
pub fn input_count(&self) -> usize {
self.inputs.len()
}
/// The number of output channels that this buffer was created for
#[inline]
pub fn output_count(&self) -> usize {
self.outputs.len()
}
/// The number of samples in this buffer (same for all channels)
#[inline]
pub fn samples(&self) -> usize {
self.samples
}
/// The raw inputs to pass to processReplacing
#[inline]
pub(crate) fn raw_inputs(&self) -> &[*const T] {
self.inputs
}
/// The raw outputs to pass to processReplacing
#[inline]
pub(crate) fn raw_outputs(&mut self) -> &mut [*mut T] {
&mut self.outputs
}
/// Split this buffer into separate inputs and outputs.
#[inline]
pub fn split<'b>(&'b mut self) -> (Inputs<'b, T>, Outputs<'b, T>)
where
'a: 'b,
{
(
Inputs {
bufs: self.inputs,
samples: self.samples,
},
Outputs {
bufs: self.outputs,
samples: self.samples,
},
)
}
/// Create an iterator over pairs of input buffers and output buffers.
#[inline]
pub fn zip<'b>(&'b mut self) -> AudioBufferIterator<'a, 'b, T> {
AudioBufferIterator {
audio_buffer: self,
index: 0,
}
}
}
/// Iterator over pairs of buffers of input channels and output channels.
pub struct AudioBufferIterator<'a, 'b, T>
where
T: 'a + Float,
'a: 'b,
{
audio_buffer: &'b mut AudioBuffer<'a, T>,
index: usize,
}
impl<'a, 'b, T> Iterator for AudioBufferIterator<'a, 'b, T>
where
T: 'b + Float,
{
type Item = (&'b [T], &'b mut [T]);
fn next(&mut self) -> Option<Self::Item> {
if self.index < self.audio_buffer.inputs.len() && self.index < self.audio_buffer.outputs.len() {
let input =
unsafe { slice::from_raw_parts(self.audio_buffer.inputs[self.index], self.audio_buffer.samples) };
let output =
unsafe { slice::from_raw_parts_mut(self.audio_buffer.outputs[self.index], self.audio_buffer.samples) };
let val = (input, output);
self.index += 1;
Some(val)
} else {
None
}
}
}
use std::ops::{Index, IndexMut};
/// Wrapper type to access the buffers for the input channels of an `AudioBuffer` in a safe way.
/// Behaves like a slice.
#[derive(Copy, Clone)]
pub struct Inputs<'a, T: 'a> {
bufs: &'a [*const T],
samples: usize,
}
impl<'a, T> Inputs<'a, T> {
/// Number of channels
pub fn len(&self) -> usize {
self.bufs.len()
}
/// Returns true if the buffer is empty
pub fn is_empty(&self) -> bool {
self.len() == 0
}
/// Access channel at the given index
pub fn get(&self, i: usize) -> &'a [T] {
unsafe { slice::from_raw_parts(self.bufs[i], self.samples) }
}
/// Split borrowing at the given index, like for slices
pub fn split_at(&self, i: usize) -> (Inputs<'a, T>, Inputs<'a, T>) {
let (l, r) = self.bufs.split_at(i);
(
Inputs {
bufs: l,
samples: self.samples,
},
Inputs {
bufs: r,
samples: self.samples,
},
)
}
}
impl<'a, T> Index<usize> for Inputs<'a, T> {
type Output = [T];
fn index(&self, i: usize) -> &Self::Output {
self.get(i)
}
}
/// Iterator over buffers for input channels of an `AudioBuffer`.
pub struct InputIterator<'a, T: 'a> {
data: Inputs<'a, T>,
i: usize,
}
impl<'a, T> Iterator for InputIterator<'a, T> {
type Item = &'a [T];
fn next(&mut self) -> Option<Self::Item> {
if self.i < self.data.len() {
let val = self.data.get(self.i);
self.i += 1;
Some(val)
} else {
None
}
}
}
impl<'a, T: Sized> IntoIterator for Inputs<'a, T> {
type Item = &'a [T];
type IntoIter = InputIterator<'a, T>;
fn into_iter(self) -> Self::IntoIter {
InputIterator { data: self, i: 0 }
}
}
/// Wrapper type to access the buffers for the output channels of an `AudioBuffer` in a safe way.
/// Behaves like a slice.
pub struct Outputs<'a, T: 'a> {
bufs: &'a [*mut T],
samples: usize,
}
impl<'a, T> Outputs<'a, T> {
/// Number of channels
pub fn len(&self) -> usize {
self.bufs.len()
}
/// Returns true if the buffer is empty
pub fn is_empty(&self) -> bool {
self.len() == 0
}
/// Access channel at the given index
pub fn get(&self, i: usize) -> &'a [T] {
unsafe { slice::from_raw_parts(self.bufs[i], self.samples) }
}
/// Mutably access channel at the given index
pub fn get_mut(&mut self, i: usize) -> &'a mut [T] {
unsafe { slice::from_raw_parts_mut(self.bufs[i], self.samples) }
}
/// Split borrowing at the given index, like for slices
pub fn split_at_mut(self, i: usize) -> (Outputs<'a, T>, Outputs<'a, T>) {
let (l, r) = self.bufs.split_at(i);
(
Outputs {
bufs: l,
samples: self.samples,
},
Outputs {
bufs: r,
samples: self.samples,
},
)
}
}
impl<'a, T> Index<usize> for Outputs<'a, T> {
type Output = [T];
fn index(&self, i: usize) -> &Self::Output {
self.get(i)
}
}
impl<'a, T> IndexMut<usize> for Outputs<'a, T> {
fn index_mut(&mut self, i: usize) -> &mut Self::Output {
self.get_mut(i)
}
}
/// Iterator over buffers for output channels of an `AudioBuffer`.
pub struct OutputIterator<'a, 'b, T>
where
T: 'a,
'a: 'b,
{
data: &'b mut Outputs<'a, T>,
i: usize,
}
impl<'a, 'b, T> Iterator for OutputIterator<'a, 'b, T>
where
T: 'b,
{
type Item = &'b mut [T];
fn next(&mut self) -> Option<Self::Item> {
if self.i < self.data.len() {
let val = self.data.get_mut(self.i);
self.i += 1;
Some(val)
} else {
None
}
}
}
impl<'a, 'b, T: Sized> IntoIterator for &'b mut Outputs<'a, T> {
type Item = &'b mut [T];
type IntoIter = OutputIterator<'a, 'b, T>;
fn into_iter(self) -> Self::IntoIter {
OutputIterator { data: self, i: 0 }
}
}
use crate::event::{Event, MidiEvent, SysExEvent};
/// This is used as a placeholder to pre-allocate space for a fixed number of
/// midi events in the re-useable `SendEventBuffer`, because `SysExEvent` is
/// larger than `MidiEvent`, so either one can be stored in a `SysExEvent`.
pub type PlaceholderEvent = api::SysExEvent;
/// This trait is used by `SendEventBuffer::send_events` to accept iterators over midi events
pub trait WriteIntoPlaceholder {
/// writes an event into the given placeholder memory location
fn write_into(&self, out: &mut PlaceholderEvent);
}
impl<'a, T: WriteIntoPlaceholder> WriteIntoPlaceholder for &'a T {
fn write_into(&self, out: &mut PlaceholderEvent) {
(*self).write_into(out);
}
}
impl WriteIntoPlaceholder for MidiEvent {
fn write_into(&self, out: &mut PlaceholderEvent) {
let out = unsafe { &mut *(out as *mut _ as *mut _) };
*out = api::MidiEvent {
event_type: api::EventType::Midi,
byte_size: mem::size_of::<api::MidiEvent>() as i32,
delta_frames: self.delta_frames,
flags: if self.live {
api::MidiEventFlags::REALTIME_EVENT.bits()
} else {
0
},
note_length: self.note_length.unwrap_or(0),
note_offset: self.note_offset.unwrap_or(0),
midi_data: self.data,
_midi_reserved: 0,
detune: self.detune,
note_off_velocity: self.note_off_velocity,
_reserved1: 0,
_reserved2: 0,
};
}
}
impl<'a> WriteIntoPlaceholder for SysExEvent<'a> {
fn write_into(&self, out: &mut PlaceholderEvent) {
*out = PlaceholderEvent {
event_type: api::EventType::SysEx,
byte_size: mem::size_of::<PlaceholderEvent>() as i32,
delta_frames: self.delta_frames,
_flags: 0,
data_size: self.payload.len() as i32,
_reserved1: 0,
system_data: self.payload.as_ptr() as *const u8 as *mut u8,
_reserved2: 0,
};
}
}
impl<'a> WriteIntoPlaceholder for Event<'a> {
fn write_into(&self, out: &mut PlaceholderEvent) {
match *self {
Event::Midi(ref ev) => {
ev.write_into(out);
}
Event::SysEx(ref ev) => {
ev.write_into(out);
}
Event::Deprecated(e) => {
let out = unsafe { &mut *(out as *mut _ as *mut _) };
*out = e;
}
};
}
}
use crate::{api, host::Host};
use std::mem;
/// This buffer is used for sending midi events through the VST interface.
/// The purpose of this is to convert outgoing midi events from `event::Event` to `api::Events`.
/// It only allocates memory in new() and reuses the memory between calls.
pub struct SendEventBuffer {
buf: Vec<u8>,
api_events: Vec<PlaceholderEvent>, // using SysExEvent to store both because it's larger than MidiEvent
}
impl Default for SendEventBuffer {
fn default() -> Self {
SendEventBuffer::new(1024)
}
}
impl SendEventBuffer {
/// Creates a buffer for sending up to the given number of midi events per frame
#[inline(always)]
pub fn new(capacity: usize) -> Self {
let header_size = mem::size_of::<api::Events>() - (mem::size_of::<*mut api::Event>() * 2);
let body_size = mem::size_of::<*mut api::Event>() * capacity;
let mut buf = vec![0u8; header_size + body_size];
let api_events = vec![unsafe { mem::zeroed::<PlaceholderEvent>() }; capacity];
{
let ptrs = {
let e = Self::buf_as_api_events(&mut buf);
e.num_events = capacity as i32;
e.events_raw_mut()
};
for (ptr, event) in ptrs.iter_mut().zip(&api_events) {
let (ptr, event): (&mut *const PlaceholderEvent, &PlaceholderEvent) = (ptr, event);
*ptr = event;
}
}
Self { buf, api_events }
}
/// Sends events to the host. See the `fwd_midi` example.
///
/// # Example
/// ```no_run
/// # use vst::plugin::{Info, Plugin, HostCallback};
/// # use vst::buffer::{AudioBuffer, SendEventBuffer};
/// # use vst::host::Host;
/// # use vst::event::*;
/// # struct ExamplePlugin { host: HostCallback, send_buffer: SendEventBuffer }
/// # impl Plugin for ExamplePlugin {
/// # fn new(host: HostCallback) -> Self { Self { host, send_buffer: Default::default() } }
/// #
/// # fn get_info(&self) -> Info { Default::default() }
/// #
/// fn process(&mut self, buffer: &mut AudioBuffer<f32>){
/// let events: Vec<MidiEvent> = vec![
/// // ...
/// ];
/// self.send_buffer.send_events(&events, &mut self.host);
/// }
/// # }
/// ```
#[inline(always)]
pub fn send_events<T: IntoIterator<Item = U>, U: WriteIntoPlaceholder>(&mut self, events: T, host: &mut dyn Host) {
self.store_events(events);
host.process_events(self.events());
}
/// Stores events in the buffer, replacing the buffer's current content.
/// Use this in [`process_events`](crate::Plugin::process_events) to store received input events, then read them in [`process`](crate::Plugin::process) using [`events`](SendEventBuffer::events).
#[inline(always)]
pub fn store_events<T: IntoIterator<Item = U>, U: WriteIntoPlaceholder>(&mut self, events: T) {
#[allow(clippy::suspicious_map)]
let count = events
.into_iter()
.zip(self.api_events.iter_mut())
.map(|(ev, out)| ev.write_into(out))
.count();
self.set_num_events(count);
}
/// Returns a reference to the stored events
#[inline(always)]
pub fn events(&self) -> &api::Events {
#[allow(clippy::cast_ptr_alignment)]
unsafe {
&*(self.buf.as_ptr() as *const api::Events)
}
}
/// Clears the buffer
#[inline(always)]
pub fn clear(&mut self) {
self.set_num_events(0);
}
#[inline(always)]
fn buf_as_api_events(buf: &mut [u8]) -> &mut api::Events {
#[allow(clippy::cast_ptr_alignment)]
unsafe {
&mut *(buf.as_mut_ptr() as *mut api::Events)
}
}
#[inline(always)]
fn set_num_events(&mut self, events_len: usize) {
use std::cmp::min;
let e = Self::buf_as_api_events(&mut self.buf);
e.num_events = min(self.api_events.len(), events_len) as i32;
}
}
#[cfg(test)]
mod tests {
use crate::buffer::AudioBuffer;
/// Size of buffers used in tests.
const SIZE: usize = 1024;
/// Test that creating and zipping buffers works.
///
/// This test creates a channel for 2 inputs and 2 outputs.
/// The input channels are simply values
/// from 0 to `SIZE-1` (e.g. [0, 1, 2, 3, 4, .. , SIZE - 1])
/// and the output channels are just 0.
/// This test assures that when the buffers are zipped together,
/// the input values do not change.
#[test]
fn buffer_zip() {
let in1: Vec<f32> = (0..SIZE).map(|x| x as f32).collect();
let in2 = in1.clone();
let mut out1 = vec![0.0; SIZE];
let mut out2 = out1.clone();
let inputs = vec![in1.as_ptr(), in2.as_ptr()];
let mut outputs = vec![out1.as_mut_ptr(), out2.as_mut_ptr()];
let mut buffer = unsafe { AudioBuffer::from_raw(2, 2, inputs.as_ptr(), outputs.as_mut_ptr(), SIZE) };
for (input, output) in buffer.zip() {
input.iter().zip(output.iter_mut()).fold(0, |acc, (input, output)| {
assert_eq!(*input, acc as f32);
assert_eq!(*output, 0.0);
acc + 1
});
}
}
// Test that the `zip()` method returns an iterator that gives `n` elements
// where n is the number of inputs when this is lower than the number of outputs.
#[test]
fn buffer_zip_fewer_inputs_than_outputs() {
let in1 = vec![1.0; SIZE];
let in2 = vec![2.0; SIZE];
let mut out1 = vec![3.0; SIZE];
let mut out2 = vec![4.0; SIZE];
let mut out3 = vec![5.0; SIZE];
let inputs = vec![in1.as_ptr(), in2.as_ptr()];
let mut outputs = vec![out1.as_mut_ptr(), out2.as_mut_ptr(), out3.as_mut_ptr()];
let mut buffer = unsafe { AudioBuffer::from_raw(2, 3, inputs.as_ptr(), outputs.as_mut_ptr(), SIZE) };
let mut iter = buffer.zip();
if let Some((observed_in1, observed_out1)) = iter.next() {
assert_eq!(1.0, observed_in1[0]);
assert_eq!(3.0, observed_out1[0]);
} else {
unreachable!();
}
if let Some((observed_in2, observed_out2)) = iter.next() {
assert_eq!(2.0, observed_in2[0]);
assert_eq!(4.0, observed_out2[0]);
} else {
unreachable!();
}
assert_eq!(None, iter.next());
}
// Test that the `zip()` method returns an iterator that gives `n` elements
// where n is the number of outputs when this is lower than the number of inputs.
#[test]
fn buffer_zip_more_inputs_than_outputs() {
let in1 = vec![1.0; SIZE];
let in2 = vec![2.0; SIZE];
let in3 = vec![3.0; SIZE];
let mut out1 = vec![4.0; SIZE];
let mut out2 = vec![5.0; SIZE];
let inputs = vec![in1.as_ptr(), in2.as_ptr(), in3.as_ptr()];
let mut outputs = vec![out1.as_mut_ptr(), out2.as_mut_ptr()];
let mut buffer = unsafe { AudioBuffer::from_raw(3, 2, inputs.as_ptr(), outputs.as_mut_ptr(), SIZE) };
let mut iter = buffer.zip();
if let Some((observed_in1, observed_out1)) = iter.next() {
assert_eq!(1.0, observed_in1[0]);
assert_eq!(4.0, observed_out1[0]);
} else {
unreachable!();
}
if let Some((observed_in2, observed_out2)) = iter.next() {
assert_eq!(2.0, observed_in2[0]);
assert_eq!(5.0, observed_out2[0]);
} else {
unreachable!();
}
assert_eq!(None, iter.next());
}
/// Test that creating buffers from raw pointers works.
#[test]
fn from_raw() {
let in1: Vec<f32> = (0..SIZE).map(|x| x as f32).collect();
let in2 = in1.clone();
let mut out1 = vec![0.0; SIZE];
let mut out2 = out1.clone();
let inputs = vec![in1.as_ptr(), in2.as_ptr()];
let mut outputs = vec![out1.as_mut_ptr(), out2.as_mut_ptr()];
let mut buffer = unsafe { AudioBuffer::from_raw(2, 2, inputs.as_ptr(), outputs.as_mut_ptr(), SIZE) };
for (input, output) in buffer.zip() {
input.iter().zip(output.iter_mut()).fold(0, |acc, (input, output)| {
assert_eq!(*input, acc as f32);
assert_eq!(*output, 0.0);
acc + 1
});
}
}
}

View file

@ -0,0 +1,19 @@
use std::sync::Arc;
use crate::{editor::Editor, prelude::*};
pub(crate) struct PluginCache {
pub info: Info,
pub params: Arc<dyn PluginParameters>,
pub editor: Option<Box<dyn Editor>>,
}
impl PluginCache {
pub fn new(info: &Info, params: Arc<dyn PluginParameters>, editor: Option<Box<dyn Editor>>) -> Self {
Self {
info: info.clone(),
params,
editor,
}
}
}

View file

@ -0,0 +1,352 @@
//! Meta data for dealing with input / output channels. Not all hosts use this so it is not
//! necessary for plugin functionality.
use crate::api;
use crate::api::consts::{MAX_LABEL, MAX_SHORT_LABEL};
/// Information about an input / output channel. This isn't necessary for a channel to function but
/// informs the host how the channel is meant to be used.
pub struct ChannelInfo {
name: String,
short_name: String,
active: bool,
arrangement_type: SpeakerArrangementType,
}
impl ChannelInfo {
/// Construct a new `ChannelInfo` object.
///
/// `name` is a user friendly name for this channel limited to `MAX_LABEL` characters.
/// `short_name` is an optional field which provides a short name limited to `MAX_SHORT_LABEL`.
/// `active` determines whether this channel is active.
/// `arrangement_type` describes the arrangement type for this channel.
pub fn new(
name: String,
short_name: Option<String>,
active: bool,
arrangement_type: Option<SpeakerArrangementType>,
) -> ChannelInfo {
ChannelInfo {
name: name.clone(),
short_name: if let Some(short_name) = short_name {
short_name
} else {
name
},
active,
arrangement_type: arrangement_type.unwrap_or(SpeakerArrangementType::Custom),
}
}
}
impl Into<api::ChannelProperties> for ChannelInfo {
/// Convert to the VST api equivalent of this structure.
fn into(self) -> api::ChannelProperties {
api::ChannelProperties {
name: {
let mut label = [0; MAX_LABEL as usize];
for (b, c) in self.name.bytes().zip(label.iter_mut()) {
*c = b;
}
label
},
flags: {
let mut flag = api::ChannelFlags::empty();
if self.active {
flag |= api::ChannelFlags::ACTIVE
}
if self.arrangement_type.is_left_stereo() {
flag |= api::ChannelFlags::STEREO
}
if self.arrangement_type.is_speaker_type() {
flag |= api::ChannelFlags::SPEAKER
}
flag.bits()
},
arrangement_type: self.arrangement_type.into(),
short_name: {
let mut label = [0; MAX_SHORT_LABEL as usize];
for (b, c) in self.short_name.bytes().zip(label.iter_mut()) {
*c = b;
}
label
},
future: [0; 48],
}
}
}
impl From<api::ChannelProperties> for ChannelInfo {
fn from(api: api::ChannelProperties) -> ChannelInfo {
ChannelInfo {
name: String::from_utf8_lossy(&api.name).to_string(),
short_name: String::from_utf8_lossy(&api.short_name).to_string(),
active: api::ChannelFlags::from_bits(api.flags)
.expect("Invalid bits in channel info")
.intersects(api::ChannelFlags::ACTIVE),
arrangement_type: SpeakerArrangementType::from(api),
}
}
}
/// Target for Speaker arrangement type. Can be a cinema configuration or music configuration. Both
/// are technically identical but this provides extra information to the host.
pub enum ArrangementTarget {
/// Music arrangement. Technically identical to Cinema.
Music,
/// Cinematic arrangement. Technically identical to Music.
Cinema,
}
/// An enum for all channels in a stereo configuration.
pub enum StereoChannel {
/// Left channel.
Left,
/// Right channel.
Right,
}
/// Possible stereo speaker configurations.
#[allow(non_camel_case_types)]
pub enum StereoConfig {
/// Regular.
L_R,
/// Left surround, right surround.
Ls_Rs,
/// Left center, right center.
Lc_Rc,
/// Side left, side right.
Sl_Sr,
/// Center, low frequency effects.
C_Lfe,
}
/// Possible surround speaker configurations.
#[allow(non_camel_case_types)]
pub enum SurroundConfig {
/// 3.0 surround sound.
/// Cinema: L R C
/// Music: L R S
S3_0(ArrangementTarget),
/// 3.1 surround sound.
/// Cinema: L R C Lfe
/// Music: L R Lfe S
S3_1(ArrangementTarget),
/// 4.0 surround sound.
/// Cinema: L R C S (LCRS)
/// Music: L R Ls Rs (Quadro)
S4_0(ArrangementTarget),
/// 4.1 surround sound.
/// Cinema: L R C Lfe S (LCRS + Lfe)
/// Music: L R Ls Rs (Quadro + Lfe)
S4_1(ArrangementTarget),
/// 5.0 surround sound.
/// Cinema and music: L R C Ls Rs
S5_0,
/// 5.1 surround sound.
/// Cinema and music: L R C Lfe Ls Rs
S5_1,
/// 6.0 surround sound.
/// Cinema: L R C Ls Rs Cs
/// Music: L R Ls Rs Sl Sr
S6_0(ArrangementTarget),
/// 6.1 surround sound.
/// Cinema: L R C Lfe Ls Rs Cs
/// Music: L R Ls Rs Sl Sr
S6_1(ArrangementTarget),
/// 7.0 surround sound.
/// Cinema: L R C Ls Rs Lc Rc
/// Music: L R C Ls Rs Sl Sr
S7_0(ArrangementTarget),
/// 7.1 surround sound.
/// Cinema: L R C Lfe Ls Rs Lc Rc
/// Music: L R C Lfe Ls Rs Sl Sr
S7_1(ArrangementTarget),
/// 8.0 surround sound.
/// Cinema: L R C Ls Rs Lc Rc Cs
/// Music: L R C Ls Rs Cs Sl Sr
S8_0(ArrangementTarget),
/// 8.1 surround sound.
/// Cinema: L R C Lfe Ls Rs Lc Rc Cs
/// Music: L R C Lfe Ls Rs Cs Sl Sr
S8_1(ArrangementTarget),
/// 10.2 surround sound.
/// Cinema + Music: L R C Lfe Ls Rs Tfl Tfc Tfr Trl Trr Lfe2
S10_2,
}
/// Type representing how a channel is used. Only useful for some hosts.
pub enum SpeakerArrangementType {
/// Custom arrangement not specified to host.
Custom,
/// Empty arrangement.
Empty,
/// Mono channel.
Mono,
/// Stereo channel. Contains type of stereo arrangement and speaker represented.
Stereo(StereoConfig, StereoChannel),
/// Surround channel. Contains surround arrangement and target (cinema or music).
Surround(SurroundConfig),
}
impl Default for SpeakerArrangementType {
fn default() -> SpeakerArrangementType {
SpeakerArrangementType::Mono
}
}
impl SpeakerArrangementType {
/// Determine whether this channel is part of a surround speaker arrangement.
pub fn is_speaker_type(&self) -> bool {
if let SpeakerArrangementType::Surround(..) = *self {
true
} else {
false
}
}
/// Determine whether this channel is the left speaker in a stereo pair.
pub fn is_left_stereo(&self) -> bool {
if let SpeakerArrangementType::Stereo(_, StereoChannel::Left) = *self {
true
} else {
false
}
}
}
impl Into<api::SpeakerArrangementType> for SpeakerArrangementType {
/// Convert to VST API arrangement type.
fn into(self) -> api::SpeakerArrangementType {
use self::ArrangementTarget::{Cinema, Music};
use self::SpeakerArrangementType::*;
use api::SpeakerArrangementType as Raw;
match self {
Custom => Raw::Custom,
Empty => Raw::Empty,
Mono => Raw::Mono,
Stereo(conf, _) => {
match conf {
// Stereo channels.
StereoConfig::L_R => Raw::Stereo,
StereoConfig::Ls_Rs => Raw::StereoSurround,
StereoConfig::Lc_Rc => Raw::StereoCenter,
StereoConfig::Sl_Sr => Raw::StereoSide,
StereoConfig::C_Lfe => Raw::StereoCLfe,
}
}
Surround(conf) => {
match conf {
// Surround channels.
SurroundConfig::S3_0(Music) => Raw::Music30,
SurroundConfig::S3_0(Cinema) => Raw::Cinema30,
SurroundConfig::S3_1(Music) => Raw::Music31,
SurroundConfig::S3_1(Cinema) => Raw::Cinema31,
SurroundConfig::S4_0(Music) => Raw::Music40,
SurroundConfig::S4_0(Cinema) => Raw::Cinema40,
SurroundConfig::S4_1(Music) => Raw::Music41,
SurroundConfig::S4_1(Cinema) => Raw::Cinema41,
SurroundConfig::S5_0 => Raw::Surround50,
SurroundConfig::S5_1 => Raw::Surround51,
SurroundConfig::S6_0(Music) => Raw::Music60,
SurroundConfig::S6_0(Cinema) => Raw::Cinema60,
SurroundConfig::S6_1(Music) => Raw::Music61,
SurroundConfig::S6_1(Cinema) => Raw::Cinema61,
SurroundConfig::S7_0(Music) => Raw::Music70,
SurroundConfig::S7_0(Cinema) => Raw::Cinema70,
SurroundConfig::S7_1(Music) => Raw::Music71,
SurroundConfig::S7_1(Cinema) => Raw::Cinema71,
SurroundConfig::S8_0(Music) => Raw::Music80,
SurroundConfig::S8_0(Cinema) => Raw::Cinema80,
SurroundConfig::S8_1(Music) => Raw::Music81,
SurroundConfig::S8_1(Cinema) => Raw::Cinema81,
SurroundConfig::S10_2 => Raw::Surround102,
}
}
}
}
}
/// Convert the VST API equivalent struct into something more usable.
///
/// We implement `From<ChannelProperties>` as `SpeakerArrangementType` contains extra info about
/// stereo speakers found in the channel flags.
impl From<api::ChannelProperties> for SpeakerArrangementType {
fn from(api: api::ChannelProperties) -> SpeakerArrangementType {
use self::ArrangementTarget::{Cinema, Music};
use self::SpeakerArrangementType::*;
use self::SurroundConfig::*;
use api::SpeakerArrangementType as Raw;
let stereo = if api::ChannelFlags::from_bits(api.flags)
.expect("Invalid Channel Flags")
.intersects(api::ChannelFlags::STEREO)
{
StereoChannel::Left
} else {
StereoChannel::Right
};
match api.arrangement_type {
Raw::Custom => Custom,
Raw::Empty => Empty,
Raw::Mono => Mono,
Raw::Stereo => Stereo(StereoConfig::L_R, stereo),
Raw::StereoSurround => Stereo(StereoConfig::Ls_Rs, stereo),
Raw::StereoCenter => Stereo(StereoConfig::Lc_Rc, stereo),
Raw::StereoSide => Stereo(StereoConfig::Sl_Sr, stereo),
Raw::StereoCLfe => Stereo(StereoConfig::C_Lfe, stereo),
Raw::Music30 => Surround(S3_0(Music)),
Raw::Cinema30 => Surround(S3_0(Cinema)),
Raw::Music31 => Surround(S3_1(Music)),
Raw::Cinema31 => Surround(S3_1(Cinema)),
Raw::Music40 => Surround(S4_0(Music)),
Raw::Cinema40 => Surround(S4_0(Cinema)),
Raw::Music41 => Surround(S4_1(Music)),
Raw::Cinema41 => Surround(S4_1(Cinema)),
Raw::Surround50 => Surround(S5_0),
Raw::Surround51 => Surround(S5_1),
Raw::Music60 => Surround(S6_0(Music)),
Raw::Cinema60 => Surround(S6_0(Cinema)),
Raw::Music61 => Surround(S6_1(Music)),
Raw::Cinema61 => Surround(S6_1(Cinema)),
Raw::Music70 => Surround(S7_0(Music)),
Raw::Cinema70 => Surround(S7_0(Cinema)),
Raw::Music71 => Surround(S7_1(Music)),
Raw::Cinema71 => Surround(S7_1(Cinema)),
Raw::Music80 => Surround(S8_0(Music)),
Raw::Cinema80 => Surround(S8_0(Cinema)),
Raw::Music81 => Surround(S8_1(Music)),
Raw::Cinema81 => Surround(S8_1(Cinema)),
Raw::Surround102 => Surround(S10_2),
}
}
}

Some files were not shown because too many files have changed in this diff Show more