mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-02-01 00:36:40 +01:00
collect crates/ and deps/
This commit is contained in:
parent
2f8882f6cd
commit
8fa0f8a409
140 changed files with 23 additions and 21 deletions
28
crates/app/Cargo.toml
Normal file
28
crates/app/Cargo.toml
Normal 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"]
|
||||
0
crates/app/examples/arranger.edn
Normal file
0
crates/app/examples/arranger.edn
Normal file
0
crates/app/examples/clip.edn
Normal file
0
crates/app/examples/clip.edn
Normal file
12
crates/app/examples/mixer.edn
Normal file
12
crates/app/examples/mixer.edn
Normal 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)))
|
||||
0
crates/app/examples/sampler.edn
Normal file
0
crates/app/examples/sampler.edn
Normal file
18
crates/app/examples/sequencer.edn
Normal file
18
crates/app/examples/sequencer.edn
Normal 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
201
crates/app/src/api.rs
Normal 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
95
crates/app/src/audio.rs
Normal 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
10
crates/app/src/device.rs
Normal 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
177
crates/app/src/keys.rs
Normal 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:?}")
|
||||
});
|
||||
31
crates/app/src/keys/keys_clip.rs
Normal file
31
crates/app/src/keys/keys_clip.rs
Normal 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
|
||||
});
|
||||
8
crates/app/src/keys/keys_ins.rs
Normal file
8
crates/app/src/keys/keys_ins.rs
Normal 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
|
||||
},
|
||||
});
|
||||
8
crates/app/src/keys/keys_outs.rs
Normal file
8
crates/app/src/keys/keys_outs.rs
Normal 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
|
||||
},
|
||||
});
|
||||
35
crates/app/src/keys/keys_scene.rs
Normal file
35
crates/app/src/keys/keys_scene.rs
Normal 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
|
||||
});
|
||||
52
crates/app/src/keys/keys_track.rs
Normal file
52
crates/app/src/keys/keys_track.rs
Normal 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
50
crates/app/src/lib.rs
Normal 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
126
crates/app/src/model.rs
Normal 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);
|
||||
//});
|
||||
97
crates/app/src/model/model_scene.rs
Normal file
97
crates/app/src/model/model_scene.rs
Normal 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(&[]);
|
||||
}
|
||||
51
crates/app/src/model/model_select.rs
Normal file
51
crates/app/src/model/model_select.rs
Normal 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 }
|
||||
}
|
||||
103
crates/app/src/model/model_track.rs
Normal file
103
crates/app/src/model/model_track.rs
Normal 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
282
crates/app/src/view.rs
Normal 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();
|
||||
}
|
||||
88
crates/app/src/view/view_clock.rs
Normal file
88
crates/app/src/view/view_clock.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
32
crates/app/src/view/view_color.rs
Normal file
32
crates/app/src/view/view_color.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
178
crates/app/src/view/view_layout.rs
Normal file
178
crates/app/src/view/view_layout.rs
Normal 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, "");
|
||||
}
|
||||
}
|
||||
120
crates/app/src/view/view_memo.rs
Normal file
120
crates/app/src/view/view_memo.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
48
crates/app/src/view/view_meter.rs
Normal file
48
crates/app/src/view/view_meter.rs
Normal 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
140
crates/app/src/view/view_ports.rs
Normal file
140
crates/app/src/view/view_ports.rs
Normal 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()
|
||||
)))
|
||||
}
|
||||
}
|
||||
147
crates/app/src/view/view_track.rs
Normal file
147
crates/app/src/view/view_track.rs
Normal 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
12
crates/cli/Cargo.toml
Normal 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"
|
||||
5
crates/cli/edn/arranger.edn
Normal file
5
crates/cli/edn/arranger.edn
Normal 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))))
|
||||
21
crates/cli/edn/arranger_keys.edn
Normal file
21
crates/cli/edn/arranger_keys.edn
Normal 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)
|
||||
8
crates/cli/edn/arranger_keys_clip.edn
Normal file
8
crates/cli/edn/arranger_keys_clip.edn
Normal 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)
|
||||
0
crates/cli/edn/arranger_keys_mix.edn
Normal file
0
crates/cli/edn/arranger_keys_mix.edn
Normal file
7
crates/cli/edn/arranger_keys_scene.edn
Normal file
7
crates/cli/edn/arranger_keys_scene.edn
Normal 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)
|
||||
12
crates/cli/edn/arranger_keys_track.edn
Normal file
12
crates/cli/edn/arranger_keys_track.edn
Normal 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)
|
||||
6
crates/cli/edn/groovebox.edn
Normal file
6
crates/cli/edn/groovebox.edn
Normal 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))))))
|
||||
4
crates/cli/edn/sequencer.edn
Normal file
4
crates/cli/edn/sequencer.edn
Normal 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))))
|
||||
1
crates/cli/edn/transport.edn
Normal file
1
crates/cli/edn/transport.edn
Normal file
|
|
@ -0,0 +1 @@
|
|||
:transport
|
||||
184
crates/cli/tek.rs
Normal file
184
crates/cli/tek.rs
Normal 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
7
crates/jack/Cargo.toml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[package]
|
||||
name = "tek_jack"
|
||||
edition = "2021"
|
||||
version = "0.2.0"
|
||||
|
||||
[dependencies]
|
||||
jack = { workspace = true }
|
||||
152
crates/jack/src/jack_client.rs
Normal file
152
crates/jack/src/jack_client.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
197
crates/jack/src/jack_device.rs
Normal file
197
crates/jack/src/jack_device.rs
Normal 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 {}
|
||||
|
||||
/////////
|
||||
|
||||
/*
|
||||
*/
|
||||
52
crates/jack/src/jack_event.rs
Normal file
52
crates/jack/src/jack_event.rs
Normal 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
|
||||
}
|
||||
}
|
||||
217
crates/jack/src/jack_port.rs
Normal file
217
crates/jack/src/jack_port.rs
Normal 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
17
crates/jack/src/lib.rs
Normal 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
13
crates/midi/Cargo.toml
Normal 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 }
|
||||
6
crates/midi/edn/keys_clip_length.edn
Normal file
6
crates/midi/edn/keys_clip_length.edn
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
(@up inc)
|
||||
(@down dec)
|
||||
(@right next)
|
||||
(@left prev)
|
||||
(@return set :length)
|
||||
(@escape cancel)
|
||||
4
crates/midi/edn/keys_clip_rename.edn
Normal file
4
crates/midi/edn/keys_clip_rename.edn
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
(:char append :char)
|
||||
(@backspace delete :last)
|
||||
(@return confirm)
|
||||
(@escape cancel)
|
||||
21
crates/midi/edn/keys_edit.edn
Normal file
21
crates/midi/edn/keys_edit.edn
Normal 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)
|
||||
12
crates/midi/edn/keys_pool.edn
Normal file
12
crates/midi/edn/keys_pool.edn
Normal 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)
|
||||
8
crates/midi/edn/keys_pool_file.edn
Normal file
8
crates/midi/edn/keys_pool_file.edn
Normal 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)
|
||||
0
crates/midi/edn/piano-view-h.edn
Normal file
0
crates/midi/edn/piano-view-h.edn
Normal file
0
crates/midi/edn/piano-view-v.edn
Normal file
0
crates/midi/edn/piano-view-v.edn
Normal file
0
crates/midi/edn/view_pool.edn
Normal file
0
crates/midi/edn/view_pool.edn
Normal file
17
crates/midi/examples/midi-import.rs
Normal file
17
crates/midi/examples/midi-import.rs
Normal 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
86
crates/midi/src/lib.rs
Normal 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:?}");
|
||||
}
|
||||
107
crates/midi/src/midi_clip.rs
Normal file
107
crates/midi/src/midi_clip.rs
Normal 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 {}
|
||||
306
crates/midi/src/midi_edit.rs
Normal file
306
crates/midi/src/midi_edit.rs
Normal 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();
|
||||
}
|
||||
91
crates/midi/src/midi_in.rs
Normal file
91
crates/midi/src/midi_in.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
84
crates/midi/src/midi_launch.rs
Normal file
84
crates/midi/src/midi_launch.rs
Normal 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
160
crates/midi/src/midi_out.rs
Normal 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:?}");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
crates/midi/src/midi_pitch.rs
Normal file
23
crates/midi/src/midi_pitch.rs
Normal 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]
|
||||
}
|
||||
}
|
||||
192
crates/midi/src/midi_player.rs
Normal file
192
crates/midi/src/midi_player.rs
Normal 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
|
||||
}
|
||||
}
|
||||
50
crates/midi/src/midi_point.rs
Normal file
50
crates/midi/src/midi_point.rs
Normal 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) }
|
||||
}
|
||||
561
crates/midi/src/midi_pool.rs
Normal file
561
crates/midi/src/midi_pool.rs
Normal 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
|
||||
//})
|
||||
//}
|
||||
79
crates/midi/src/midi_range.rs
Normal file
79
crates/midi/src/midi_range.rs
Normal 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();
|
||||
}
|
||||
66
crates/midi/src/midi_view.rs
Normal file
66
crates/midi/src/midi_view.rs
Normal 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
318
crates/midi/src/piano_h.rs
Normal 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!(),
|
||||
}
|
||||
}
|
||||
34
crates/midi/src/piano_v.rs
Normal file
34
crates/midi/src/piano_v.rs
Normal 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
17
crates/plugin/Cargo.toml
Normal 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" ]
|
||||
0
crates/plugin/edn/plugin-keys.edn
Normal file
0
crates/plugin/edn/plugin-keys.edn
Normal file
0
crates/plugin/edn/plugin-view.edn
Normal file
0
crates/plugin/edn/plugin-view.edn
Normal file
8
crates/plugin/src/lib.rs
Normal file
8
crates/plugin/src/lib.rs
Normal 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
40
crates/plugin/src/lv2.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
59
crates/plugin/src/lv2_gui.rs
Normal file
59
crates/plugin/src/lv2_gui.rs
Normal 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
|
||||
}
|
||||
|
||||
47
crates/plugin/src/lv2_tui.rs
Normal file
47
crates/plugin/src/lv2_tui.rs
Normal 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
275
crates/plugin/src/plugin.rs
Normal 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)
|
||||
//});
|
||||
14
crates/plugin/src/vst2_tui.rs
Normal file
14
crates/plugin/src/vst2_tui.rs
Normal 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()?
|
||||
})
|
||||
}
|
||||
|
||||
2
crates/plugin/src/vst3_tui.rs
Normal file
2
crates/plugin/src/vst3_tui.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
//! TODO
|
||||
|
||||
33
crates/plugin/vst/.github/workflows/deploy.yml
vendored
Normal file
33
crates/plugin/vst/.github/workflows/deploy.yml
vendored
Normal 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
|
||||
46
crates/plugin/vst/.github/workflows/docs.yml
vendored
Normal file
46
crates/plugin/vst/.github/workflows/docs.yml
vendored
Normal 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
|
||||
38
crates/plugin/vst/.github/workflows/rust.yml
vendored
Normal file
38
crates/plugin/vst/.github/workflows/rust.yml
vendored
Normal 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
21
crates/plugin/vst/.gitignore
vendored
Normal 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
|
||||
*~
|
||||
86
crates/plugin/vst/CHANGELOG.md
Normal file
86
crates/plugin/vst/CHANGELOG.md
Normal 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).
|
||||
75
crates/plugin/vst/Cargo.toml
Normal file
75
crates/plugin/vst/Cargo.toml
Normal 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
21
crates/plugin/vst/LICENSE
Normal 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
112
crates/plugin/vst/README.md
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
# vst-rs
|
||||
[![crates.io][crates-img]][crates-url]
|
||||
[](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)
|
||||
222
crates/plugin/vst/examples/dimension_expander.rs
Normal file
222
crates/plugin/vst/examples/dimension_expander.rs
Normal 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);
|
||||
71
crates/plugin/vst/examples/fwd_midi.rs
Normal file
71
crates/plugin/vst/examples/fwd_midi.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
129
crates/plugin/vst/examples/gain_effect.rs
Normal file
129
crates/plugin/vst/examples/gain_effect.rs
Normal 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);
|
||||
248
crates/plugin/vst/examples/ladder_filter.rs
Normal file
248
crates/plugin/vst/examples/ladder_filter.rs
Normal 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);
|
||||
63
crates/plugin/vst/examples/simple_host.rs
Normal file
63
crates/plugin/vst/examples/simple_host.rs
Normal 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);
|
||||
}
|
||||
160
crates/plugin/vst/examples/sine_synth.rs
Normal file
160
crates/plugin/vst/examples/sine_synth.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
136
crates/plugin/vst/examples/transfer_and_smooth.rs
Normal file
136
crates/plugin/vst/examples/transfer_and_smooth.rs
Normal 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);
|
||||
61
crates/plugin/vst/osx_vst_bundler.sh
Executable file
61
crates/plugin/vst/osx_vst_bundler.sh
Executable 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
|
||||
1
crates/plugin/vst/rustfmt.toml
Normal file
1
crates/plugin/vst/rustfmt.toml
Normal file
|
|
@ -0,0 +1 @@
|
|||
max_width = 120
|
||||
927
crates/plugin/vst/src/api.rs
Normal file
927
crates/plugin/vst/src/api.rs
Normal 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!
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
606
crates/plugin/vst/src/buffer.rs
Normal file
606
crates/plugin/vst/src/buffer.rs
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
19
crates/plugin/vst/src/cache.rs
Normal file
19
crates/plugin/vst/src/cache.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
352
crates/plugin/vst/src/channels.rs
Normal file
352
crates/plugin/vst/src/channels.rs
Normal 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
Loading…
Add table
Add a link
Reference in a new issue