mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-04-03 21:00:44 +02:00
wip: nermalize
This commit is contained in:
parent
915e13aec8
commit
35197fb826
12 changed files with 4649 additions and 4718 deletions
586
src/arrange.rs
586
src/arrange.rs
|
|
@ -1,44 +1,7 @@
|
||||||
|
use ::std::sync::{Arc, RwLock};
|
||||||
/// Represents the current user selection in the arranger
|
use ::tengri::{space::east, color::ItemTheme};
|
||||||
#[derive(PartialEq, Clone, Copy, Debug, Default)] pub enum Selection {
|
use ::tengri::{draw::*, term::*};
|
||||||
#[default]
|
use crate::device::{MidiInput, MidiOutput, AudioInput, AudioOutput};
|
||||||
/// Nothing is selected
|
|
||||||
Nothing,
|
|
||||||
/// The whole mix is selected
|
|
||||||
Mix,
|
|
||||||
/// A MIDI input is selected.
|
|
||||||
Input(usize),
|
|
||||||
/// A MIDI output is selected.
|
|
||||||
Output(usize),
|
|
||||||
/// A scene is selected.
|
|
||||||
#[cfg(feature = "scene")] Scene(usize),
|
|
||||||
/// A track is selected.
|
|
||||||
#[cfg(feature = "track")] Track(usize),
|
|
||||||
/// A clip (track × scene) is selected.
|
|
||||||
#[cfg(feature = "track")] TrackClip { track: usize, scene: usize },
|
|
||||||
/// A track's MIDI input connection is selected.
|
|
||||||
#[cfg(feature = "track")] TrackInput { track: usize, port: usize },
|
|
||||||
/// A track's MIDI output connection is selected.
|
|
||||||
#[cfg(feature = "track")] TrackOutput { track: usize, port: usize },
|
|
||||||
/// A track device slot is selected.
|
|
||||||
#[cfg(feature = "track")] TrackDevice { track: usize, device: usize },
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A scene consists of a set of clips to play together.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let scene: tek::Scene = Default::default();
|
|
||||||
/// let _ = scene.pulses();
|
|
||||||
/// let _ = scene.is_playing(&[]);
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Default)] pub struct Scene {
|
|
||||||
/// Name of scene
|
|
||||||
pub name: Arc<str>,
|
|
||||||
/// Identifying color of scene
|
|
||||||
pub color: ItemTheme,
|
|
||||||
/// Clips in scene, one per track
|
|
||||||
pub clips: Vec<Option<Arc<RwLock<MidiClip>>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Arranger.
|
/// Arranger.
|
||||||
///
|
///
|
||||||
|
|
@ -155,6 +118,47 @@ pub trait ClipsView: TracksView + ScenesView {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents the current user selection in the arranger
|
||||||
|
#[derive(PartialEq, Clone, Copy, Debug, Default)] pub enum Selection {
|
||||||
|
#[default]
|
||||||
|
/// Nothing is selected
|
||||||
|
Nothing,
|
||||||
|
/// The whole mix is selected
|
||||||
|
Mix,
|
||||||
|
/// A MIDI input is selected.
|
||||||
|
Input(usize),
|
||||||
|
/// A MIDI output is selected.
|
||||||
|
Output(usize),
|
||||||
|
/// A scene is selected.
|
||||||
|
#[cfg(feature = "scene")] Scene(usize),
|
||||||
|
/// A track is selected.
|
||||||
|
#[cfg(feature = "track")] Track(usize),
|
||||||
|
/// A clip (track × scene) is selected.
|
||||||
|
#[cfg(feature = "track")] TrackClip { track: usize, scene: usize },
|
||||||
|
/// A track's MIDI input connection is selected.
|
||||||
|
#[cfg(feature = "track")] TrackInput { track: usize, port: usize },
|
||||||
|
/// A track's MIDI output connection is selected.
|
||||||
|
#[cfg(feature = "track")] TrackOutput { track: usize, port: usize },
|
||||||
|
/// A track device slot is selected.
|
||||||
|
#[cfg(feature = "track")] TrackDevice { track: usize, device: usize },
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A scene consists of a set of clips to play together.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let scene: tek::Scene = Default::default();
|
||||||
|
/// let _ = scene.pulses();
|
||||||
|
/// let _ = scene.is_playing(&[]);
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Default)] pub struct Scene {
|
||||||
|
/// Name of scene
|
||||||
|
pub name: Arc<str>,
|
||||||
|
/// Identifying color of scene
|
||||||
|
pub color: ItemTheme,
|
||||||
|
/// Clips in scene, one per track
|
||||||
|
pub clips: Vec<Option<Arc<RwLock<MidiClip>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll + Measured<Tui> {
|
pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll + Measured<Tui> {
|
||||||
|
|
||||||
fn tracks_width_available (&self) -> u16 {
|
fn tracks_width_available (&self) -> u16 {
|
||||||
|
|
@ -414,3 +418,503 @@ pub trait HasTrack: AsRefOpt<Track> + AsMutOpt<Track> {
|
||||||
self.track().map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs()))
|
self.track().map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl_as_ref!(Vec<Track>: |self: App| self.project.as_ref());
|
||||||
|
impl_as_mut!(Vec<Track>: |self: App| self.project.as_mut());
|
||||||
|
#[cfg(feature = "select")] impl_as_ref_opt!(Track: |self: App| self.project.as_ref_opt());
|
||||||
|
#[cfg(feature = "select")] impl_as_mut_opt!(Track: |self: App| self.project.as_mut_opt());
|
||||||
|
impl HasTrackScroll for App { fn track_scroll (&self) -> usize { self.project.track_scroll() } }
|
||||||
|
impl HasTrackScroll for Arrangement { fn track_scroll (&self) -> usize { self.track_scroll } }
|
||||||
|
|
||||||
|
impl HasWidth for 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 Track {
|
||||||
|
/// Create a new track with only the default [Sequencer].
|
||||||
|
pub fn new (
|
||||||
|
name: &impl AsRef<str>,
|
||||||
|
color: Option<ItemTheme>,
|
||||||
|
jack: &Jack<'static>,
|
||||||
|
clock: Option<&Clock>,
|
||||||
|
clip: Option<&Arc<RwLock<MidiClip>>>,
|
||||||
|
midi_from: &[Connect],
|
||||||
|
midi_to: &[Connect],
|
||||||
|
) -> Usually<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
name: name.as_ref().into(),
|
||||||
|
color: color.unwrap_or_default(),
|
||||||
|
sequencer: Sequencer::new(format!("{}/sequencer", name.as_ref()), jack, clock, clip, midi_from, midi_to)?,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn audio_ins (&self) -> &[AudioInput] {
|
||||||
|
self.devices.first().map(|x|x.audio_ins()).unwrap_or_default()
|
||||||
|
}
|
||||||
|
pub fn audio_outs (&self) -> &[AudioOutput] {
|
||||||
|
self.devices.last().map(|x|x.audio_outs()).unwrap_or_default()
|
||||||
|
}
|
||||||
|
fn _todo_opt_bool_stub_ (&self) -> Option<bool> { todo!() }
|
||||||
|
fn _todo_usize_stub_ (&self) -> usize { todo!() }
|
||||||
|
fn _todo_arc_str_stub_ (&self) -> Arc<str> { todo!() }
|
||||||
|
fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() }
|
||||||
|
pub fn per <'a, T: Draw<Tui> + 'a, U: TracksSizes<'a>> (
|
||||||
|
tracks: impl Fn() -> U + Send + Sync + 'a,
|
||||||
|
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
|
||||||
|
) -> impl Draw<Tui> + 'a {
|
||||||
|
iter(tracks,
|
||||||
|
move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{
|
||||||
|
let width = (x2 - x1) as u16;
|
||||||
|
iter_east(x1 as u16, width, w_exact(width, Tui::fg_bg(
|
||||||
|
track.color.lightest.rgb,
|
||||||
|
track.color.base.rgb,
|
||||||
|
callback(index, track))))})
|
||||||
|
}
|
||||||
|
/// Create a new track connecting the [Sequencer] to a [Sampler].
|
||||||
|
#[cfg(feature = "sampler")] pub fn new_with_sampler (
|
||||||
|
name: &impl AsRef<str>,
|
||||||
|
color: Option<ItemTheme>,
|
||||||
|
jack: &Jack<'static>,
|
||||||
|
clock: Option<&Clock>,
|
||||||
|
clip: Option<&Arc<RwLock<MidiClip>>>,
|
||||||
|
midi_from: &[Connect],
|
||||||
|
midi_to: &[Connect],
|
||||||
|
audio_from: &[&[Connect];2],
|
||||||
|
audio_to: &[&[Connect];2],
|
||||||
|
) -> Usually<Self> {
|
||||||
|
let mut track = Self::new(name, color, jack, clock, clip, midi_from, midi_to)?;
|
||||||
|
let client_name = jack.with_client(|c|c.name().to_string());
|
||||||
|
let port_name = track.sequencer.midi_outs[0].port_name();
|
||||||
|
let connect = [Connect::exact(format!("{client_name}:{}", port_name))];
|
||||||
|
track.devices.push(Device::Sampler(Sampler::new(
|
||||||
|
jack, &format!("{}/sampler", name.as_ref()), &connect, audio_from, audio_to
|
||||||
|
)?));
|
||||||
|
Ok(track)
|
||||||
|
}
|
||||||
|
#[cfg(feature = "sampler")] pub fn sampler (&self, mut nth: usize) -> Option<&Sampler> {
|
||||||
|
for device in self.devices.iter() {
|
||||||
|
match device {
|
||||||
|
Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; },
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
#[cfg(feature = "sampler")] pub fn sampler_mut (&mut self, mut nth: usize) -> Option<&mut Sampler> {
|
||||||
|
for device in self.devices.iter_mut() {
|
||||||
|
match device {
|
||||||
|
Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; },
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn per_track <'a, T: Draw<Tui> + 'a, U: TracksSizes<'a>> (
|
||||||
|
tracks: impl Fn() -> U + Send + Sync + 'a,
|
||||||
|
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
|
||||||
|
) -> impl Draw<Tui> + 'a {
|
||||||
|
per_track_top(tracks, move|index, track|h_full(origin_y(callback(index, track))))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn per_track_top <'a, T: Draw<Tui> + 'a, U: TracksSizes<'a>> (
|
||||||
|
tracks: impl Fn() -> U + Send + Sync + 'a,
|
||||||
|
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
|
||||||
|
) -> impl Draw<Tui> + 'a {
|
||||||
|
origin_x(Tui::bg(Reset, iter(tracks,
|
||||||
|
move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{
|
||||||
|
let width = (x2 - x1) as u16;
|
||||||
|
iter_east(x1 as u16, width, w_exact(width, Tui::fg_bg(
|
||||||
|
track.color.lightest.rgb,
|
||||||
|
track.color.base.rgb,
|
||||||
|
callback(index, track))))})))
|
||||||
|
}
|
||||||
|
#[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: App| self.project.as_ref_opt());
|
||||||
|
#[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: App| self.project.as_mut_opt());
|
||||||
|
#[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: Arrangement| self.selected_scene());
|
||||||
|
#[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: Arrangement| self.selected_scene_mut());
|
||||||
|
impl HasSceneScroll for App { fn scene_scroll (&self) -> usize { self.project.scene_scroll() } }
|
||||||
|
impl HasSceneScroll for Arrangement { fn scene_scroll (&self) -> usize { self.scene_scroll } }
|
||||||
|
impl ScenesView for App {
|
||||||
|
fn w_mid (&self) -> u16 { (self.measure_width() as u16).saturating_sub(self.w_side()) }
|
||||||
|
fn w_side (&self) -> u16 { 20 }
|
||||||
|
fn h_scenes (&self) -> u16 { (self.measure_height() as u16).saturating_sub(20) }
|
||||||
|
}
|
||||||
|
impl Scene {
|
||||||
|
/// Returns the pulse length of the longest clip in the scene
|
||||||
|
pub fn pulses (&self) -> usize {
|
||||||
|
self.clips.iter().fold(0, |a, p|{
|
||||||
|
a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/// Returns true if all clips in the scene are
|
||||||
|
/// currently playing on the given collection of tracks.
|
||||||
|
pub 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.sequencer().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 }
|
||||||
|
}
|
||||||
|
fn _todo_opt_bool_stub_ (&self) -> Option<bool> { todo!() }
|
||||||
|
fn _todo_usize_stub_ (&self) -> usize { todo!() }
|
||||||
|
fn _todo_arc_str_stub_ (&self) -> Arc<str> { todo!() }
|
||||||
|
fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Selection {
|
||||||
|
pub fn describe (
|
||||||
|
&self,
|
||||||
|
#[cfg(feature = "track")] tracks: &[Track],
|
||||||
|
#[cfg(feature = "scene")] scenes: &[Scene],
|
||||||
|
) -> Arc<str> {
|
||||||
|
use Selection::*;
|
||||||
|
format!("{}", match self {
|
||||||
|
Mix => "Everything".to_string(),
|
||||||
|
#[cfg(feature = "scene")] Scene(s) =>
|
||||||
|
scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name)).unwrap_or_else(||"S??".into()),
|
||||||
|
#[cfg(feature = "track")] Track(t) =>
|
||||||
|
tracks.get(*t).map(|track|format!("T{t}: {}", &track.name)).unwrap_or_else(||"T??".into()),
|
||||||
|
TrackClip { track, scene } => match (tracks.get(*track), scenes.get(*scene)) {
|
||||||
|
(Some(_), Some(s)) => match s.clip(*track) {
|
||||||
|
Some(clip) => format!("T{track} S{scene} C{}", &clip.read().unwrap().name),
|
||||||
|
None => format!("T{track} S{scene}: Empty")
|
||||||
|
},
|
||||||
|
_ => format!("T{track} S{scene}: Empty"),
|
||||||
|
},
|
||||||
|
_ => todo!()
|
||||||
|
}).into()
|
||||||
|
}
|
||||||
|
#[cfg(feature = "scene")] pub fn scene (&self) -> Option<usize> {
|
||||||
|
use Selection::*;
|
||||||
|
match self { Scene(scene) | TrackClip { scene, .. } => Some(*scene), _ => None }
|
||||||
|
}
|
||||||
|
#[cfg(feature = "scene")] pub fn select_scene (&self, scene_count: usize) -> Self {
|
||||||
|
use Selection::*;
|
||||||
|
match self {
|
||||||
|
Mix | Track(_) => Scene(0),
|
||||||
|
Scene(s) => Scene((s + 1) % scene_count),
|
||||||
|
TrackClip { scene, .. } => Track(*scene),
|
||||||
|
_ => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(feature = "scene")] pub fn select_scene_next (&self, len: usize) -> Self {
|
||||||
|
use Selection::*;
|
||||||
|
match self {
|
||||||
|
Mix => Scene(0),
|
||||||
|
Track(t) => TrackClip { track: *t, scene: 0 },
|
||||||
|
Scene(s) => if s + 1 < len { Scene(s + 1) } else { Mix },
|
||||||
|
TrackClip { track, scene } => if scene + 1 < len { TrackClip { track: *track, scene: scene + 1 } } else { Track(*track) },
|
||||||
|
_ => todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(feature = "scene")] pub fn select_scene_prev (&self) -> Self {
|
||||||
|
use Selection::*;
|
||||||
|
match self {
|
||||||
|
Mix | Scene(0) => Mix,
|
||||||
|
Scene(s) => Scene(s - 1),
|
||||||
|
Track(t) => Track(*t),
|
||||||
|
TrackClip { track, scene: 0 } => Track(*track),
|
||||||
|
TrackClip { track, scene } => TrackClip { track: *track, scene: scene - 1 },
|
||||||
|
_ => todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(feature = "track")] pub fn track (&self) -> Option<usize> {
|
||||||
|
use Selection::*;
|
||||||
|
if let Track(track)|TrackClip{track,..}|TrackInput{track,..}|TrackOutput{track,..}|TrackDevice{track,..} = self {
|
||||||
|
Some(*track)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(feature = "track")] pub fn select_track (&self, track_count: usize) -> Self {
|
||||||
|
use Selection::*;
|
||||||
|
match self {
|
||||||
|
Mix => Track(0),
|
||||||
|
Scene(_) => Mix,
|
||||||
|
Track(t) => Track((t + 1) % track_count),
|
||||||
|
TrackClip { track, .. } => Track(*track),
|
||||||
|
_ => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(feature = "track")] pub fn select_track_next (&self, len: usize) -> Self {
|
||||||
|
use Selection::*;
|
||||||
|
match self {
|
||||||
|
Mix => Track(0),
|
||||||
|
Scene(s) => TrackClip { track: 0, scene: *s },
|
||||||
|
Track(t) => if t + 1 < len { Track(t + 1) } else { Mix },
|
||||||
|
TrackClip {track, scene} => if track + 1 < len { TrackClip { track: track + 1, scene: *scene } } else { Scene(*scene) },
|
||||||
|
_ => todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(feature = "track")] pub fn select_track_prev (&self) -> Self {
|
||||||
|
use Selection::*;
|
||||||
|
match self {
|
||||||
|
Mix => Mix,
|
||||||
|
Scene(s) => Scene(*s),
|
||||||
|
Track(0) => Mix,
|
||||||
|
Track(t) => Track(t - 1),
|
||||||
|
TrackClip { track: 0, scene } => Scene(*scene),
|
||||||
|
TrackClip { track: t, scene } => TrackClip { track: t - 1, scene: *scene },
|
||||||
|
_ => todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl_has!(Jack<'static>: |self: Arrangement| self.jack);
|
||||||
|
impl_has!(Measure<Tui>: |self: Arrangement| self.size);
|
||||||
|
impl_has!(Vec<Track>: |self: Arrangement| self.tracks);
|
||||||
|
impl_has!(Vec<Scene>: |self: Arrangement| self.scenes);
|
||||||
|
impl_has!(Vec<MidiInput>: |self: Arrangement| self.midi_ins);
|
||||||
|
impl_has!(Vec<MidiOutput>: |self: Arrangement| self.midi_outs);
|
||||||
|
impl_has!(Clock: |self: Arrangement| self.clock);
|
||||||
|
impl_has!(Selection: |self: Arrangement| self.selection);
|
||||||
|
impl_as_ref_opt!(MidiEditor: |self: Arrangement| self.editor.as_ref());
|
||||||
|
impl_as_mut_opt!(MidiEditor: |self: Arrangement| self.editor.as_mut());
|
||||||
|
impl_as_ref_opt!(Track: |self: Arrangement| self.selected_track());
|
||||||
|
impl_as_mut_opt!(Track: |self: Arrangement| self.selected_track_mut());
|
||||||
|
impl Arrangement {
|
||||||
|
/// Create a new arrangement.
|
||||||
|
pub fn new (
|
||||||
|
jack: &Jack<'static>,
|
||||||
|
name: Option<Arc<str>>,
|
||||||
|
clock: Clock,
|
||||||
|
tracks: Vec<Track>,
|
||||||
|
scenes: Vec<Scene>,
|
||||||
|
midi_ins: Vec<MidiInput>,
|
||||||
|
midi_outs: Vec<MidiOutput>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
clock, tracks, scenes, midi_ins, midi_outs,
|
||||||
|
jack: jack.clone(),
|
||||||
|
name: name.unwrap_or_default(),
|
||||||
|
color: ItemTheme::random(),
|
||||||
|
selection: Selection::TrackClip { track: 0, scene: 0 },
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Width of display
|
||||||
|
pub fn w (&self) -> u16 {
|
||||||
|
self.size.w() as u16
|
||||||
|
}
|
||||||
|
/// Width allocated for sidebar.
|
||||||
|
pub fn w_sidebar (&self, is_editing: bool) -> u16 {
|
||||||
|
self.w() / if is_editing { 16 } else { 8 } as u16
|
||||||
|
}
|
||||||
|
/// Width available to display tracks.
|
||||||
|
pub fn w_tracks_area (&self, is_editing: bool) -> u16 {
|
||||||
|
self.w().saturating_sub(self.w_sidebar(is_editing))
|
||||||
|
}
|
||||||
|
/// Height of display
|
||||||
|
pub fn h (&self) -> u16 {
|
||||||
|
self.size.h() as u16
|
||||||
|
}
|
||||||
|
/// Height taken by visible device slots.
|
||||||
|
pub fn h_devices (&self) -> u16 {
|
||||||
|
2
|
||||||
|
//1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0)
|
||||||
|
}
|
||||||
|
/// Add multiple tracks
|
||||||
|
#[cfg(feature = "track")] pub fn tracks_add (
|
||||||
|
&mut self,
|
||||||
|
count: usize, width: Option<usize>,
|
||||||
|
mins: &[Connect], mouts: &[Connect],
|
||||||
|
) -> Usually<()> {
|
||||||
|
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 track = self.track_add(None, Some(color), mins, mouts)?.1;
|
||||||
|
if let Some(width) = width {
|
||||||
|
track.width = width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
/// Add a track
|
||||||
|
#[cfg(feature = "track")] pub fn track_add (
|
||||||
|
&mut self,
|
||||||
|
name: Option<&str>, color: Option<ItemTheme>,
|
||||||
|
mins: &[Connect], mouts: &[Connect],
|
||||||
|
) -> Usually<(usize, &mut Track)> {
|
||||||
|
let name: Arc<str> = name.map_or_else(
|
||||||
|
||format!("trk{:02}", self.track_last).into(),
|
||||||
|
|x|x.to_string().into()
|
||||||
|
);
|
||||||
|
self.track_last += 1;
|
||||||
|
let track = Track {
|
||||||
|
width: (name.len() + 2).max(12),
|
||||||
|
color: color.unwrap_or_else(ItemTheme::random),
|
||||||
|
sequencer: Sequencer::new(
|
||||||
|
&format!("{name}"),
|
||||||
|
self.jack(),
|
||||||
|
Some(self.clock()),
|
||||||
|
None,
|
||||||
|
mins,
|
||||||
|
mouts
|
||||||
|
)?,
|
||||||
|
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]))
|
||||||
|
}
|
||||||
|
#[cfg(feature = "track")] pub fn view_inputs (&self, _theme: ItemTheme) -> impl Draw<Tui> + '_ {
|
||||||
|
south(
|
||||||
|
h_exact(1, self.view_inputs_header()),
|
||||||
|
Thunk::new(|to: &mut Tui|{
|
||||||
|
for (index, port) in self.midi_ins().iter().enumerate() {
|
||||||
|
to.place(&x_push(index as u16 * 10, h_exact(1, self.view_inputs_row(port))))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
#[cfg(feature = "track")] fn view_inputs_header (&self) -> impl Draw<Tui> + '_ {
|
||||||
|
east(w_exact(20, origin_w(button_3("i", "nput ", format!("{}", self.midi_ins.len()), false))),
|
||||||
|
west(w_exact(4, button_2("I", "+", false)), Thunk::new(move|to: &mut Tui|for (_index, track, x1, _x2) in self.tracks_with_sizes() {
|
||||||
|
#[cfg(feature = "track")]
|
||||||
|
to.place(&x_push(x1 as u16, Tui::bg(track.color.dark.rgb, origin_w(w_exact(track.width as u16, east!(
|
||||||
|
either(track.sequencer.monitoring, Tui::fg(Green, "mon "), "mon "),
|
||||||
|
either(track.sequencer.recording, Tui::fg(Red, "rec "), "rec "),
|
||||||
|
either(track.sequencer.overdub, Tui::fg(Yellow, "dub "), "dub "),
|
||||||
|
))))))
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
#[cfg(feature = "track")] fn view_inputs_row (&self, port: &MidiInput) -> impl Draw<Tui> {
|
||||||
|
east(w_exact(20, origin_w(east(" ● ", Tui::bold(true, Tui::fg(Rgb(255,255,255), port.port_name()))))),
|
||||||
|
west(w_exact(4, ()), Thunk::new(move|to: &mut Tui|for (_index, track, _x1, _x2) in self.tracks_with_sizes() {
|
||||||
|
#[cfg(feature = "track")]
|
||||||
|
to.place(&Tui::bg(track.color.darker.rgb, origin_w(w_exact(track.width as u16, east!(
|
||||||
|
either(track.sequencer.monitoring, Tui::fg(Green, " ● "), " · "),
|
||||||
|
either(track.sequencer.recording, Tui::fg(Red, " ● "), " · "),
|
||||||
|
either(track.sequencer.overdub, Tui::fg(Yellow, " ● "), " · "),
|
||||||
|
)))))
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
#[cfg(feature = "track")] pub fn view_outputs (&self, theme: ItemTheme) -> impl Draw<Tui> {
|
||||||
|
let mut h = 1;
|
||||||
|
for output in self.midi_outs().iter() {
|
||||||
|
h += 1 + output.connections.len();
|
||||||
|
}
|
||||||
|
let h = h as u16;
|
||||||
|
let list = south(
|
||||||
|
h_exact(1, w_full(origin_w(button_3("o", "utput", format!("{}", self.midi_outs.len()), false)))),
|
||||||
|
h_exact(h - 1, wh_full(origin_nw(Thunk::new(|to: &mut Tui|{
|
||||||
|
for (_index, port) in self.midi_outs().iter().enumerate() {
|
||||||
|
to.place(&h_exact(1,w_full(east(
|
||||||
|
origin_w(east(" ● ", Tui::fg(Rgb(255,255,255),Tui::bold(true, port.port_name())))),
|
||||||
|
w_full(origin_e(format!("{}/{} ",
|
||||||
|
port.port().get_connections().len(),
|
||||||
|
port.connections.len())))))));
|
||||||
|
for (index, conn) in port.connections.iter().enumerate() {
|
||||||
|
to.place(&h_exact(1, w_full(origin_w(format!(" c{index:02}{}", conn.info())))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})))));
|
||||||
|
h_exact(h, view_track_row_section(theme, list, button_2("O", "+", false),
|
||||||
|
Tui::bg(theme.darker.rgb, origin_w(w_full(
|
||||||
|
Thunk::new(|to: &mut Tui|{
|
||||||
|
for (index, track, _x1, _x2) in self.tracks_with_sizes() {
|
||||||
|
to.place(&w_exact(track_width(index, track),
|
||||||
|
Thunk::new(|to: &mut Tui|{
|
||||||
|
to.place(&h_exact(1, origin_w(east(
|
||||||
|
either(true, Tui::fg(Green, "play "), "play "),
|
||||||
|
either(false, Tui::fg(Yellow, "solo "), "solo "),
|
||||||
|
))));
|
||||||
|
for (_index, port) in self.midi_outs().iter().enumerate() {
|
||||||
|
to.place(&h_exact(1, origin_w(east(
|
||||||
|
either(true, Tui::fg(Green, " ● "), " · "),
|
||||||
|
either(false, Tui::fg(Yellow, " ● "), " · "),
|
||||||
|
))));
|
||||||
|
for (_index, _conn) in port.connections.iter().enumerate() {
|
||||||
|
to.place(&h_exact(1, w_full("")));
|
||||||
|
}
|
||||||
|
}})))}}))))))
|
||||||
|
}
|
||||||
|
#[cfg(feature = "track")] pub fn view_track_devices (&self, theme: ItemTheme) -> impl Draw<Tui> {
|
||||||
|
let mut h = 2u16;
|
||||||
|
for track in self.tracks().iter() {
|
||||||
|
h = h.max(track.devices.len() as u16 * 2);
|
||||||
|
}
|
||||||
|
view_track_row_section(theme,
|
||||||
|
button_3("d", "evice", format!("{}", self.track().map(|t|t.devices.len()).unwrap_or(0)), false),
|
||||||
|
button_2("D", "+", false),
|
||||||
|
Thunk::new(move|to: &mut Tui|for (index, track, _x1, _x2) in self.tracks_with_sizes() {
|
||||||
|
to.place(&wh_exact(track_width(index, track), h + 1,
|
||||||
|
Tui::bg(track.color.dark.rgb, origin_nw(iter_south(2, move||0..h,
|
||||||
|
|_, _index|wh_exact(track.width as u16, 2,
|
||||||
|
Tui::fg_bg(
|
||||||
|
ItemTheme::G[32].lightest.rgb,
|
||||||
|
ItemTheme::G[32].dark.rgb,
|
||||||
|
origin_nw(format!(" · {}", "--")))))))));
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/// Put a clip in a slot
|
||||||
|
#[cfg(feature = "clip")] pub fn clip_put (
|
||||||
|
&mut self, track: usize, scene: usize, clip: Option<Arc<RwLock<MidiClip>>>
|
||||||
|
) -> Option<Arc<RwLock<MidiClip>>> {
|
||||||
|
let old = self.scenes[scene].clips[track].clone();
|
||||||
|
self.scenes[scene].clips[track] = clip;
|
||||||
|
old
|
||||||
|
}
|
||||||
|
/// Change the color of a clip, returning the previous one
|
||||||
|
#[cfg(feature = "clip")] pub fn clip_set_color (&self, track: usize, scene: usize, color: ItemTheme)
|
||||||
|
-> Option<ItemTheme>
|
||||||
|
{
|
||||||
|
self.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:?}");
|
||||||
|
//old
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/// Toggle looping for the active clip
|
||||||
|
#[cfg(feature = "clip")] pub fn toggle_loop (&mut self) {
|
||||||
|
if let Some(clip) = self.selected_clip() {
|
||||||
|
clip.write().unwrap().toggle_loop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Get the first sampler of the active track
|
||||||
|
#[cfg(feature = "sampler")] pub fn sampler (&self) -> Option<&Sampler> {
|
||||||
|
self.selected_track()?.sampler(0)
|
||||||
|
}
|
||||||
|
/// Get the first sampler of the active track
|
||||||
|
#[cfg(feature = "sampler")] pub fn sampler_mut (&mut self) -> Option<&mut Sampler> {
|
||||||
|
self.selected_track_mut()?.sampler_mut(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ScenesView for Arrangement {
|
||||||
|
fn h_scenes (&self) -> u16 {
|
||||||
|
(self.measure_height() as u16).saturating_sub(20)
|
||||||
|
}
|
||||||
|
fn w_side (&self) -> u16 {
|
||||||
|
(self.measure_width() as u16 * 2 / 10).max(20)
|
||||||
|
}
|
||||||
|
fn w_mid (&self) -> u16 {
|
||||||
|
(self.measure_width() as u16).saturating_sub(2 * self.w_side()).max(40)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl HasClipsSize for Arrangement {
|
||||||
|
fn clips_size (&self) -> &Measure<Tui> { &self.size_inner }
|
||||||
|
}
|
||||||
|
|
|
||||||
254
src/browse.rs
254
src/browse.rs
|
|
@ -1,3 +1,7 @@
|
||||||
|
use ::std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::*}};
|
||||||
|
use crate::sequence::MidiClip;
|
||||||
|
use crate::sample::Sample;
|
||||||
|
|
||||||
/// Browses for files to load/save.
|
/// Browses for files to load/save.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
|
|
@ -95,3 +99,253 @@ pub struct PoolView<'a>(pub &'a Pool);
|
||||||
/// Editing the number of ticks
|
/// Editing the number of ticks
|
||||||
Tick,
|
Tick,
|
||||||
}
|
}
|
||||||
|
has_clip!(|self: Pool|self.clips().get(self.clip_index()).map(|c|c.clone()));
|
||||||
|
impl_has_clips!(|self: Pool|self.clips);
|
||||||
|
impl_from!(Pool: |clip:&Arc<RwLock<MidiClip>>|{
|
||||||
|
let model = Self::default();
|
||||||
|
model.clips.write().unwrap().push(clip.clone());
|
||||||
|
model.clip.store(1, Relaxed);
|
||||||
|
model
|
||||||
|
});
|
||||||
|
impl_default!(Pool: Self {
|
||||||
|
browse: None,
|
||||||
|
clip: 0.into(),
|
||||||
|
clips: Arc::from(RwLock::from(vec![])),
|
||||||
|
mode: None,
|
||||||
|
samples: Arc::from(RwLock::from(vec![])),
|
||||||
|
visible: true,
|
||||||
|
});
|
||||||
|
impl Pool {
|
||||||
|
pub fn clip_index (&self) -> usize {
|
||||||
|
self.clip.load(Relaxed)
|
||||||
|
}
|
||||||
|
pub fn set_clip_index (&self, value: usize) {
|
||||||
|
self.clip.store(value, Relaxed);
|
||||||
|
}
|
||||||
|
pub fn mode (&self) -> &Option<PoolMode> {
|
||||||
|
&self.mode
|
||||||
|
}
|
||||||
|
pub fn mode_mut (&mut self) -> &mut Option<PoolMode> {
|
||||||
|
&mut self.mode
|
||||||
|
}
|
||||||
|
pub 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
|
||||||
|
));
|
||||||
|
}
|
||||||
|
pub 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
|
||||||
|
));
|
||||||
|
}
|
||||||
|
pub fn begin_import (&mut self) -> Usually<()> {
|
||||||
|
*self.mode_mut() = Some(PoolMode::Import(
|
||||||
|
self.clip_index(),
|
||||||
|
Browse::new(None)?
|
||||||
|
));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn begin_export (&mut self) -> Usually<()> {
|
||||||
|
*self.mode_mut() = Some(PoolMode::Export(
|
||||||
|
self.clip_index(),
|
||||||
|
Browse::new(None)?
|
||||||
|
));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn new_clip (&self) -> MidiClip {
|
||||||
|
MidiClip::new("Clip", true, 4 * PPQ, None, Some(ItemTheme::random()))
|
||||||
|
}
|
||||||
|
pub fn cloned_clip (&self) -> MidiClip {
|
||||||
|
let index = self.clip_index();
|
||||||
|
let mut clip = self.clips()[index].read().unwrap().duplicate();
|
||||||
|
clip.color = ItemTheme::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)
|
||||||
|
}
|
||||||
|
pub fn delete_clip (&mut self, clip: &MidiClip) -> bool {
|
||||||
|
let index = self.clips.read().unwrap().iter().position(|x|*x.read().unwrap()==*clip);
|
||||||
|
if let Some(index) = index {
|
||||||
|
self.clips.write().unwrap().remove(index);
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ClipLengthFocus {
|
||||||
|
pub fn next (&mut self) {
|
||||||
|
use ClipLengthFocus::*;
|
||||||
|
*self = match self { Bar => Beat, Beat => Tick, Tick => Bar, }
|
||||||
|
}
|
||||||
|
pub fn prev (&mut self) {
|
||||||
|
use ClipLengthFocus::*;
|
||||||
|
*self = match self { Bar => Tick, Beat => Bar, Tick => Beat, }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ClipLength {
|
||||||
|
pub fn _new (pulses: usize, focus: Option<ClipLengthFocus>) -> Self {
|
||||||
|
Self { ppq: PPQ, bpb: 4, pulses, focus }
|
||||||
|
}
|
||||||
|
pub fn bars (&self) -> usize {
|
||||||
|
self.pulses / (self.bpb * self.ppq)
|
||||||
|
}
|
||||||
|
pub fn beats (&self) -> usize {
|
||||||
|
(self.pulses % (self.bpb * self.ppq)) / self.ppq
|
||||||
|
}
|
||||||
|
pub fn ticks (&self) -> usize {
|
||||||
|
self.pulses % self.ppq
|
||||||
|
}
|
||||||
|
pub fn bars_string (&self) -> Arc<str> {
|
||||||
|
format!("{}", self.bars()).into()
|
||||||
|
}
|
||||||
|
pub fn beats_string (&self) -> Arc<str> {
|
||||||
|
format!("{}", self.beats()).into()
|
||||||
|
}
|
||||||
|
pub fn ticks_string (&self) -> Arc<str> {
|
||||||
|
format!("{:>02}", self.ticks()).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pool {
|
||||||
|
fn _todo_usize_ (&self) -> usize { todo!() }
|
||||||
|
fn _todo_bool_ (&self) -> bool { todo!() }
|
||||||
|
fn _todo_clip_ (&self) -> MidiClip { todo!() }
|
||||||
|
fn _todo_path_ (&self) -> PathBuf { todo!() }
|
||||||
|
fn _todo_color_ (&self) -> ItemColor { todo!() }
|
||||||
|
fn _todo_str_ (&self) -> Arc<str> { todo!() }
|
||||||
|
fn _clip_new (&self) -> MidiClip { self.new_clip() }
|
||||||
|
fn _clip_cloned (&self) -> MidiClip { self.cloned_clip() }
|
||||||
|
fn _clip_index_current (&self) -> usize { 0 }
|
||||||
|
fn _clip_index_after (&self) -> usize { 0 }
|
||||||
|
fn _clip_index_previous (&self) -> usize { 0 }
|
||||||
|
fn _clip_index_next (&self) -> usize { 0 }
|
||||||
|
fn _color_random (&self) -> ItemColor { ItemColor::random() }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> PoolView<'a> {
|
||||||
|
fn tui (&self) -> impl Draw<Tui> {
|
||||||
|
let Self(pool) = self;
|
||||||
|
//let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||Tui::g(32).into());
|
||||||
|
//let on_bg = |x|x;//below(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 height = pool.clips.read().unwrap().len() as u16;
|
||||||
|
w_exact(20, h_full(origin_n(iter(
|
||||||
|
||pool.clips().clone().into_iter(),
|
||||||
|
move|clip: Arc<RwLock<MidiClip>>, i: usize|{
|
||||||
|
let item_height = 1;
|
||||||
|
let item_offset = i as u16 * item_height;
|
||||||
|
let selected = i == pool.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 false { format!(" {i:>3}") } else { format!(" {i:>3} {name}") };
|
||||||
|
let length = if false { String::default() } else { format!("{length} ") };
|
||||||
|
h_exact(1, iter_south(item_offset, item_height, Tui::bg(bg, below!(
|
||||||
|
w_full(origin_w(Tui::fg(fg, Tui::bold(selected, name)))),
|
||||||
|
w_full(origin_e(Tui::fg(fg, Tui::bold(selected, length)))),
|
||||||
|
w_full(origin_w(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "▶"))))),
|
||||||
|
w_full(origin_e(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "◀"))))),
|
||||||
|
))))
|
||||||
|
}))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ClipLength {
|
||||||
|
fn tui (&self) -> impl Draw<Tui> {
|
||||||
|
use ClipLengthFocus::*;
|
||||||
|
let bars = ||self.bars_string();
|
||||||
|
let beats = ||self.beats_string();
|
||||||
|
let ticks = ||self.ticks_string();
|
||||||
|
match self.focus {
|
||||||
|
None => east!(" ", bars(), ".", beats(), ".", ticks()),
|
||||||
|
Some(Bar) => east!("[", bars(), "]", beats(), ".", ticks()),
|
||||||
|
Some(Beat) => east!(" ", bars(), "[", beats(), "]", ticks()),
|
||||||
|
Some(Tick) => east!(" ", bars(), ".", beats(), "[", ticks()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Browse {
|
||||||
|
pub fn new (cwd: Option<PathBuf>) -> Usually<Self> {
|
||||||
|
let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? };
|
||||||
|
let mut dirs = vec![];
|
||||||
|
let mut files = vec![];
|
||||||
|
for entry in std::fs::read_dir(&cwd)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let name = entry.file_name();
|
||||||
|
let decoded = name.clone().into_string().unwrap_or_else(|_|"<unreadable>".to_string());
|
||||||
|
let meta = entry.metadata()?;
|
||||||
|
if meta.is_dir() {
|
||||||
|
dirs.push((name, format!("📁 {decoded}")));
|
||||||
|
} else if meta.is_file() {
|
||||||
|
files.push((name, format!("📄 {decoded}")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Self { cwd, dirs, files, ..Default::default() })
|
||||||
|
}
|
||||||
|
pub fn chdir (&self) -> Usually<Self> { Self::new(Some(self.path())) }
|
||||||
|
pub fn len (&self) -> usize { self.dirs.len() + self.files.len() }
|
||||||
|
pub fn is_dir (&self) -> bool { self.index < self.dirs.len() }
|
||||||
|
pub fn is_file (&self) -> bool { self.index >= self.dirs.len() }
|
||||||
|
pub fn path (&self) -> PathBuf {
|
||||||
|
self.cwd.join(if self.is_dir() {
|
||||||
|
&self.dirs[self.index].0
|
||||||
|
} else if self.is_file() {
|
||||||
|
&self.files[self.index - self.dirs.len()].0
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn _todo_stub_path_buf (&self) -> PathBuf { todo!() }
|
||||||
|
fn _todo_stub_usize (&self) -> usize { todo!() }
|
||||||
|
fn _todo_stub_arc_str (&self) -> Arc<str> { todo!() }
|
||||||
|
}
|
||||||
|
impl Browse {
|
||||||
|
fn tui (&self) -> impl Draw<Tui> {
|
||||||
|
iter_south(1, ||EntriesIterator {
|
||||||
|
offset: 0,
|
||||||
|
index: 0,
|
||||||
|
length: self.dirs.len() + self.files.len(),
|
||||||
|
browser: self,
|
||||||
|
}, |entry, _index|w_full(origin_w(entry)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<'a> Iterator for EntriesIterator<'a> {
|
||||||
|
type Item = Modify<&'a str>;
|
||||||
|
fn next (&mut self) -> Option<Self::Item> {
|
||||||
|
let dirs = self.browser.dirs.len();
|
||||||
|
let files = self.browser.files.len();
|
||||||
|
let index = self.index;
|
||||||
|
if self.index < dirs {
|
||||||
|
self.index += 1;
|
||||||
|
Some(Tui::bold(true, self.browser.dirs[index].1.as_str()))
|
||||||
|
} else if self.index < dirs + files {
|
||||||
|
self.index += 1;
|
||||||
|
Some(Tui::bold(false, self.browser.files[index - dirs].1.as_str()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl PartialEq for BrowseTarget {
|
||||||
|
fn eq (&self, other: &Self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::ImportSample(_) => false,
|
||||||
|
Self::ExportSample(_) => false,
|
||||||
|
Self::ImportClip(_) => false,
|
||||||
|
Self::ExportClip(_) => false,
|
||||||
|
#[allow(unused)] t => matches!(other, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
702
src/clock.rs
Normal file
702
src/clock.rs
Normal file
|
|
@ -0,0 +1,702 @@
|
||||||
|
use ::std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::*}};
|
||||||
|
use ::atomic_float::AtomicF64;
|
||||||
|
use ::tengri::{draw::*, term::*};
|
||||||
|
|
||||||
|
pub trait HasClock: AsRef<Clock> + AsMut<Clock> {
|
||||||
|
fn clock (&self) -> &Clock { self.as_ref() }
|
||||||
|
fn clock_mut (&mut self) -> &mut Clock { self.as_mut() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The source of time.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let clock = tek::Clock::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Clone, Default)] pub struct Clock {
|
||||||
|
/// JACK transport handle.
|
||||||
|
pub transport: Arc<Option<Transport>>,
|
||||||
|
/// Global temporal resolution (shared by [Moment] fields)
|
||||||
|
pub timebase: Arc<Timebase>,
|
||||||
|
/// Current global sample and usec (monotonic from JACK clock)
|
||||||
|
pub global: Arc<Moment>,
|
||||||
|
/// Global sample and usec at which playback started
|
||||||
|
pub started: Arc<RwLock<Option<Moment>>>,
|
||||||
|
/// Playback offset (when playing not from start)
|
||||||
|
pub offset: Arc<Moment>,
|
||||||
|
/// Current playhead position
|
||||||
|
pub playhead: Arc<Moment>,
|
||||||
|
/// Note quantization factor
|
||||||
|
pub quant: Arc<Quantize>,
|
||||||
|
/// Launch quantization factor
|
||||||
|
pub sync: Arc<LaunchSync>,
|
||||||
|
/// Size of buffer in samples
|
||||||
|
pub chunk: Arc<AtomicUsize>,
|
||||||
|
// Cache of formatted strings
|
||||||
|
pub view_cache: Arc<RwLock<ClockView>>,
|
||||||
|
/// For syncing the clock to an external source
|
||||||
|
#[cfg(feature = "port")] pub midi_in: Arc<RwLock<Option<MidiInput>>>,
|
||||||
|
/// For syncing other devices to this clock
|
||||||
|
#[cfg(feature = "port")] pub midi_out: Arc<RwLock<Option<MidiOutput>>>,
|
||||||
|
/// For emitting a metronome
|
||||||
|
#[cfg(feature = "port")] pub click_out: Arc<RwLock<Option<AudioOutput>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Temporal resolutions: sample rate, tempo, MIDI pulses per quaver (beat)
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let _ = tek::Timebase::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone)] pub struct Timebase {
|
||||||
|
/// Audio samples per second
|
||||||
|
pub sr: SampleRate,
|
||||||
|
/// MIDI beats per minute
|
||||||
|
pub bpm: Bpm,
|
||||||
|
/// MIDI ticks per beat
|
||||||
|
pub ppq: Ppq,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterator that emits subsequent ticks within a range.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let iter = tek::Ticker::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Default)] pub struct Ticker {
|
||||||
|
pub spp: f64,
|
||||||
|
pub sample: usize,
|
||||||
|
pub start: usize,
|
||||||
|
pub end: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A point in time in all time scales (microsecond, sample, MIDI pulse)
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let _ = tek::Moment::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Default, Clone)] pub struct Moment {
|
||||||
|
pub timebase: Arc<Timebase>,
|
||||||
|
/// Current time in microseconds
|
||||||
|
pub usec: Microsecond,
|
||||||
|
/// Current time in audio samples
|
||||||
|
pub sample: SampleCount,
|
||||||
|
/// Current time in MIDI pulses
|
||||||
|
pub pulse: Pulse,
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let _ = tek::Moment2::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default)] pub enum Moment2 {
|
||||||
|
#[default] None,
|
||||||
|
Zero,
|
||||||
|
Usec(Microsecond),
|
||||||
|
Sample(SampleCount),
|
||||||
|
Pulse(Pulse),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MIDI resolution in PPQ (pulses per quarter note)
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Default)] pub struct Ppq (pub(crate) AtomicF64);
|
||||||
|
|
||||||
|
/// Timestamp in MIDI pulses
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Default)] pub struct Pulse (pub(crate) AtomicF64);
|
||||||
|
|
||||||
|
/// Tempo in beats per minute
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Default)] pub struct Bpm (pub(crate) AtomicF64);
|
||||||
|
|
||||||
|
/// Quantization setting for launching clips
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Default)] pub struct LaunchSync (pub(crate) AtomicF64);
|
||||||
|
|
||||||
|
/// Quantization setting for notes
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Default)] pub struct Quantize (pub(crate) AtomicF64);
|
||||||
|
|
||||||
|
/// Timestamp in audio samples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Default)] pub struct SampleCount (pub(crate) AtomicF64);
|
||||||
|
|
||||||
|
/// Audio sample rate in Hz (samples per second)
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Default)] pub struct SampleRate (pub(crate) AtomicF64);
|
||||||
|
|
||||||
|
/// Timestamp in microseconds
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Default)] pub struct Microsecond (pub(crate) AtomicF64);
|
||||||
|
|
||||||
|
/// A unit of time, represented as an atomic 64-bit float.
|
||||||
|
///
|
||||||
|
/// According to https://stackoverflow.com/a/873367, as per IEEE754,
|
||||||
|
/// every integer between 1 and 2^53 can be represented exactly.
|
||||||
|
/// This should mean that, even at 192kHz sampling rate, over 1 year of audio
|
||||||
|
/// can be clocked in microseconds with f64 without losing precision.
|
||||||
|
pub trait TimeUnit: InteriorMutable<f64> {}
|
||||||
|
|
||||||
|
/// Contains memoized renders of clock values.
|
||||||
|
///
|
||||||
|
/// Performance optimization.
|
||||||
|
#[derive(Debug)] pub struct ClockView {
|
||||||
|
pub sr: Memo<Option<(bool, f64)>, String>,
|
||||||
|
pub buf: Memo<Option<f64>, String>,
|
||||||
|
pub lat: Memo<Option<f64>, String>,
|
||||||
|
pub bpm: Memo<Option<f64>, String>,
|
||||||
|
pub beat: Memo<Option<f64>, String>,
|
||||||
|
pub time: Memo<Option<f64>, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// FIXME: remove this and use PPQ from timebase everywhere:
|
||||||
|
pub const PPQ: usize = 96;
|
||||||
|
|
||||||
|
/// (pulses, name), assuming 96 PPQ
|
||||||
|
pub const NOTE_DURATIONS: [(usize, &str);26] = [
|
||||||
|
(1, "1/384"), (2, "1/192"),
|
||||||
|
(3, "1/128"), (4, "1/96"),
|
||||||
|
(6, "1/64"), (8, "1/48"),
|
||||||
|
(12, "1/32"), (16, "1/24"),
|
||||||
|
(24, "1/16"), (32, "1/12"),
|
||||||
|
(48, "1/8"), (64, "1/6"),
|
||||||
|
(96, "1/4"), (128, "1/3"),
|
||||||
|
(192, "1/2"), (256, "2/3"),
|
||||||
|
(384, "1/1"), (512, "4/3"),
|
||||||
|
(576, "3/2"), (768, "2/1"),
|
||||||
|
(1152, "3/1"), (1536, "4/1"),
|
||||||
|
(2304, "6/1"), (3072, "8/1"),
|
||||||
|
(3456, "9/1"), (6144, "16/1"),
|
||||||
|
];
|
||||||
|
|
||||||
|
pub const NOTE_NAMES: [&str; 128] = [
|
||||||
|
"C0", "C#0", "D0", "D#0", "E0", "F0", "F#0", "G0", "G#0", "A0", "A#0", "B0",
|
||||||
|
"C1", "C#1", "D1", "D#1", "E1", "F1", "F#1", "G1", "G#1", "A1", "A#1", "B1",
|
||||||
|
"C2", "C#2", "D2", "D#2", "E2", "F2", "F#2", "G2", "G#2", "A2", "A#2", "B2",
|
||||||
|
"C3", "C#3", "D3", "D#3", "E3", "F3", "F#3", "G3", "G#3", "A3", "A#3", "B3",
|
||||||
|
"C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4",
|
||||||
|
"C5", "C#5", "D5", "D#5", "E5", "F5", "F#5", "G5", "G#5", "A5", "A#5", "B5",
|
||||||
|
"C6", "C#6", "D6", "D#6", "E6", "F6", "F#6", "G6", "G#6", "A6", "A#6", "B6",
|
||||||
|
"C7", "C#7", "D7", "D#7", "E7", "F7", "F#7", "G7", "G#7", "A7", "A#7", "B7",
|
||||||
|
"C8", "C#8", "D8", "D#8", "E8", "F8", "F#8", "G8", "G#8", "A8", "A#8", "B8",
|
||||||
|
"C9", "C#9", "D9", "D#9", "E9", "F9", "F#9", "G9", "G#9", "A9", "A#9", "B9",
|
||||||
|
"C10", "C#10", "D10", "D#10", "E10", "F10", "F#10", "G10",
|
||||||
|
];
|
||||||
|
|
||||||
|
pub const DEFAULT_PPQ: f64 = 96.0;
|
||||||
|
|
||||||
|
def_command!(ClockCommand: |clock: Clock| {
|
||||||
|
SeekUsec { usec: f64 } => {
|
||||||
|
clock.playhead.update_from_usec(*usec); Ok(None) },
|
||||||
|
SeekSample { sample: f64 } => {
|
||||||
|
clock.playhead.update_from_sample(*sample); Ok(None) },
|
||||||
|
SeekPulse { pulse: f64 } => {
|
||||||
|
clock.playhead.update_from_pulse(*pulse); Ok(None) },
|
||||||
|
SetBpm { bpm: f64 } => Ok(Some(
|
||||||
|
Self::SetBpm { bpm: clock.timebase().bpm.set(*bpm) })),
|
||||||
|
SetQuant { quant: f64 } => Ok(Some(
|
||||||
|
Self::SetQuant { quant: clock.quant.set(*quant) })),
|
||||||
|
SetSync { sync: f64 } => Ok(Some(
|
||||||
|
Self::SetSync { sync: clock.sync.set(*sync) })),
|
||||||
|
|
||||||
|
Play { position: Option<u32> } => {
|
||||||
|
clock.play_from(*position)?; Ok(None) /* TODO Some(Pause(previousPosition)) */ },
|
||||||
|
Pause { position: Option<u32> } => {
|
||||||
|
clock.pause_at(*position)?; Ok(None) },
|
||||||
|
|
||||||
|
TogglePlayback { position: u32 } => Ok(if clock.is_rolling() {
|
||||||
|
clock.pause_at(Some(*position))?; None
|
||||||
|
} else {
|
||||||
|
clock.play_from(Some(*position))?; None
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
impl Moment {
|
||||||
|
pub fn zero (timebase: &Arc<Timebase>) -> Self {
|
||||||
|
Self { usec: 0.into(), sample: 0.into(), pulse: 0.into(), timebase: timebase.clone() }
|
||||||
|
}
|
||||||
|
pub fn from_usec (timebase: &Arc<Timebase>, usec: f64) -> Self {
|
||||||
|
Self {
|
||||||
|
usec: usec.into(),
|
||||||
|
sample: timebase.sr.usecs_to_sample(usec).into(),
|
||||||
|
pulse: timebase.usecs_to_pulse(usec).into(),
|
||||||
|
timebase: timebase.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn from_sample (timebase: &Arc<Timebase>, sample: f64) -> Self {
|
||||||
|
Self {
|
||||||
|
sample: sample.into(),
|
||||||
|
usec: timebase.sr.samples_to_usec(sample).into(),
|
||||||
|
pulse: timebase.samples_to_pulse(sample).into(),
|
||||||
|
timebase: timebase.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn from_pulse (timebase: &Arc<Timebase>, pulse: f64) -> Self {
|
||||||
|
Self {
|
||||||
|
pulse: pulse.into(),
|
||||||
|
sample: timebase.pulses_to_sample(pulse).into(),
|
||||||
|
usec: timebase.pulses_to_usec(pulse).into(),
|
||||||
|
timebase: timebase.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[inline] pub fn update_from_usec (&self, usec: f64) {
|
||||||
|
self.usec.set(usec);
|
||||||
|
self.pulse.set(self.timebase.usecs_to_pulse(usec));
|
||||||
|
self.sample.set(self.timebase.sr.usecs_to_sample(usec));
|
||||||
|
}
|
||||||
|
#[inline] pub fn update_from_sample (&self, sample: f64) {
|
||||||
|
self.usec.set(self.timebase.sr.samples_to_usec(sample));
|
||||||
|
self.pulse.set(self.timebase.samples_to_pulse(sample));
|
||||||
|
self.sample.set(sample);
|
||||||
|
}
|
||||||
|
#[inline] pub fn update_from_pulse (&self, pulse: f64) {
|
||||||
|
self.usec.set(self.timebase.pulses_to_usec(pulse));
|
||||||
|
self.pulse.set(pulse);
|
||||||
|
self.sample.set(self.timebase.pulses_to_sample(pulse));
|
||||||
|
}
|
||||||
|
#[inline] pub fn format_beat (&self) -> Arc<str> {
|
||||||
|
self.timebase.format_beats_1(self.pulse.get()).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl LaunchSync {
|
||||||
|
pub fn next (&self) -> f64 {
|
||||||
|
note_duration_next(self.get() as usize) as f64
|
||||||
|
}
|
||||||
|
pub fn prev (&self) -> f64 {
|
||||||
|
note_duration_prev(self.get() as usize) as f64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Quantize {
|
||||||
|
pub fn next (&self) -> f64 {
|
||||||
|
note_duration_next(self.get() as usize) as f64
|
||||||
|
}
|
||||||
|
pub fn prev (&self) -> f64 {
|
||||||
|
note_duration_prev(self.get() as usize) as f64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Timebase {
|
||||||
|
/// Specify sample rate, BPM and PPQ
|
||||||
|
pub fn new (
|
||||||
|
s: impl Into<SampleRate>,
|
||||||
|
b: impl Into<Bpm>,
|
||||||
|
p: impl Into<Ppq>
|
||||||
|
) -> Self {
|
||||||
|
Self { sr: s.into(), bpm: b.into(), ppq: p.into() }
|
||||||
|
}
|
||||||
|
/// Iterate over ticks between start and end.
|
||||||
|
#[inline] pub fn pulses_between_samples (&self, start: usize, end: usize) -> Ticker {
|
||||||
|
Ticker { spp: self.samples_per_pulse(), sample: start, start, end }
|
||||||
|
}
|
||||||
|
/// Return the duration fo a beat in microseconds
|
||||||
|
#[inline] pub fn usec_per_beat (&self) -> f64 { 60_000_000f64 / self.bpm.get() }
|
||||||
|
/// Return the number of beats in a second
|
||||||
|
#[inline] pub fn beat_per_second (&self) -> f64 { self.bpm.get() / 60f64 }
|
||||||
|
/// Return the number of microseconds corresponding to a note of the given duration
|
||||||
|
#[inline] pub fn note_to_usec (&self, (num, den): (f64, f64)) -> f64 {
|
||||||
|
4.0 * self.usec_per_beat() * num / den
|
||||||
|
}
|
||||||
|
/// Return duration of a pulse in microseconds (BPM-dependent)
|
||||||
|
#[inline] pub fn pulse_per_usec (&self) -> f64 { self.ppq.get() / self.usec_per_beat() }
|
||||||
|
/// Return duration of a pulse in microseconds (BPM-dependent)
|
||||||
|
#[inline] pub fn usec_per_pulse (&self) -> f64 { self.usec_per_beat() / self.ppq.get() }
|
||||||
|
/// Return number of pulses to which a number of microseconds corresponds (BPM-dependent)
|
||||||
|
#[inline] pub fn usecs_to_pulse (&self, usec: f64) -> f64 { usec * self.pulse_per_usec() }
|
||||||
|
/// Convert a number of pulses to a sample number (SR- and BPM-dependent)
|
||||||
|
#[inline] pub fn pulses_to_usec (&self, pulse: f64) -> f64 { pulse / self.usec_per_pulse() }
|
||||||
|
/// Return number of pulses in a second (BPM-dependent)
|
||||||
|
#[inline] pub fn pulses_per_second (&self) -> f64 { self.beat_per_second() * self.ppq.get() }
|
||||||
|
/// Return fraction of a pulse to which a sample corresponds (SR- and BPM-dependent)
|
||||||
|
#[inline] pub fn pulses_per_sample (&self) -> f64 {
|
||||||
|
self.usec_per_pulse() / self.sr.usec_per_sample()
|
||||||
|
}
|
||||||
|
/// Return number of samples in a pulse (SR- and BPM-dependent)
|
||||||
|
#[inline] pub fn samples_per_pulse (&self) -> f64 {
|
||||||
|
self.sr.get() / self.pulses_per_second()
|
||||||
|
}
|
||||||
|
/// Convert a number of pulses to a sample number (SR- and BPM-dependent)
|
||||||
|
#[inline] pub fn pulses_to_sample (&self, p: f64) -> f64 {
|
||||||
|
self.pulses_per_sample() * p
|
||||||
|
}
|
||||||
|
/// Convert a number of samples to a pulse number (SR- and BPM-dependent)
|
||||||
|
#[inline] pub fn samples_to_pulse (&self, s: f64) -> f64 {
|
||||||
|
s / self.pulses_per_sample()
|
||||||
|
}
|
||||||
|
/// Return the number of samples corresponding to a note of the given duration
|
||||||
|
#[inline] pub fn note_to_samples (&self, note: (f64, f64)) -> f64 {
|
||||||
|
self.usec_to_sample(self.note_to_usec(note))
|
||||||
|
}
|
||||||
|
/// Return the number of samples corresponding to the given number of microseconds
|
||||||
|
#[inline] pub fn usec_to_sample (&self, usec: f64) -> f64 {
|
||||||
|
usec * self.sr.get() / 1000f64
|
||||||
|
}
|
||||||
|
/// Return the quantized position of a moment in time given a step
|
||||||
|
#[inline] pub fn quantize (&self, step: (f64, f64), time: f64) -> (f64, f64) {
|
||||||
|
let step = self.note_to_usec(step);
|
||||||
|
(time / step, time % step)
|
||||||
|
}
|
||||||
|
/// Quantize a collection of events
|
||||||
|
#[inline] pub fn quantize_into <E: Iterator<Item=(f64, f64)> + Sized, T> (
|
||||||
|
&self, step: (f64, f64), events: E
|
||||||
|
) -> Vec<(f64, f64)> {
|
||||||
|
events.map(|(time, event)|(self.quantize(step, time).0, event)).collect()
|
||||||
|
}
|
||||||
|
/// Format a number of pulses into Beat.Bar.Pulse starting from 0
|
||||||
|
#[inline] pub fn format_beats_0 (&self, pulse: f64) -> Arc<str> {
|
||||||
|
let pulse = pulse as usize;
|
||||||
|
let ppq = self.ppq.get() as usize;
|
||||||
|
let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) };
|
||||||
|
format!("{}.{}.{pulses:02}", beats / 4, beats % 4).into()
|
||||||
|
}
|
||||||
|
/// Format a number of pulses into Beat.Bar starting from 0
|
||||||
|
#[inline] pub fn format_beats_0_short (&self, pulse: f64) -> Arc<str> {
|
||||||
|
let pulse = pulse as usize;
|
||||||
|
let ppq = self.ppq.get() as usize;
|
||||||
|
let beats = if ppq > 0 { pulse / ppq } else { 0 };
|
||||||
|
format!("{}.{}", beats / 4, beats % 4).into()
|
||||||
|
}
|
||||||
|
/// Format a number of pulses into Beat.Bar.Pulse starting from 1
|
||||||
|
#[inline] pub fn format_beats_1 (&self, pulse: f64) -> Arc<str> {
|
||||||
|
let mut string = String::with_capacity(16);
|
||||||
|
self.format_beats_1_to(&mut string, pulse).expect("failed to format {pulse} into beat");
|
||||||
|
string.into()
|
||||||
|
}
|
||||||
|
/// Format a number of pulses into Beat.Bar.Pulse starting from 1
|
||||||
|
#[inline] pub fn format_beats_1_to (&self, w: &mut impl std::fmt::Write, pulse: f64) -> Result<(), std::fmt::Error> {
|
||||||
|
let pulse = pulse as usize;
|
||||||
|
let ppq = self.ppq.get() as usize;
|
||||||
|
let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) };
|
||||||
|
write!(w, "{}.{}.{pulses:02}", beats / 4 + 1, beats % 4 + 1)
|
||||||
|
}
|
||||||
|
/// Format a number of pulses into Beat.Bar.Pulse starting from 1
|
||||||
|
#[inline] pub fn format_beats_1_short (&self, pulse: f64) -> Arc<str> {
|
||||||
|
let pulse = pulse as usize;
|
||||||
|
let ppq = self.ppq.get() as usize;
|
||||||
|
let beats = if ppq > 0 { pulse / ppq } else { 0 };
|
||||||
|
format!("{}.{}", beats / 4 + 1, beats % 4 + 1).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl SampleRate {
|
||||||
|
/// Return the duration of a sample in microseconds (floating)
|
||||||
|
#[inline] pub fn usec_per_sample (&self) -> f64 {
|
||||||
|
1_000_000f64 / self.get()
|
||||||
|
}
|
||||||
|
/// Return the duration of a sample in microseconds (floating)
|
||||||
|
#[inline] pub fn sample_per_usec (&self) -> f64 {
|
||||||
|
self.get() / 1_000_000f64
|
||||||
|
}
|
||||||
|
/// Convert a number of samples to microseconds (floating)
|
||||||
|
#[inline] pub fn samples_to_usec (&self, samples: f64) -> f64 {
|
||||||
|
self.usec_per_sample() * samples
|
||||||
|
}
|
||||||
|
/// Convert a number of microseconds to samples (floating)
|
||||||
|
#[inline] pub fn usecs_to_sample (&self, usecs: f64) -> f64 {
|
||||||
|
self.sample_per_usec() * usecs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Microsecond {
|
||||||
|
#[inline] pub fn format_msu (&self) -> Arc<str> {
|
||||||
|
let usecs = self.get() as usize;
|
||||||
|
let (seconds, msecs) = (usecs / 1000000, usecs / 1000 % 1000);
|
||||||
|
let (minutes, seconds) = (seconds / 60, seconds % 60);
|
||||||
|
format!("{minutes}:{seconds:02}:{msecs:03}").into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Define and implement a unit of time
|
||||||
|
#[macro_export] macro_rules! impl_time_unit {
|
||||||
|
($T:ident) => {
|
||||||
|
impl Gettable<f64> for $T {
|
||||||
|
fn get (&self) -> f64 { self.0.load(Relaxed) }
|
||||||
|
}
|
||||||
|
impl InteriorMutable<f64> for $T {
|
||||||
|
fn set (&self, value: f64) -> f64 {
|
||||||
|
let old = self.get();
|
||||||
|
self.0.store(value, Relaxed);
|
||||||
|
old
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl TimeUnit for $T {}
|
||||||
|
impl_op!($T, Add, add, |a, b|{a + b});
|
||||||
|
impl_op!($T, Sub, sub, |a, b|{a - b});
|
||||||
|
impl_op!($T, Mul, mul, |a, b|{a * b});
|
||||||
|
impl_op!($T, Div, div, |a, b|{a / b});
|
||||||
|
impl_op!($T, Rem, rem, |a, b|{a % b});
|
||||||
|
impl From<f64> for $T { fn from (value: f64) -> Self { Self(value.into()) } }
|
||||||
|
impl From<usize> for $T { fn from (value: usize) -> Self { Self((value as f64).into()) } }
|
||||||
|
impl From<$T> for f64 { fn from (value: $T) -> Self { value.get() } }
|
||||||
|
impl From<$T> for usize { fn from (value: $T) -> Self { value.get() as usize } }
|
||||||
|
impl From<&$T> for f64 { fn from (value: &$T) -> Self { value.get() } }
|
||||||
|
impl From<&$T> for usize { fn from (value: &$T) -> Self { value.get() as usize } }
|
||||||
|
impl Clone for $T { fn clone (&self) -> Self { Self(self.get().into()) } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_time_unit!(SampleCount);
|
||||||
|
impl_time_unit!(SampleRate);
|
||||||
|
impl_time_unit!(Microsecond);
|
||||||
|
impl_time_unit!(Quantize);
|
||||||
|
impl_time_unit!(Ppq);
|
||||||
|
impl_time_unit!(Pulse);
|
||||||
|
impl_time_unit!(Bpm);
|
||||||
|
impl_time_unit!(LaunchSync);
|
||||||
|
impl std::fmt::Debug for Clock {
|
||||||
|
fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||||
|
f.debug_struct("Clock")
|
||||||
|
.field("timebase", &self.timebase)
|
||||||
|
.field("chunk", &self.chunk)
|
||||||
|
.field("quant", &self.quant)
|
||||||
|
.field("sync", &self.sync)
|
||||||
|
.field("global", &self.global)
|
||||||
|
.field("playhead", &self.playhead)
|
||||||
|
.field("started", &self.started)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Clock {
|
||||||
|
pub fn new (jack: &Jack<'static>, bpm: Option<f64>) -> Usually<Self> {
|
||||||
|
let (chunk, transport) = jack.with_client(|c|(c.buffer_size(), c.transport()));
|
||||||
|
let timebase = Arc::new(Timebase::default());
|
||||||
|
let clock = Self {
|
||||||
|
quant: Arc::new(24.into()),
|
||||||
|
sync: Arc::new(384.into()),
|
||||||
|
transport: Arc::new(Some(transport)),
|
||||||
|
chunk: Arc::new((chunk as usize).into()),
|
||||||
|
global: Arc::new(Moment::zero(&timebase)),
|
||||||
|
playhead: Arc::new(Moment::zero(&timebase)),
|
||||||
|
offset: Arc::new(Moment::zero(&timebase)),
|
||||||
|
started: RwLock::new(None).into(),
|
||||||
|
timebase,
|
||||||
|
midi_in: Arc::new(RwLock::new(Some(MidiInput::new(jack, &"M/clock", &[])?))),
|
||||||
|
midi_out: Arc::new(RwLock::new(Some(MidiOutput::new(jack, &"clock/M", &[])?))),
|
||||||
|
click_out: Arc::new(RwLock::new(Some(AudioOutput::new(jack, &"click", &[])?))),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
if let Some(bpm) = bpm {
|
||||||
|
clock.timebase.bpm.set(bpm);
|
||||||
|
}
|
||||||
|
Ok(clock)
|
||||||
|
}
|
||||||
|
pub fn timebase (&self) -> &Arc<Timebase> {
|
||||||
|
&self.timebase
|
||||||
|
}
|
||||||
|
/// Current sample rate
|
||||||
|
pub fn sr (&self) -> &SampleRate {
|
||||||
|
&self.timebase.sr
|
||||||
|
}
|
||||||
|
/// Current tempo
|
||||||
|
pub fn bpm (&self) -> &Bpm {
|
||||||
|
&self.timebase.bpm
|
||||||
|
}
|
||||||
|
/// Current MIDI resolution
|
||||||
|
pub fn ppq (&self) -> &Ppq {
|
||||||
|
&self.timebase.ppq
|
||||||
|
}
|
||||||
|
/// Next pulse that matches launch sync (for phrase switchover)
|
||||||
|
pub fn next_launch_pulse (&self) -> usize {
|
||||||
|
let sync = self.sync.get() as usize;
|
||||||
|
let pulse = self.playhead.pulse.get() as usize;
|
||||||
|
if pulse % sync == 0 {
|
||||||
|
pulse
|
||||||
|
} else {
|
||||||
|
(pulse / sync + 1) * sync
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Start playing, optionally seeking to a given location beforehand
|
||||||
|
pub fn play_from (&self, start: Option<u32>) -> Usually<()> {
|
||||||
|
if let Some(transport) = self.transport.as_ref() {
|
||||||
|
if let Some(start) = start {
|
||||||
|
transport.locate(start)?;
|
||||||
|
}
|
||||||
|
transport.start()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
/// Pause, optionally seeking to a given location afterwards
|
||||||
|
pub fn pause_at (&self, pause: Option<u32>) -> Usually<()> {
|
||||||
|
if let Some(transport) = self.transport.as_ref() {
|
||||||
|
transport.stop()?;
|
||||||
|
if let Some(pause) = pause {
|
||||||
|
transport.locate(pause)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
/// Is currently paused?
|
||||||
|
pub fn is_stopped (&self) -> bool {
|
||||||
|
self.started.read().unwrap().is_none()
|
||||||
|
}
|
||||||
|
/// Is currently playing?
|
||||||
|
pub fn is_rolling (&self) -> bool {
|
||||||
|
self.started.read().unwrap().is_some()
|
||||||
|
}
|
||||||
|
/// Update chunk size
|
||||||
|
pub fn set_chunk (&self, n_frames: usize) {
|
||||||
|
self.chunk.store(n_frames, Relaxed);
|
||||||
|
}
|
||||||
|
pub fn update_from_scope (&self, scope: &ProcessScope) -> Usually<()> {
|
||||||
|
// Store buffer length
|
||||||
|
self.set_chunk(scope.n_frames() as usize);
|
||||||
|
|
||||||
|
// Store reported global frame and usec
|
||||||
|
let CycleTimes { current_frames, current_usecs, .. } = scope.cycle_times()?;
|
||||||
|
self.global.sample.set(current_frames as f64);
|
||||||
|
self.global.usec.set(current_usecs as f64);
|
||||||
|
|
||||||
|
let mut started = self.started.write().unwrap();
|
||||||
|
|
||||||
|
// If transport has just started or just stopped,
|
||||||
|
// update starting point:
|
||||||
|
if let Some(transport) = self.transport.as_ref() {
|
||||||
|
match (transport.query_state()?, started.as_ref()) {
|
||||||
|
(TransportState::Rolling, None) => {
|
||||||
|
let moment = Moment::zero(&self.timebase);
|
||||||
|
moment.sample.set(current_frames as f64);
|
||||||
|
moment.usec.set(current_usecs as f64);
|
||||||
|
*started = Some(moment);
|
||||||
|
},
|
||||||
|
(TransportState::Stopped, Some(_)) => {
|
||||||
|
*started = None;
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
self.playhead.update_from_sample(started.as_ref()
|
||||||
|
.map(|started|current_frames as f64 - started.sample.get())
|
||||||
|
.unwrap_or(0.));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bbt (&self) -> PositionBBT {
|
||||||
|
let pulse = self.playhead.pulse.get() as i32;
|
||||||
|
let ppq = self.timebase.ppq.get() as i32;
|
||||||
|
let bpm = self.timebase.bpm.get();
|
||||||
|
let bar = (pulse / ppq) / 4;
|
||||||
|
PositionBBT {
|
||||||
|
bar: 1 + bar,
|
||||||
|
beat: 1 + (pulse / ppq) % 4,
|
||||||
|
tick: (pulse % ppq),
|
||||||
|
bar_start_tick: (bar * 4 * ppq) as f64,
|
||||||
|
beat_type: 4.,
|
||||||
|
beats_per_bar: 4.,
|
||||||
|
beats_per_minute: bpm,
|
||||||
|
ticks_per_beat: ppq as f64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_launch_instant (&self) -> Moment {
|
||||||
|
Moment::from_pulse(self.timebase(), self.next_launch_pulse() as f64)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get index of first sample to populate.
|
||||||
|
///
|
||||||
|
/// Greater than 0 means that the first pulse of the clip
|
||||||
|
/// falls somewhere in the middle of the chunk.
|
||||||
|
pub fn get_sample_offset (&self, scope: &ProcessScope, started: &Moment) -> usize{
|
||||||
|
(scope.last_frame_time() as usize).saturating_sub(
|
||||||
|
started.sample.get() as usize +
|
||||||
|
self.started.read().unwrap().as_ref().unwrap().sample.get() as usize
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get iterator that emits sample paired with pulse.
|
||||||
|
//
|
||||||
|
// * Sample: index into output buffer at which to write MIDI event
|
||||||
|
// * Pulse: index into clip from which to take the MIDI event
|
||||||
|
//
|
||||||
|
// Emitted for each sample of the output buffer that corresponds to a MIDI pulse.
|
||||||
|
pub fn get_pulses (&self, scope: &ProcessScope, offset: usize) -> Ticker {
|
||||||
|
self.timebase().pulses_between_samples(offset, offset + scope.n_frames() as usize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Clock {
|
||||||
|
fn _todo_provide_u32 (&self) -> u32 {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
fn _todo_provide_opt_u32 (&self) -> Option<u32> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
fn _todo_provide_f64 (&self) -> f64 {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T: HasClock> Command<T> for ClockCommand {
|
||||||
|
fn execute (&self, state: &mut T) -> Perhaps<Self> {
|
||||||
|
self.execute(state.clock_mut()) // awesome
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ClockView {
|
||||||
|
pub const BEAT_EMPTY: &'static str = "-.-.--";
|
||||||
|
pub const TIME_EMPTY: &'static str = "-.---s";
|
||||||
|
pub const BPM_EMPTY: &'static str = "---.---";
|
||||||
|
pub 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, "{}", ClockView::BEAT_EMPTY));
|
||||||
|
cache.time.update(None, rewrite!(buf, "{}", ClockView::TIME_EMPTY));
|
||||||
|
cache.bpm.update(None, rewrite!(buf, "{}", ClockView::BPM_EMPTY));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl_default!(ClockView: {
|
||||||
|
let mut beat = String::with_capacity(16);
|
||||||
|
let _ = write!(beat, "{}", Self::BEAT_EMPTY);
|
||||||
|
let mut time = String::with_capacity(16);
|
||||||
|
let _ = write!(time, "{}", Self::TIME_EMPTY);
|
||||||
|
let mut bpm = String::with_capacity(16);
|
||||||
|
let _ = write!(bpm, "{}", Self::BPM_EMPTY);
|
||||||
|
Self {
|
||||||
|
beat: Memo::new(None, beat),
|
||||||
|
time: Memo::new(None, time),
|
||||||
|
bpm: Memo::new(None, bpm),
|
||||||
|
sr: Memo::new(None, String::with_capacity(16)),
|
||||||
|
buf: Memo::new(None, String::with_capacity(16)),
|
||||||
|
lat: Memo::new(None, String::with_capacity(16)),
|
||||||
|
}
|
||||||
|
});
|
||||||
306
src/connect.rs
306
src/connect.rs
|
|
@ -1,306 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
/// Audio input port.
|
|
||||||
#[derive(Debug)] pub struct AudioInput {
|
|
||||||
/// Handle to JACK client, for receiving reconnect events.
|
|
||||||
pub jack: Jack<'static>,
|
|
||||||
/// Port name
|
|
||||||
pub name: Arc<str>,
|
|
||||||
/// Port handle.
|
|
||||||
pub port: Port<AudioIn>,
|
|
||||||
/// List of ports to connect to.
|
|
||||||
pub connections: Vec<Connect>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Audio output port.
|
|
||||||
#[derive(Debug)] pub struct AudioOutput {
|
|
||||||
/// Handle to JACK client, for receiving reconnect events.
|
|
||||||
pub jack: Jack<'static>,
|
|
||||||
/// Port name
|
|
||||||
pub name: Arc<str>,
|
|
||||||
/// Port handle.
|
|
||||||
pub port: Port<AudioOut>,
|
|
||||||
/// List of ports to connect to.
|
|
||||||
pub connections: Vec<Connect>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// MIDI input port.
|
|
||||||
#[derive(Debug)] pub struct MidiInput {
|
|
||||||
/// Handle to JACK client, for receiving reconnect events.
|
|
||||||
pub jack: Jack<'static>,
|
|
||||||
/// Port name
|
|
||||||
pub name: Arc<str>,
|
|
||||||
/// Port handle.
|
|
||||||
pub port: Port<MidiIn>,
|
|
||||||
/// List of currently held notes.
|
|
||||||
pub held: Arc<RwLock<[bool;128]>>,
|
|
||||||
/// List of ports to connect to.
|
|
||||||
pub connections: Vec<Connect>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// MIDI output port.
|
|
||||||
#[derive(Debug)] pub struct MidiOutput {
|
|
||||||
/// Handle to JACK client, for receiving reconnect events.
|
|
||||||
pub jack: Jack<'static>,
|
|
||||||
/// Port name
|
|
||||||
pub name: Arc<str>,
|
|
||||||
/// Port handle.
|
|
||||||
pub port: Port<MidiOut>,
|
|
||||||
/// List of ports to connect to.
|
|
||||||
pub connections: Vec<Connect>,
|
|
||||||
/// List of currently held notes.
|
|
||||||
pub held: Arc<RwLock<[bool;128]>>,
|
|
||||||
/// Buffer
|
|
||||||
pub note_buffer: Vec<u8>,
|
|
||||||
/// Buffer
|
|
||||||
pub output_buffer: Vec<Vec<Vec<u8>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)] pub enum ConnectName {
|
|
||||||
/** Exact match */
|
|
||||||
Exact(Arc<str>),
|
|
||||||
/** Match regular expression */
|
|
||||||
RegExp(Arc<str>),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectScope {
|
|
||||||
One,
|
|
||||||
All
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectStatus {
|
|
||||||
Missing,
|
|
||||||
Disconnected,
|
|
||||||
Connected,
|
|
||||||
Mismatch,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Port connection manager.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let connect = tek::Connect::default();
|
|
||||||
/// ```
|
|
||||||
#[derive(Clone, Debug, Default)] pub struct Connect {
|
|
||||||
pub name: Option<ConnectName>,
|
|
||||||
pub scope: Option<ConnectScope>,
|
|
||||||
pub status: Arc<RwLock<Vec<(Port<Unowned>, Arc<str>, ConnectStatus)>>>,
|
|
||||||
pub info: Arc<str>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Connect {
|
|
||||||
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 = Some(Exact(name.as_ref().into()));
|
|
||||||
Self { name, scope: Some(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 = Some(RegExp(name.as_ref().into()));
|
|
||||||
Self { name, scope: Some(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 = Some(RegExp(name.as_ref().into()));
|
|
||||||
Self { name, scope: Some(All), status: Arc::new(RwLock::new(vec![])), info }
|
|
||||||
}
|
|
||||||
pub fn info (&self) -> Arc<str> {
|
|
||||||
format!(" ({}) {} {}", {
|
|
||||||
let status = self.status.read().unwrap();
|
|
||||||
let mut ok = 0;
|
|
||||||
for (_, _, state) in status.iter() {
|
|
||||||
if *state == Connected {
|
|
||||||
ok += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
format!("{ok}/{}", status.len())
|
|
||||||
}, match self.scope {
|
|
||||||
None => "x",
|
|
||||||
Some(One) => " ",
|
|
||||||
Some(All) => "*",
|
|
||||||
}, match &self.name {
|
|
||||||
None => format!("x"),
|
|
||||||
Some(Exact(name)) => format!("= {name}"),
|
|
||||||
Some(RegExp(name)) => format!("~ {name}"),
|
|
||||||
}).into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HasJack<'static> for MidiInput { fn jack (&self) -> &Jack<'static> { &self.jack } }
|
|
||||||
impl HasJack<'static> for MidiOutput { fn jack (&self) -> &Jack<'static> { &self.jack } }
|
|
||||||
impl HasJack<'static> for AudioInput { fn jack (&self) -> &Jack<'static> { &self.jack } }
|
|
||||||
impl HasJack<'static> for AudioOutput { fn jack (&self) -> &Jack<'static> { &self.jack } }
|
|
||||||
impl<J: HasJack<'static>> RegisterPorts for J {
|
|
||||||
fn midi_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiInput> {
|
|
||||||
MidiInput::new(self.jack(), name, connect)
|
|
||||||
}
|
|
||||||
fn midi_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiOutput> {
|
|
||||||
MidiOutput::new(self.jack(), name, connect)
|
|
||||||
}
|
|
||||||
fn audio_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioInput> {
|
|
||||||
AudioInput::new(self.jack(), name, connect)
|
|
||||||
}
|
|
||||||
fn audio_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioOutput> {
|
|
||||||
AudioOutput::new(self.jack(), name, connect)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// May create new MIDI input ports.
|
|
||||||
pub trait AddMidiIn {
|
|
||||||
fn midi_in_add (&mut self) -> Usually<()>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// May create new MIDI output ports.
|
|
||||||
pub trait AddMidiOut {
|
|
||||||
fn midi_out_add (&mut self) -> Usually<()>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait RegisterPorts: HasJack<'static> {
|
|
||||||
/// Register a MIDI input port.
|
|
||||||
fn midi_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiInput>;
|
|
||||||
/// Register a MIDI output port.
|
|
||||||
fn midi_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiOutput>;
|
|
||||||
/// Register an audio input port.
|
|
||||||
fn audio_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioInput>;
|
|
||||||
/// Register an audio output port.
|
|
||||||
fn audio_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioOutput>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait JackPort: HasJack<'static> {
|
|
||||||
|
|
||||||
type Port: PortSpec + Default;
|
|
||||||
|
|
||||||
type Pair: PortSpec + Default;
|
|
||||||
|
|
||||||
fn new (jack: &Jack<'static>, name: &impl AsRef<str>, connect: &[Connect])
|
|
||||||
-> Usually<Self> where Self: Sized;
|
|
||||||
|
|
||||||
fn register (jack: &Jack<'static>, name: &impl AsRef<str>) -> Usually<Port<Self::Port>> {
|
|
||||||
jack.with_client(|c|c.register_port::<Self::Port>(name.as_ref(), Default::default()))
|
|
||||||
.map_err(|e|e.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn port_name (&self) -> &Arc<str>;
|
|
||||||
|
|
||||||
fn connections (&self) -> &[Connect];
|
|
||||||
|
|
||||||
fn port (&self) -> &Port<Self::Port>;
|
|
||||||
|
|
||||||
fn port_mut (&mut self) -> &mut Port<Self::Port>;
|
|
||||||
|
|
||||||
fn into_port (self) -> Port<Self::Port> where Self: Sized;
|
|
||||||
|
|
||||||
fn close (self) -> Usually<()> where Self: Sized {
|
|
||||||
let jack = self.jack().clone();
|
|
||||||
Ok(jack.with_client(|c|c.unregister_port(self.into_port()))?)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 <'k> (&'k self) -> Usually<()> {
|
|
||||||
for connect in self.connections().iter() {
|
|
||||||
match &connect.name {
|
|
||||||
Some(Exact(name)) => {
|
|
||||||
*connect.status.write().unwrap() = self.connect_exact(name)?;
|
|
||||||
},
|
|
||||||
Some(RegExp(re)) => {
|
|
||||||
*connect.status.write().unwrap() = self.connect_regexp(re, connect.scope)?;
|
|
||||||
},
|
|
||||||
_ => {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn connect_exact <'k> (&'k self, name: &str) ->
|
|
||||||
Usually<Vec<(Port<Unowned>, Arc<str>, ConnectStatus)>>
|
|
||||||
{
|
|
||||||
self.with_client(move|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_unowned(&port)?;
|
|
||||||
let name = port.name()?.into();
|
|
||||||
status.push((port, name, port_status));
|
|
||||||
if port_status == Connected {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(status)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn connect_regexp <'k> (
|
|
||||||
&'k self, re: &str, scope: Option<ConnectScope>
|
|
||||||
) -> Usually<Vec<(Port<Unowned>, Arc<str>, ConnectStatus)>> {
|
|
||||||
self.with_client(move|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_unowned(&port)?;
|
|
||||||
let name = port.name()?.into();
|
|
||||||
status.push((port, name, port_status));
|
|
||||||
if port_status == Connected && scope == Some(One) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(status)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Connect to a matching port by name. */
|
|
||||||
fn connect_to_name (&self, name: impl AsRef<str>) -> Usually<ConnectStatus> {
|
|
||||||
self.with_client(|c|if let Some(ref port) = c.port_by_name(name.as_ref()) {
|
|
||||||
self.connect_to_unowned(port)
|
|
||||||
} else {
|
|
||||||
Ok(Missing)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Connect to a matching port by reference. */
|
|
||||||
fn connect_to_unowned (&self, port: &Port<Unowned>) -> Usually<ConnectStatus> {
|
|
||||||
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
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Connect to an owned matching port by reference. */
|
|
||||||
fn connect_to_owned (&self, port: &Port<Self::Pair>) -> Usually<ConnectStatus> {
|
|
||||||
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
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
583
src/device.rs
583
src/device.rs
|
|
@ -0,0 +1,583 @@
|
||||||
|
|
||||||
|
impl Device {
|
||||||
|
pub fn name (&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::Sampler(sampler) => sampler.name.as_ref(),
|
||||||
|
_ => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn midi_ins (&self) -> &[MidiInput] {
|
||||||
|
match self {
|
||||||
|
//Self::Sampler(Sampler { midi_in, .. }) => &[midi_in],
|
||||||
|
_ => todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn midi_outs (&self) -> &[MidiOutput] {
|
||||||
|
match self {
|
||||||
|
Self::Sampler(_) => &[],
|
||||||
|
_ => todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn audio_ins (&self) -> &[AudioInput] {
|
||||||
|
match self {
|
||||||
|
Self::Sampler(Sampler { audio_ins, .. }) => audio_ins.as_slice(),
|
||||||
|
_ => todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn audio_outs (&self) -> &[AudioOutput] {
|
||||||
|
match self {
|
||||||
|
Self::Sampler(Sampler { audio_outs, .. }) => audio_outs.as_slice(),
|
||||||
|
_ => todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A device that can be plugged into the chain.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let device = tek::Device::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Default)] pub enum Device {
|
||||||
|
#[default]
|
||||||
|
Bypass,
|
||||||
|
Mute,
|
||||||
|
#[cfg(feature = "sampler")]
|
||||||
|
Sampler(Sampler),
|
||||||
|
#[cfg(feature = "lv2")] // TODO
|
||||||
|
Lv2(Lv2),
|
||||||
|
#[cfg(feature = "vst2")] // TODO
|
||||||
|
Vst2,
|
||||||
|
#[cfg(feature = "vst3")] // TODO
|
||||||
|
Vst3,
|
||||||
|
#[cfg(feature = "clap")] // TODO
|
||||||
|
Clap,
|
||||||
|
#[cfg(feature = "sf2")] // TODO
|
||||||
|
Sf2,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Some sort of wrapper?
|
||||||
|
pub struct DeviceAudio<'a>(pub &'a mut Device);
|
||||||
|
|
||||||
|
/// Audio input port.
|
||||||
|
#[derive(Debug)] pub struct AudioInput {
|
||||||
|
/// Handle to JACK client, for receiving reconnect events.
|
||||||
|
pub jack: Jack<'static>,
|
||||||
|
/// Port name
|
||||||
|
pub name: Arc<str>,
|
||||||
|
/// Port handle.
|
||||||
|
pub port: Port<AudioIn>,
|
||||||
|
/// List of ports to connect to.
|
||||||
|
pub connections: Vec<Connect>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Audio output port.
|
||||||
|
#[derive(Debug)] pub struct AudioOutput {
|
||||||
|
/// Handle to JACK client, for receiving reconnect events.
|
||||||
|
pub jack: Jack<'static>,
|
||||||
|
/// Port name
|
||||||
|
pub name: Arc<str>,
|
||||||
|
/// Port handle.
|
||||||
|
pub port: Port<AudioOut>,
|
||||||
|
/// List of ports to connect to.
|
||||||
|
pub connections: Vec<Connect>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MIDI input port.
|
||||||
|
#[derive(Debug)] pub struct MidiInput {
|
||||||
|
/// Handle to JACK client, for receiving reconnect events.
|
||||||
|
pub jack: Jack<'static>,
|
||||||
|
/// Port name
|
||||||
|
pub name: Arc<str>,
|
||||||
|
/// Port handle.
|
||||||
|
pub port: Port<MidiIn>,
|
||||||
|
/// List of currently held notes.
|
||||||
|
pub held: Arc<RwLock<[bool;128]>>,
|
||||||
|
/// List of ports to connect to.
|
||||||
|
pub connections: Vec<Connect>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MIDI output port.
|
||||||
|
#[derive(Debug)] pub struct MidiOutput {
|
||||||
|
/// Handle to JACK client, for receiving reconnect events.
|
||||||
|
pub jack: Jack<'static>,
|
||||||
|
/// Port name
|
||||||
|
pub name: Arc<str>,
|
||||||
|
/// Port handle.
|
||||||
|
pub port: Port<MidiOut>,
|
||||||
|
/// List of ports to connect to.
|
||||||
|
pub connections: Vec<Connect>,
|
||||||
|
/// List of currently held notes.
|
||||||
|
pub held: Arc<RwLock<[bool;128]>>,
|
||||||
|
/// Buffer
|
||||||
|
pub note_buffer: Vec<u8>,
|
||||||
|
/// Buffer
|
||||||
|
pub output_buffer: Vec<Vec<Vec<u8>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)] pub enum ConnectName {
|
||||||
|
/** Exact match */
|
||||||
|
Exact(Arc<str>),
|
||||||
|
/** Match regular expression */
|
||||||
|
RegExp(Arc<str>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectScope {
|
||||||
|
One,
|
||||||
|
All
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectStatus {
|
||||||
|
Missing,
|
||||||
|
Disconnected,
|
||||||
|
Connected,
|
||||||
|
Mismatch,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Port connection manager.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let connect = tek::Connect::default();
|
||||||
|
/// ```
|
||||||
|
#[derive(Clone, Debug, Default)] pub struct Connect {
|
||||||
|
pub name: Option<ConnectName>,
|
||||||
|
pub scope: Option<ConnectScope>,
|
||||||
|
pub status: Arc<RwLock<Vec<(Port<Unowned>, Arc<str>, ConnectStatus)>>>,
|
||||||
|
pub info: Arc<str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Connect {
|
||||||
|
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 = Some(Exact(name.as_ref().into()));
|
||||||
|
Self { name, scope: Some(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 = Some(RegExp(name.as_ref().into()));
|
||||||
|
Self { name, scope: Some(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 = Some(RegExp(name.as_ref().into()));
|
||||||
|
Self { name, scope: Some(All), status: Arc::new(RwLock::new(vec![])), info }
|
||||||
|
}
|
||||||
|
pub fn info (&self) -> Arc<str> {
|
||||||
|
format!(" ({}) {} {}", {
|
||||||
|
let status = self.status.read().unwrap();
|
||||||
|
let mut ok = 0;
|
||||||
|
for (_, _, state) in status.iter() {
|
||||||
|
if *state == Connected {
|
||||||
|
ok += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
format!("{ok}/{}", status.len())
|
||||||
|
}, match self.scope {
|
||||||
|
None => "x",
|
||||||
|
Some(One) => " ",
|
||||||
|
Some(All) => "*",
|
||||||
|
}, match &self.name {
|
||||||
|
None => format!("x"),
|
||||||
|
Some(Exact(name)) => format!("= {name}"),
|
||||||
|
Some(RegExp(name)) => format!("~ {name}"),
|
||||||
|
}).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasJack<'static> for MidiInput { fn jack (&self) -> &Jack<'static> { &self.jack } }
|
||||||
|
impl HasJack<'static> for MidiOutput { fn jack (&self) -> &Jack<'static> { &self.jack } }
|
||||||
|
impl HasJack<'static> for AudioInput { fn jack (&self) -> &Jack<'static> { &self.jack } }
|
||||||
|
impl HasJack<'static> for AudioOutput { fn jack (&self) -> &Jack<'static> { &self.jack } }
|
||||||
|
impl<J: HasJack<'static>> RegisterPorts for J {
|
||||||
|
fn midi_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiInput> {
|
||||||
|
MidiInput::new(self.jack(), name, connect)
|
||||||
|
}
|
||||||
|
fn midi_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiOutput> {
|
||||||
|
MidiOutput::new(self.jack(), name, connect)
|
||||||
|
}
|
||||||
|
fn audio_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioInput> {
|
||||||
|
AudioInput::new(self.jack(), name, connect)
|
||||||
|
}
|
||||||
|
fn audio_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioOutput> {
|
||||||
|
AudioOutput::new(self.jack(), name, connect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// May create new MIDI input ports.
|
||||||
|
pub trait AddMidiIn {
|
||||||
|
fn midi_in_add (&mut self) -> Usually<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// May create new MIDI output ports.
|
||||||
|
pub trait AddMidiOut {
|
||||||
|
fn midi_out_add (&mut self) -> Usually<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait RegisterPorts: HasJack<'static> {
|
||||||
|
/// Register a MIDI input port.
|
||||||
|
fn midi_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiInput>;
|
||||||
|
/// Register a MIDI output port.
|
||||||
|
fn midi_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiOutput>;
|
||||||
|
/// Register an audio input port.
|
||||||
|
fn audio_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioInput>;
|
||||||
|
/// Register an audio output port.
|
||||||
|
fn audio_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioOutput>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait JackPort: HasJack<'static> {
|
||||||
|
|
||||||
|
type Port: PortSpec + Default;
|
||||||
|
|
||||||
|
type Pair: PortSpec + Default;
|
||||||
|
|
||||||
|
fn new (jack: &Jack<'static>, name: &impl AsRef<str>, connect: &[Connect])
|
||||||
|
-> Usually<Self> where Self: Sized;
|
||||||
|
|
||||||
|
fn register (jack: &Jack<'static>, name: &impl AsRef<str>) -> Usually<Port<Self::Port>> {
|
||||||
|
jack.with_client(|c|c.register_port::<Self::Port>(name.as_ref(), Default::default()))
|
||||||
|
.map_err(|e|e.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn port_name (&self) -> &Arc<str>;
|
||||||
|
|
||||||
|
fn connections (&self) -> &[Connect];
|
||||||
|
|
||||||
|
fn port (&self) -> &Port<Self::Port>;
|
||||||
|
|
||||||
|
fn port_mut (&mut self) -> &mut Port<Self::Port>;
|
||||||
|
|
||||||
|
fn into_port (self) -> Port<Self::Port> where Self: Sized;
|
||||||
|
|
||||||
|
fn close (self) -> Usually<()> where Self: Sized {
|
||||||
|
let jack = self.jack().clone();
|
||||||
|
Ok(jack.with_client(|c|c.unregister_port(self.into_port()))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 <'k> (&'k self) -> Usually<()> {
|
||||||
|
for connect in self.connections().iter() {
|
||||||
|
match &connect.name {
|
||||||
|
Some(Exact(name)) => {
|
||||||
|
*connect.status.write().unwrap() = self.connect_exact(name)?;
|
||||||
|
},
|
||||||
|
Some(RegExp(re)) => {
|
||||||
|
*connect.status.write().unwrap() = self.connect_regexp(re, connect.scope)?;
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn connect_exact <'k> (&'k self, name: &str) ->
|
||||||
|
Usually<Vec<(Port<Unowned>, Arc<str>, ConnectStatus)>>
|
||||||
|
{
|
||||||
|
self.with_client(move|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_unowned(&port)?;
|
||||||
|
let name = port.name()?.into();
|
||||||
|
status.push((port, name, port_status));
|
||||||
|
if port_status == Connected {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(status)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn connect_regexp <'k> (
|
||||||
|
&'k self, re: &str, scope: Option<ConnectScope>
|
||||||
|
) -> Usually<Vec<(Port<Unowned>, Arc<str>, ConnectStatus)>> {
|
||||||
|
self.with_client(move|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_unowned(&port)?;
|
||||||
|
let name = port.name()?.into();
|
||||||
|
status.push((port, name, port_status));
|
||||||
|
if port_status == Connected && scope == Some(One) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(status)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Connect to a matching port by name. */
|
||||||
|
fn connect_to_name (&self, name: impl AsRef<str>) -> Usually<ConnectStatus> {
|
||||||
|
self.with_client(|c|if let Some(ref port) = c.port_by_name(name.as_ref()) {
|
||||||
|
self.connect_to_unowned(port)
|
||||||
|
} else {
|
||||||
|
Ok(Missing)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Connect to a matching port by reference. */
|
||||||
|
fn connect_to_unowned (&self, port: &Port<Unowned>) -> Usually<ConnectStatus> {
|
||||||
|
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
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Connect to an owned matching port by reference. */
|
||||||
|
fn connect_to_owned (&self, port: &Port<Self::Pair>) -> Usually<ConnectStatus> {
|
||||||
|
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 JackPort for MidiInput {
|
||||||
|
type Port = MidiIn;
|
||||||
|
type Pair = MidiOut;
|
||||||
|
fn port_name (&self) -> &Arc<str> {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
fn port (&self) -> &Port<Self::Port> {
|
||||||
|
&self.port
|
||||||
|
}
|
||||||
|
fn port_mut (&mut self) -> &mut Port<Self::Port> {
|
||||||
|
&mut self.port
|
||||||
|
}
|
||||||
|
fn into_port (self) -> Port<Self::Port> {
|
||||||
|
self.port
|
||||||
|
}
|
||||||
|
fn connections (&self) -> &[Connect] {
|
||||||
|
self.connections.as_slice()
|
||||||
|
}
|
||||||
|
fn new (jack: &Jack<'static>, name: &impl AsRef<str>, connect: &[Connect])
|
||||||
|
-> Usually<Self> where Self: Sized
|
||||||
|
{
|
||||||
|
let port = Self {
|
||||||
|
port: Self::register(jack, name)?,
|
||||||
|
jack: jack.clone(),
|
||||||
|
name: name.as_ref().into(),
|
||||||
|
connections: connect.to_vec(),
|
||||||
|
held: Arc::new(RwLock::new([false;128]))
|
||||||
|
};
|
||||||
|
port.connect_to_matching()?;
|
||||||
|
Ok(port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JackPort for MidiOutput {
|
||||||
|
type Port = MidiOut;
|
||||||
|
type Pair = MidiIn;
|
||||||
|
fn port_name (&self) -> &Arc<str> {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
fn port (&self) -> &Port<Self::Port> {
|
||||||
|
&self.port
|
||||||
|
}
|
||||||
|
fn port_mut (&mut self) -> &mut Port<Self::Port> {
|
||||||
|
&mut self.port
|
||||||
|
}
|
||||||
|
fn into_port (self) -> Port<Self::Port> {
|
||||||
|
self.port
|
||||||
|
}
|
||||||
|
fn connections (&self) -> &[Connect] {
|
||||||
|
self.connections.as_slice()
|
||||||
|
}
|
||||||
|
fn new (jack: &Jack<'static>, name: &impl AsRef<str>, connect: &[Connect])
|
||||||
|
-> Usually<Self> where Self: Sized
|
||||||
|
{
|
||||||
|
let port = Self::register(jack, name)?;
|
||||||
|
let jack = jack.clone();
|
||||||
|
let name = name.as_ref().into();
|
||||||
|
let connections = connect.to_vec();
|
||||||
|
let port = Self {
|
||||||
|
jack,
|
||||||
|
port,
|
||||||
|
name,
|
||||||
|
connections,
|
||||||
|
held: Arc::new([false;128].into()),
|
||||||
|
note_buffer: vec![0;8],
|
||||||
|
output_buffer: vec![vec![];65536],
|
||||||
|
};
|
||||||
|
port.connect_to_matching()?;
|
||||||
|
Ok(port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MidiOutput {
|
||||||
|
/// Clear the section of the output buffer that we will be using,
|
||||||
|
/// emitting "all notes off" at start of buffer if requested.
|
||||||
|
pub fn buffer_clear (&mut self, scope: &ProcessScope, reset: bool) {
|
||||||
|
let n_frames = (scope.n_frames() as usize).min(self.output_buffer.len());
|
||||||
|
for frame in &mut self.output_buffer[0..n_frames] {
|
||||||
|
frame.clear();
|
||||||
|
}
|
||||||
|
if reset {
|
||||||
|
all_notes_off(&mut self.output_buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Write a note to the output buffer
|
||||||
|
pub fn buffer_write <'a> (
|
||||||
|
&'a mut self,
|
||||||
|
sample: usize,
|
||||||
|
event: LiveEvent,
|
||||||
|
) {
|
||||||
|
self.note_buffer.fill(0);
|
||||||
|
event.write(&mut self.note_buffer).expect("failed to serialize MIDI event");
|
||||||
|
self.output_buffer[sample].push(self.note_buffer.clone());
|
||||||
|
// Update the list of currently held notes.
|
||||||
|
if let LiveEvent::Midi { ref message, .. } = event {
|
||||||
|
update_keys(&mut*self.held.write().unwrap(), message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Write a chunk of MIDI data from the output buffer to the output port.
|
||||||
|
pub fn buffer_emit (&mut self, scope: &ProcessScope) {
|
||||||
|
let samples = scope.n_frames() as usize;
|
||||||
|
let mut writer = self.port.writer(scope);
|
||||||
|
for (time, events) in self.output_buffer.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:?}");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl MidiInput {
|
||||||
|
pub fn parsed <'a> (&'a self, scope: &'a ProcessScope) -> impl Iterator<Item=(usize, LiveEvent<'a>, &'a [u8])> {
|
||||||
|
parse_midi_input(self.port().iter(scope))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T: AsRef<Vec<MidiInput>> + AsMut<Vec<MidiInput>>> HasMidiIns for T {
|
||||||
|
fn midi_ins (&self) -> &Vec<MidiInput> { self.as_ref() }
|
||||||
|
fn midi_ins_mut (&mut self) -> &mut Vec<MidiInput> { self.as_mut() }
|
||||||
|
}
|
||||||
|
impl<T: AsRef<Vec<MidiOutput>> + AsMut<Vec<MidiOutput>>> HasMidiOuts for T {
|
||||||
|
fn midi_outs (&self) -> &Vec<MidiOutput> { self.as_ref() }
|
||||||
|
fn midi_outs_mut (&mut self) -> &mut Vec<MidiOutput> { self.as_mut() }
|
||||||
|
}
|
||||||
|
impl<T: HasMidiIns + HasJack<'static>> AddMidiIn for T {
|
||||||
|
fn midi_in_add (&mut self) -> Usually<()> {
|
||||||
|
let index = self.midi_ins().len();
|
||||||
|
let port = MidiInput::new(self.jack(), &format!("M/{index}"), &[])?;
|
||||||
|
self.midi_ins_mut().push(port);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Trail for thing that may gain new MIDI ports.
|
||||||
|
impl<T: HasMidiOuts + HasJack<'static>> AddMidiOut for T {
|
||||||
|
fn midi_out_add (&mut self) -> Usually<()> {
|
||||||
|
let index = self.midi_outs().len();
|
||||||
|
let port = MidiOutput::new(self.jack(), &format!("{index}/M"), &[])?;
|
||||||
|
self.midi_outs_mut().push(port);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JackPort for AudioInput {
|
||||||
|
type Port = AudioIn;
|
||||||
|
type Pair = AudioOut;
|
||||||
|
fn port_name (&self) -> &Arc<str> {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
fn port (&self) -> &Port<Self::Port> {
|
||||||
|
&self.port
|
||||||
|
}
|
||||||
|
fn port_mut (&mut self) -> &mut Port<Self::Port> {
|
||||||
|
&mut self.port
|
||||||
|
}
|
||||||
|
fn into_port (self) -> Port<Self::Port> {
|
||||||
|
self.port
|
||||||
|
}
|
||||||
|
fn connections (&self) -> &[Connect] {
|
||||||
|
self.connections.as_slice()
|
||||||
|
}
|
||||||
|
fn new (jack: &Jack<'static>, name: &impl AsRef<str>, connect: &[Connect])
|
||||||
|
-> Usually<Self> where Self: Sized
|
||||||
|
{
|
||||||
|
let port = Self {
|
||||||
|
port: Self::register(jack, name)?,
|
||||||
|
jack: jack.clone(),
|
||||||
|
name: name.as_ref().into(),
|
||||||
|
connections: connect.to_vec()
|
||||||
|
};
|
||||||
|
port.connect_to_matching()?;
|
||||||
|
Ok(port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JackPort for AudioOutput {
|
||||||
|
type Port = AudioOut;
|
||||||
|
type Pair = AudioIn;
|
||||||
|
fn port_name (&self) -> &Arc<str> {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
fn port (&self) -> &Port<Self::Port> {
|
||||||
|
&self.port
|
||||||
|
}
|
||||||
|
fn port_mut (&mut self) -> &mut Port<Self::Port> {
|
||||||
|
&mut self.port
|
||||||
|
}
|
||||||
|
fn into_port (self) -> Port<Self::Port> {
|
||||||
|
self.port
|
||||||
|
}
|
||||||
|
fn connections (&self) -> &[Connect] {
|
||||||
|
self.connections.as_slice()
|
||||||
|
}
|
||||||
|
fn new (jack: &Jack<'static>, name: &impl AsRef<str>, connect: &[Connect])
|
||||||
|
-> Usually<Self> where Self: Sized
|
||||||
|
{
|
||||||
|
let port = Self {
|
||||||
|
port: Self::register(jack, name)?,
|
||||||
|
jack: jack.clone(),
|
||||||
|
name: name.as_ref().into(),
|
||||||
|
connections: connect.to_vec()
|
||||||
|
};
|
||||||
|
port.connect_to_matching()?;
|
||||||
|
Ok(port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_audio!(|self: DeviceAudio<'a>, client, scope|{
|
||||||
|
use Device::*;
|
||||||
|
match self.0 {
|
||||||
|
Mute => { Control::Continue },
|
||||||
|
Bypass => { /*TODO*/ Control::Continue },
|
||||||
|
#[cfg(feature = "sampler")] Sampler(sampler) => sampler.process(client, scope),
|
||||||
|
#[cfg(feature = "lv2")] Lv2(lv2) => lv2.process(client, scope),
|
||||||
|
#[cfg(feature = "vst2")] Vst2 => { todo!() }, // TODO
|
||||||
|
#[cfg(feature = "vst3")] Vst3 => { todo!() }, // TODO
|
||||||
|
#[cfg(feature = "clap")] Clap => { todo!() }, // TODO
|
||||||
|
#[cfg(feature = "sf2")] Sf2 => { todo!() }, // TODO
|
||||||
|
}
|
||||||
|
});
|
||||||
56
src/mix.rs
56
src/mix.rs
|
|
@ -12,3 +12,59 @@
|
||||||
Average,
|
Average,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(test)] mod test_view_meter {
|
||||||
|
use super::*;
|
||||||
|
use proptest::prelude::*;
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Draw<Tui> for RmsMeter {
|
||||||
|
fn draw(self, to: &mut Tui) -> Usually<XYWH<u16>> {
|
||||||
|
let XYWH(x, y, w, h) = to.area();
|
||||||
|
let signal = f32::max(0.0, f32::min(100.0, self.0.abs()));
|
||||||
|
let v = (signal * h as f32).ceil() as u16;
|
||||||
|
let y2 = y + h;
|
||||||
|
//to.blit(&format!("\r{v} {} {signal}", self.0), x * 30, y, Some(Style::default()));
|
||||||
|
for y in y..(y + v) {
|
||||||
|
for x in x..(x + w) {
|
||||||
|
to.blit(&"▌", x, y2.saturating_sub(y), Some(Style::default().green()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Draw<Tui> for Log10Meter {
|
||||||
|
fn draw(self, to: &mut Tui) -> Usually<XYWH<u16>> {
|
||||||
|
let XYWH(x, y, w, h) = to.area();
|
||||||
|
let signal = 100.0 - f32::max(0.0, f32::min(100.0, self.0.abs()));
|
||||||
|
let v = (signal * h as f32 / 100.0).ceil() as u16;
|
||||||
|
let y2 = y + h;
|
||||||
|
//to.blit(&format!("\r{v} {} {signal}", self.0), x * 20, y, None);
|
||||||
|
for y in y..(y + v) {
|
||||||
|
for x in x..(x + w) {
|
||||||
|
to.blit(&"▌", x, y2 - y, Some(Style::default().green()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_meters (meters: &[f32]) -> impl Draw<Tui> + use<'_> {
|
||||||
|
Tui::bg(Black, w_exact(2, iter_east(1, ||meters.iter(), |value, _index|{
|
||||||
|
h_full(RmsMeter(*value))
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
|
||||||
150
src/plugin.rs
150
src/plugin.rs
|
|
@ -26,3 +26,153 @@
|
||||||
pub window: Option<Window>
|
pub window: Option<Window>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl_audio!(Lv2: lv2_jack_process);
|
||||||
|
|
||||||
|
impl Lv2 {
|
||||||
|
const INPUT_BUFFER: usize = 1024;
|
||||||
|
pub fn new (
|
||||||
|
jack: &Jack<'static>,
|
||||||
|
name: &str,
|
||||||
|
uri: &str,
|
||||||
|
) -> Usually<Self> {
|
||||||
|
let lv2_world = livi::World::with_load_bundle(&uri);
|
||||||
|
let lv2_features = lv2_world.build_features(livi::FeaturesBuilder {
|
||||||
|
min_block_length: 1,
|
||||||
|
max_block_length: 65536,
|
||||||
|
});
|
||||||
|
let lv2_plugin = lv2_world.iter_plugins().nth(0)
|
||||||
|
.unwrap_or_else(||panic!("plugin not found: {uri}"));
|
||||||
|
Ok(Self {
|
||||||
|
jack: jack.clone(),
|
||||||
|
name: name.into(),
|
||||||
|
path: Some(String::from(uri).into()),
|
||||||
|
selected: 0,
|
||||||
|
mapping: false,
|
||||||
|
midi_ins: vec![],
|
||||||
|
midi_outs: vec![],
|
||||||
|
audio_ins: vec![],
|
||||||
|
audio_outs: vec![],
|
||||||
|
lv2_instance: unsafe {
|
||||||
|
lv2_plugin
|
||||||
|
.instantiate(lv2_features.clone(), 48000.0)
|
||||||
|
.expect(&format!("instantiate failed: {uri}"))
|
||||||
|
},
|
||||||
|
lv2_port_list: lv2_plugin.ports().collect::<Vec<_>>(),
|
||||||
|
lv2_input_buffer: Vec::with_capacity(Self::INPUT_BUFFER),
|
||||||
|
lv2_ui_thread: None,
|
||||||
|
lv2_world,
|
||||||
|
lv2_features,
|
||||||
|
lv2_plugin,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn lv2_jack_process (
|
||||||
|
Lv2 {
|
||||||
|
midi_ins, midi_outs, audio_ins, audio_outs,
|
||||||
|
lv2_features, lv2_instance, lv2_input_buffer, ..
|
||||||
|
}: &mut Lv2,
|
||||||
|
_client: &Client,
|
||||||
|
scope: &ProcessScope
|
||||||
|
) -> Control {
|
||||||
|
let urid = lv2_features.midi_urid();
|
||||||
|
lv2_input_buffer.clear();
|
||||||
|
for port in midi_ins.iter() {
|
||||||
|
let mut atom = ::livi::event::LV2AtomSequence::new(
|
||||||
|
&lv2_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(),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lv2_input_buffer.push(atom);
|
||||||
|
}
|
||||||
|
let mut outputs = vec![];
|
||||||
|
for _ in midi_outs.iter() {
|
||||||
|
outputs.push(::livi::event::LV2AtomSequence::new(
|
||||||
|
lv2_features,
|
||||||
|
scope.n_frames() as usize
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let ports = ::livi::EmptyPortConnections::new()
|
||||||
|
.with_atom_sequence_inputs(lv2_input_buffer.iter())
|
||||||
|
.with_atom_sequence_outputs(outputs.iter_mut())
|
||||||
|
.with_audio_inputs(audio_ins.iter().map(|o|o.as_slice(scope)))
|
||||||
|
.with_audio_outputs(audio_outs.iter_mut().map(|o|o.as_mut_slice(scope)));
|
||||||
|
unsafe {
|
||||||
|
lv2_instance.run(scope.n_frames() as usize, ports).unwrap()
|
||||||
|
};
|
||||||
|
Control::Continue
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Draw<Tui> for Lv2 {
|
||||||
|
fn draw(self, to: &mut Tui) {
|
||||||
|
let area = to.area();
|
||||||
|
let XYWH(x, y, _, height) = area;
|
||||||
|
let mut width = 20u16;
|
||||||
|
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) = self.lv2_port_list.get(i) {
|
||||||
|
let value = if let Some(value) = self.lv2_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: &Lv2, to: &mut Tui, x: u16, y: u16, w: u16) {
|
||||||
|
let style = Style::default().gray();
|
||||||
|
let label1 = format!(" {}", state.name);
|
||||||
|
to.blit(&label1, x + 1, y, Some(style.white().bold()));
|
||||||
|
if let Some(ref path) = state.path {
|
||||||
|
let label2 = format!("{}…", &path[..((w as usize - 10).min(path.len()))]);
|
||||||
|
to.blit(&label2, x + 2 + label1.len() as u16, y, Some(style.not_dim()));
|
||||||
|
}
|
||||||
|
//Ok(Rect { x, y, width: w, height: 1 })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "vst2")] impl<E: Engine> ::vst::host::Host for Plugin<E> {}
|
||||||
|
|
|
||||||
549
src/sample.rs
549
src/sample.rs
|
|
@ -1,3 +1,6 @@
|
||||||
|
use ::std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::*}};
|
||||||
|
use crate::device::{MidiInput, MidiOutput, AudioInput, AudioOutput};
|
||||||
|
|
||||||
/// Plays [Voice]s from [Sample]s.
|
/// Plays [Voice]s from [Sample]s.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
|
|
@ -102,3 +105,549 @@
|
||||||
|
|
||||||
pub type MidiSample =
|
pub type MidiSample =
|
||||||
(Option<u7>, Arc<RwLock<crate::Sample>>);
|
(Option<u7>, Arc<RwLock<crate::Sample>>);
|
||||||
|
|
||||||
|
impl<const N: usize> Default for SampleKit<N> {
|
||||||
|
fn default () -> Self { Self([const { None }; N]) }
|
||||||
|
}
|
||||||
|
impl Iterator for Voice {
|
||||||
|
type Item = [f32;2];
|
||||||
|
fn next (&mut self) -> Option<Self::Item> {
|
||||||
|
if self.after > 0 {
|
||||||
|
self.after -= 1;
|
||||||
|
return Some([0.0, 0.0])
|
||||||
|
}
|
||||||
|
let sample = self.sample.read().unwrap();
|
||||||
|
if self.position < sample.end {
|
||||||
|
let position = self.position;
|
||||||
|
self.position += 1;
|
||||||
|
return sample.channels[0].get(position).map(|_amplitude|[
|
||||||
|
sample.channels[0][position] * self.velocity * sample.gain,
|
||||||
|
sample.channels[0][position] * self.velocity * sample.gain,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl NoteRange for Sampler {
|
||||||
|
fn note_lo (&self) -> &AtomicUsize {
|
||||||
|
&self.note_lo
|
||||||
|
}
|
||||||
|
fn note_axis (&self) -> &AtomicUsize {
|
||||||
|
&self.size.y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl NotePoint for Sampler {
|
||||||
|
fn note_len (&self) -> &AtomicUsize {
|
||||||
|
unreachable!();
|
||||||
|
}
|
||||||
|
fn get_note_len (&self) -> usize {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
fn set_note_len (&self, _x: usize) -> usize {
|
||||||
|
0 /*TODO?*/
|
||||||
|
}
|
||||||
|
fn note_pos (&self) -> &AtomicUsize {
|
||||||
|
&self.note_pt
|
||||||
|
}
|
||||||
|
fn get_note_pos (&self) -> usize {
|
||||||
|
self.note_pt.load(Relaxed)
|
||||||
|
}
|
||||||
|
fn set_note_pos (&self, x: usize) -> usize {
|
||||||
|
let old = self.note_pt.swap(x, Relaxed);
|
||||||
|
self.cursor.0.store(x % 8, Relaxed);
|
||||||
|
self.cursor.1.store(x / 8, Relaxed);
|
||||||
|
old
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Sampler {
|
||||||
|
pub fn new (
|
||||||
|
jack: &Jack<'static>,
|
||||||
|
name: impl AsRef<str>,
|
||||||
|
#[cfg(feature = "port")] midi_from: &[Connect],
|
||||||
|
#[cfg(feature = "port")] audio_from: &[&[Connect];2],
|
||||||
|
#[cfg(feature = "port")] audio_to: &[&[Connect];2],
|
||||||
|
) -> Usually<Self> {
|
||||||
|
let name = name.as_ref();
|
||||||
|
Ok(Self {
|
||||||
|
name: name.into(),
|
||||||
|
input_meters: vec![0.0;2],
|
||||||
|
output_meters: vec![0.0;2],
|
||||||
|
output_gain: 1.,
|
||||||
|
buffer: vec![vec![0.0;16384];2],
|
||||||
|
#[cfg(feature = "port")] midi_in: Some(
|
||||||
|
MidiInput::new(jack, &format!("M/{name}"), midi_from)?
|
||||||
|
),
|
||||||
|
#[cfg(feature = "port")] audio_ins: vec![
|
||||||
|
AudioInput::new(jack, &format!("L/{name}"), audio_from[0])?,
|
||||||
|
AudioInput::new(jack, &format!("R/{name}"), audio_from[1])?,
|
||||||
|
],
|
||||||
|
#[cfg(feature = "port")] audio_outs: vec![
|
||||||
|
AudioOutput::new(jack, &format!("{name}/L"), audio_to[0])?,
|
||||||
|
AudioOutput::new(jack, &format!("{name}/R"), audio_to[1])?,
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/// Value of cursor
|
||||||
|
pub fn cursor (&self) -> (usize, usize) {
|
||||||
|
(self.cursor.0.load(Relaxed), self.cursor.1.load(Relaxed))
|
||||||
|
}
|
||||||
|
fn sample_selected (&self) -> usize {
|
||||||
|
(self.get_note_pos() as u8).into()
|
||||||
|
}
|
||||||
|
fn sample_selected_pitch (&self) -> u7 {
|
||||||
|
(self.get_note_pos() as u8).into()
|
||||||
|
}
|
||||||
|
pub fn process_audio_in (&mut self, scope: &ProcessScope) {
|
||||||
|
self.reset_input_meters();
|
||||||
|
if self.recording.is_some() {
|
||||||
|
self.record_into(scope);
|
||||||
|
} else {
|
||||||
|
self.update_input_meters(scope);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Make sure that input meter count corresponds to input channel count
|
||||||
|
fn reset_input_meters (&mut self) {
|
||||||
|
let channels = self.audio_ins.len();
|
||||||
|
if self.input_meters.len() != channels {
|
||||||
|
self.input_meters = vec![f32::MIN;channels];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Record from inputs to sample
|
||||||
|
fn record_into (&mut self, scope: &ProcessScope) {
|
||||||
|
if let Some(ref sample) = self.recording.as_ref().expect("no recording sample").1 {
|
||||||
|
let mut sample = sample.write().unwrap();
|
||||||
|
if sample.channels.len() != self.audio_ins.len() {
|
||||||
|
panic!("channel count mismatch");
|
||||||
|
}
|
||||||
|
let samples_with_meters = self.audio_ins.iter()
|
||||||
|
.zip(self.input_meters.iter_mut())
|
||||||
|
.zip(sample.channels.iter_mut());
|
||||||
|
let mut length = 0;
|
||||||
|
for ((input, meter), channel) in samples_with_meters {
|
||||||
|
let slice = input.port().as_slice(scope);
|
||||||
|
length = length.max(slice.len());
|
||||||
|
*meter = to_rms(slice);
|
||||||
|
channel.extend_from_slice(slice);
|
||||||
|
}
|
||||||
|
sample.end += length;
|
||||||
|
} else {
|
||||||
|
panic!("tried to record into the void")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Update input meters
|
||||||
|
fn update_input_meters (&mut self, scope: &ProcessScope) {
|
||||||
|
for (input, meter) in self.audio_ins.iter().zip(self.input_meters.iter_mut()) {
|
||||||
|
let slice = input.port().as_slice(scope);
|
||||||
|
*meter = to_rms(slice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Make sure that output meter count corresponds to input channel count
|
||||||
|
fn reset_output_meters (&mut self) {
|
||||||
|
let channels = self.audio_outs.len();
|
||||||
|
if self.output_meters.len() != channels {
|
||||||
|
self.output_meters = vec![f32::MIN;channels];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Mix all currently playing samples into the output.
|
||||||
|
pub fn process_audio_out (&mut self, scope: &ProcessScope) {
|
||||||
|
self.clear_output_buffer();
|
||||||
|
self.populate_output_buffer(scope.n_frames() as usize);
|
||||||
|
self.write_output_buffer(scope);
|
||||||
|
}
|
||||||
|
/// Zero the output buffer.
|
||||||
|
fn clear_output_buffer (&mut self) {
|
||||||
|
for buffer in self.buffer.iter_mut() {
|
||||||
|
buffer.fill(0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Write playing voices to output buffer
|
||||||
|
fn populate_output_buffer (&mut self, frames: usize) {
|
||||||
|
let Sampler { buffer, voices, output_gain, mixing_mode, .. } = self;
|
||||||
|
let _channel_count = buffer.len();
|
||||||
|
match mixing_mode {
|
||||||
|
MixingMode::Summing => voices.write().unwrap().retain_mut(|voice|{
|
||||||
|
mix_summing(buffer.as_mut_slice(), *output_gain, frames, ||voice.next())
|
||||||
|
}),
|
||||||
|
MixingMode::Average => voices.write().unwrap().retain_mut(|voice|{
|
||||||
|
mix_average(buffer.as_mut_slice(), *output_gain, frames, ||voice.next())
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Write output buffer to output ports.
|
||||||
|
fn write_output_buffer (&mut self, scope: &ProcessScope) {
|
||||||
|
let Sampler { audio_outs, buffer, .. } = self;
|
||||||
|
for (i, port) in audio_outs.iter_mut().enumerate() {
|
||||||
|
let buffer = &buffer[i];
|
||||||
|
for (i, value) in port.port_mut().as_mut_slice(scope).iter_mut().enumerate() {
|
||||||
|
*value = *buffer.get(i).unwrap_or(&0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl SampleAdd {
|
||||||
|
fn exited (&self) -> bool {
|
||||||
|
self.exited
|
||||||
|
}
|
||||||
|
fn exit (&mut self) {
|
||||||
|
self.exited = true
|
||||||
|
}
|
||||||
|
pub fn new (
|
||||||
|
sample: &Arc<RwLock<Sample>>,
|
||||||
|
voices: &Arc<RwLock<Vec<Voice>>>
|
||||||
|
) -> Usually<Self> {
|
||||||
|
let dir = std::env::current_dir()?;
|
||||||
|
let (subdirs, files) = scan(&dir)?;
|
||||||
|
Ok(Self {
|
||||||
|
exited: false,
|
||||||
|
dir,
|
||||||
|
subdirs,
|
||||||
|
files,
|
||||||
|
cursor: 0,
|
||||||
|
offset: 0,
|
||||||
|
sample: sample.clone(),
|
||||||
|
voices: voices.clone(),
|
||||||
|
_search: None
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn rescan (&mut self) -> Usually<()> {
|
||||||
|
scan(&self.dir).map(|(subdirs, files)|{
|
||||||
|
self.subdirs = subdirs;
|
||||||
|
self.files = files;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn prev (&mut self) {
|
||||||
|
self.cursor = self.cursor.saturating_sub(1);
|
||||||
|
}
|
||||||
|
fn next (&mut self) {
|
||||||
|
self.cursor = self.cursor + 1;
|
||||||
|
}
|
||||||
|
fn try_preview (&mut self) -> Usually<()> {
|
||||||
|
if let Some(path) = self.cursor_file() {
|
||||||
|
if let Ok(sample) = Sample::from_file(&path) {
|
||||||
|
*self.sample.write().unwrap() = sample;
|
||||||
|
self.voices.write().unwrap().push(
|
||||||
|
Sample::play(&self.sample, 0, &u7::from(100u8))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
//load_sample(&path)?;
|
||||||
|
//let src = std::fs::File::open(&path)?;
|
||||||
|
//let mss = MediaSourceStream::new(Box::new(src), Default::default());
|
||||||
|
//let mut hint = Hint::new();
|
||||||
|
//if let Some(ext) = path.extension() {
|
||||||
|
//hint.with_extension(&ext.to_string_lossy());
|
||||||
|
//}
|
||||||
|
//let meta_opts: MetadataOptions = Default::default();
|
||||||
|
//let fmt_opts: FormatOptions = Default::default();
|
||||||
|
//if let Ok(mut probed) = symphonia::default::get_probe()
|
||||||
|
//.format(&hint, mss, &fmt_opts, &meta_opts)
|
||||||
|
//{
|
||||||
|
//panic!("{:?}", probed.format.metadata());
|
||||||
|
//};
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn cursor_dir (&self) -> Option<PathBuf> {
|
||||||
|
if self.cursor < self.subdirs.len() {
|
||||||
|
Some(self.dir.join(&self.subdirs[self.cursor]))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn cursor_file (&self) -> Option<PathBuf> {
|
||||||
|
if self.cursor < self.subdirs.len() {
|
||||||
|
return None
|
||||||
|
}
|
||||||
|
let index = self.cursor.saturating_sub(self.subdirs.len());
|
||||||
|
if index < self.files.len() {
|
||||||
|
Some(self.dir.join(&self.files[index]))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn pick (&mut self) -> Usually<bool> {
|
||||||
|
if self.cursor == 0 {
|
||||||
|
if let Some(parent) = self.dir.parent() {
|
||||||
|
self.dir = parent.into();
|
||||||
|
self.rescan()?;
|
||||||
|
self.cursor = 0;
|
||||||
|
return Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(dir) = self.cursor_dir() {
|
||||||
|
self.dir = dir;
|
||||||
|
self.rescan()?;
|
||||||
|
self.cursor = 0;
|
||||||
|
return Ok(false)
|
||||||
|
}
|
||||||
|
if let Some(path) = self.cursor_file() {
|
||||||
|
let (end, channels) = read_sample_data(&path.to_string_lossy())?;
|
||||||
|
let mut sample = self.sample.write().unwrap();
|
||||||
|
sample.name = path.file_name().unwrap().to_string_lossy().into();
|
||||||
|
sample.end = end;
|
||||||
|
sample.channels = channels;
|
||||||
|
return Ok(true)
|
||||||
|
}
|
||||||
|
return Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<const N: usize> SampleKit<N> {
|
||||||
|
pub fn get (&self, index: usize) -> &Option<Arc<RwLock<Sample>>> {
|
||||||
|
if index < self.0.len() {
|
||||||
|
&self.0[index]
|
||||||
|
} else {
|
||||||
|
&None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Sample {
|
||||||
|
pub fn new (name: impl AsRef<str>, start: usize, end: usize, channels: Vec<Vec<f32>>) -> Self {
|
||||||
|
Self {
|
||||||
|
name: name.as_ref().into(),
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
channels,
|
||||||
|
rate: None,
|
||||||
|
gain: 1.0,
|
||||||
|
color: ItemTheme::random(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn play (sample: &Arc<RwLock<Self>>, after: usize, velocity: &u7) -> Voice {
|
||||||
|
Voice {
|
||||||
|
sample: sample.clone(),
|
||||||
|
after,
|
||||||
|
position: sample.read().unwrap().start,
|
||||||
|
velocity: velocity.as_int() as f32 / 127.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn handle_cc (&mut self, controller: u7, value: u7) {
|
||||||
|
let percentage = value.as_int() as f64 / 127.;
|
||||||
|
match controller.as_int() {
|
||||||
|
20 => {
|
||||||
|
self.start = (percentage * self.end as f64) as usize;
|
||||||
|
},
|
||||||
|
21 => {
|
||||||
|
let length = self.channels[0].len();
|
||||||
|
self.end = length.min(
|
||||||
|
self.start + (percentage * (length as f64 - self.start as f64)) as usize
|
||||||
|
);
|
||||||
|
},
|
||||||
|
22 => { /*attack*/ },
|
||||||
|
23 => { /*decay*/ },
|
||||||
|
24 => {
|
||||||
|
self.gain = percentage as f32 * 2.0;
|
||||||
|
},
|
||||||
|
26 => { /* pan */ }
|
||||||
|
25 => { /* pitch */ }
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Read WAV from file
|
||||||
|
pub fn read_data (src: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
|
||||||
|
let mut channels: Vec<wavers::Samples<f32>> = vec![];
|
||||||
|
for channel in wavers::Wav::from_path(src)?.channels() {
|
||||||
|
channels.push(channel);
|
||||||
|
}
|
||||||
|
let mut end = 0;
|
||||||
|
let mut data: Vec<Vec<f32>> = vec![];
|
||||||
|
for samples in channels.iter() {
|
||||||
|
let channel = Vec::from(samples.as_ref());
|
||||||
|
end = end.max(channel.len());
|
||||||
|
data.push(channel);
|
||||||
|
}
|
||||||
|
Ok((end, data))
|
||||||
|
}
|
||||||
|
pub fn from_file (path: &PathBuf) -> Usually<Self> {
|
||||||
|
let name = path.file_name().unwrap().to_string_lossy().into();
|
||||||
|
let mut sample = Self { name, ..Default::default() };
|
||||||
|
// Use file extension if present
|
||||||
|
let mut hint = Hint::new();
|
||||||
|
if let Some(ext) = path.extension() {
|
||||||
|
hint.with_extension(&ext.to_string_lossy());
|
||||||
|
}
|
||||||
|
let probed = symphonia::default::get_probe().format(
|
||||||
|
&hint,
|
||||||
|
MediaSourceStream::new(
|
||||||
|
Box::new(File::open(path)?),
|
||||||
|
Default::default(),
|
||||||
|
),
|
||||||
|
&Default::default(),
|
||||||
|
&Default::default()
|
||||||
|
)?;
|
||||||
|
let mut format = probed.format;
|
||||||
|
let params = &format.tracks().iter()
|
||||||
|
.find(|t| t.codec_params.codec != CODEC_TYPE_NULL)
|
||||||
|
.expect("no tracks found")
|
||||||
|
.codec_params;
|
||||||
|
let mut decoder = get_codecs().make(params, &Default::default())?;
|
||||||
|
loop {
|
||||||
|
match format.next_packet() {
|
||||||
|
Ok(packet) => sample.decode_packet(&mut decoder, packet)?,
|
||||||
|
Err(symphonia::core::errors::Error::IoError(_)) => break decoder.last_decoded(),
|
||||||
|
Err(err) => return Err(err.into()),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
sample.end = sample.channels.iter().fold(0, |l, c|l + c.len());
|
||||||
|
Ok(sample)
|
||||||
|
}
|
||||||
|
fn decode_packet (
|
||||||
|
&mut self, decoder: &mut Box<dyn Decoder>, packet: Packet
|
||||||
|
) -> Usually<()> {
|
||||||
|
// Decode a packet
|
||||||
|
let decoded = decoder
|
||||||
|
.decode(&packet)
|
||||||
|
.map_err(|e|Box::<dyn std::error::Error>::from(e))?;
|
||||||
|
// Determine sample rate
|
||||||
|
let spec = *decoded.spec();
|
||||||
|
if let Some(rate) = self.rate {
|
||||||
|
if rate != spec.rate as usize {
|
||||||
|
panic!("sample rate changed");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.rate = Some(spec.rate as usize);
|
||||||
|
}
|
||||||
|
// Determine channel count
|
||||||
|
while self.channels.len() < spec.channels.count() {
|
||||||
|
self.channels.push(vec![]);
|
||||||
|
}
|
||||||
|
// Load sample
|
||||||
|
let mut samples = SampleBuffer::new(
|
||||||
|
decoded.frames() as u64,
|
||||||
|
spec
|
||||||
|
);
|
||||||
|
if samples.capacity() > 0 {
|
||||||
|
samples.copy_interleaved_ref(decoded);
|
||||||
|
for frame in samples.samples().chunks(spec.channels.count()) {
|
||||||
|
for (chan, frame) in frame.iter().enumerate() {
|
||||||
|
self.channels[chan].push(*frame)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Draw<Tui> for SampleAdd {
|
||||||
|
fn draw (self, _to: &mut Tui) -> Usually<XYWH<u16>> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_list_item (sample: &Option<Arc<RwLock<Sample>>>) -> String {
|
||||||
|
if let Some(sample) = sample {
|
||||||
|
let sample = sample.read().unwrap();
|
||||||
|
format!("{:8}", sample.name)
|
||||||
|
//format!("{:8} {:3} {:6}-{:6}/{:6}",
|
||||||
|
//sample.name,
|
||||||
|
//sample.gain,
|
||||||
|
//sample.start,
|
||||||
|
//sample.end,
|
||||||
|
//sample.channels[0].len()
|
||||||
|
//)
|
||||||
|
} else {
|
||||||
|
String::from("........")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_viewer (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> + use<'_> {
|
||||||
|
let min_db = -64.0;
|
||||||
|
Thunk::new(move|to: &mut Tui|{
|
||||||
|
let XYWH(x, y, width, height) = to.area();
|
||||||
|
let area = Rect { x, y, width, height };
|
||||||
|
if let Some(sample) = &sample {
|
||||||
|
let sample = sample.read().unwrap();
|
||||||
|
let start = sample.start as f64;
|
||||||
|
let end = sample.end as f64;
|
||||||
|
let length = end - start;
|
||||||
|
let step = length / width as f64;
|
||||||
|
let mut t = start;
|
||||||
|
let mut lines = vec![];
|
||||||
|
while t < end {
|
||||||
|
let chunk = &sample.channels[0][t as usize..((t + step) as usize).min(sample.end)];
|
||||||
|
let total: f32 = chunk.iter().map(|x|x.abs()).sum();
|
||||||
|
let count = chunk.len() as f32;
|
||||||
|
let meter = 10. * (total / count).log10();
|
||||||
|
let x = t as f64;
|
||||||
|
let y = meter as f64;
|
||||||
|
lines.push(Line::new(x, min_db, x, y, Color::Green));
|
||||||
|
t += step / 2.;
|
||||||
|
}
|
||||||
|
Canvas::default()
|
||||||
|
.x_bounds([sample.start as f64, sample.end as f64])
|
||||||
|
.y_bounds([min_db, 0.])
|
||||||
|
.paint(|ctx| {
|
||||||
|
for line in lines.iter() {
|
||||||
|
ctx.draw(line);
|
||||||
|
}
|
||||||
|
//FIXME: proportions
|
||||||
|
//let text = "press record to finish sampling";
|
||||||
|
//ctx.print(
|
||||||
|
//(width - text.len() as u16) as f64 / 2.0,
|
||||||
|
//height as f64 / 2.0,
|
||||||
|
//text.red()
|
||||||
|
//);
|
||||||
|
}).render(area, &mut to.buffer);
|
||||||
|
} else {
|
||||||
|
Canvas::default()
|
||||||
|
.x_bounds([0.0, width as f64])
|
||||||
|
.y_bounds([0.0, height as f64])
|
||||||
|
.paint(|_ctx| {
|
||||||
|
//let text = "press record to begin sampling";
|
||||||
|
//ctx.print(
|
||||||
|
//(width - text.len() as u16) as f64 / 2.0,
|
||||||
|
//height as f64 / 2.0,
|
||||||
|
//text.red()
|
||||||
|
//);
|
||||||
|
})
|
||||||
|
.render(area, &mut to.buffer);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_audio!(Sampler: sampler_jack_process);
|
||||||
|
pub(crate) fn sampler_jack_process (state: &mut Sampler, _: &Client, scope: &ProcessScope) -> Control {
|
||||||
|
if let Some(midi_in) = &state.midi_in {
|
||||||
|
for midi in midi_in.port().iter(scope) {
|
||||||
|
sampler_midi_in(&state.samples, &state.voices, midi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.process_audio_out(scope);
|
||||||
|
state.process_audio_in(scope);
|
||||||
|
Control::Continue
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create [Voice]s from [Sample]s in response to MIDI input.
|
||||||
|
fn sampler_midi_in (
|
||||||
|
samples: &SampleKit<128>, voices: &Arc<RwLock<Vec<Voice>>>, RawMidi { time, bytes }: RawMidi
|
||||||
|
) {
|
||||||
|
if let Ok(LiveEvent::Midi { message, .. }) = LiveEvent::parse(bytes) {
|
||||||
|
match message {
|
||||||
|
MidiMessage::NoteOn { ref key, ref vel } => {
|
||||||
|
if let Some(sample) = samples.get(key.as_int() as usize) {
|
||||||
|
voices.write().unwrap().push(Sample::play(sample, time as usize, vel));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MidiMessage::Controller { controller: _, value: _ } => {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_sample (
|
||||||
|
to: &mut Tui, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool
|
||||||
|
) -> Usually<usize> {
|
||||||
|
let style = if focus { Style::default().green() } else { Style::default() };
|
||||||
|
if focus {
|
||||||
|
to.blit(&"🬴", x+1, y, Some(style.bold()));
|
||||||
|
}
|
||||||
|
let label1 = format!("{:3} {:12}",
|
||||||
|
note.map(|n|n.to_string()).unwrap_or(String::default()),
|
||||||
|
sample.name);
|
||||||
|
let label2 = format!("{:>6} {:>6} +0.0",
|
||||||
|
sample.start,
|
||||||
|
sample.end);
|
||||||
|
to.blit(&label1, x+2, y, Some(style.bold()));
|
||||||
|
to.blit(&label2, x+3+label1.len()as u16, y, Some(style));
|
||||||
|
Ok(label1.len() + label2.len() + 4)
|
||||||
|
}
|
||||||
|
|
|
||||||
820
src/sequence.rs
820
src/sequence.rs
|
|
@ -1,3 +1,4 @@
|
||||||
|
use ::std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
/// Contains state for viewing and editing a clip.
|
/// Contains state for viewing and editing a clip.
|
||||||
///
|
///
|
||||||
|
|
@ -594,3 +595,822 @@ pub trait MidiRange: TimeRange + NoteRange {}
|
||||||
// Lowest note displayed
|
// Lowest note displayed
|
||||||
pub note_lo: Arc<AtomicUsize>,
|
pub note_lo: Arc<AtomicUsize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl MidiClip {
|
||||||
|
pub fn new (
|
||||||
|
name: impl AsRef<str>,
|
||||||
|
looped: bool,
|
||||||
|
length: usize,
|
||||||
|
notes: Option<MidiData>,
|
||||||
|
color: Option<ItemTheme>,
|
||||||
|
) -> 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(ItemTheme::random)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn count_midi_messages (&self) -> usize {
|
||||||
|
let mut count = 0;
|
||||||
|
for tick in self.notes.iter() {
|
||||||
|
count += tick.len();
|
||||||
|
}
|
||||||
|
count
|
||||||
|
}
|
||||||
|
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_tui(Color::Rgb(32, 32, 32)).into())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for MidiClip {
|
||||||
|
fn eq (&self, other: &Self) -> bool {
|
||||||
|
self.uuid == other.uuid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for MidiClip {}
|
||||||
|
|
||||||
|
impl MidiClip {
|
||||||
|
fn _todo_opt_bool_stub_ (&self) -> Option<bool> { todo!() }
|
||||||
|
fn _todo_bool_stub_ (&self) -> bool { todo!() }
|
||||||
|
fn _todo_usize_stub_ (&self) -> usize { todo!() }
|
||||||
|
fn _todo_arc_str_stub_ (&self) -> Arc<str> { todo!() }
|
||||||
|
fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() }
|
||||||
|
fn _todo_opt_item_theme_stub (&self) -> Option<ItemTheme> { todo!() }
|
||||||
|
}
|
||||||
|
impl_has!(Sequencer: |self: Track| self.sequencer);
|
||||||
|
impl_has!(Clock: |self: Sequencer| self.clock);
|
||||||
|
impl_has!(Vec<MidiInput>: |self: Sequencer| self.midi_ins);
|
||||||
|
impl_has!(Vec<MidiOutput>: |self: Sequencer| self.midi_outs);
|
||||||
|
impl_has!(Measure<Tui>: |self: MidiEditor| self.size);
|
||||||
|
impl_has!(Measure<Tui>: |self: PianoHorizontal| self.size);
|
||||||
|
impl_default!(Sequencer: Self {
|
||||||
|
clock: Clock::default(),
|
||||||
|
play_clip: None,
|
||||||
|
next_clip: None,
|
||||||
|
midi_ins: vec![],
|
||||||
|
midi_outs: vec![],
|
||||||
|
recording: false,
|
||||||
|
monitoring: true,
|
||||||
|
overdub: false,
|
||||||
|
notes_in: RwLock::new([false;128]).into(),
|
||||||
|
notes_out: RwLock::new([false;128]).into(),
|
||||||
|
note_buf: vec![0;8],
|
||||||
|
midi_buf: vec![],
|
||||||
|
reset: true,
|
||||||
|
});
|
||||||
|
impl Sequencer {
|
||||||
|
pub fn new (
|
||||||
|
name: impl AsRef<str>,
|
||||||
|
jack: &Jack<'static>,
|
||||||
|
#[cfg(feature = "clock")] clock: Option<&Clock>,
|
||||||
|
#[cfg(feature = "clip")] clip: Option<&Arc<RwLock<MidiClip>>>,
|
||||||
|
#[cfg(feature = "port")] midi_from: &[Connect],
|
||||||
|
#[cfg(feature = "port")] midi_to: &[Connect],
|
||||||
|
) -> Usually<Self> {
|
||||||
|
let _name = name.as_ref();
|
||||||
|
#[cfg(feature = "clock")] let clock = clock.cloned().unwrap_or_default();
|
||||||
|
Ok(Self {
|
||||||
|
reset: true,
|
||||||
|
notes_in: RwLock::new([false;128]).into(),
|
||||||
|
notes_out: RwLock::new([false;128]).into(),
|
||||||
|
#[cfg(feature = "port")] midi_ins: vec![MidiInput::new(jack, &format!("M/{}", name.as_ref()), midi_from)?,],
|
||||||
|
#[cfg(feature = "port")] midi_outs: vec![MidiOutput::new(jack, &format!("{}/M", name.as_ref()), midi_to)?, ],
|
||||||
|
#[cfg(feature = "clip")] play_clip: clip.map(|clip|(Moment::zero(&clock.timebase), Some(clip.clone()))),
|
||||||
|
#[cfg(feature = "clock")] clock,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn process_rolling (&mut self, scope: &ProcessScope) -> Control {
|
||||||
|
self.process_clear(scope, false);
|
||||||
|
// Write chunk of clip to output, handle switchover
|
||||||
|
if self.process_playback(scope) {
|
||||||
|
self.process_switchover(scope);
|
||||||
|
}
|
||||||
|
// Monitor input to output
|
||||||
|
self.process_monitoring(scope);
|
||||||
|
// Record and/or monitor input
|
||||||
|
self.process_recording(scope);
|
||||||
|
// Emit contents of MIDI buffers to JACK MIDI output ports.
|
||||||
|
self.midi_outs_emit(scope);
|
||||||
|
Control::Continue
|
||||||
|
}
|
||||||
|
fn process_stopped (&mut self, scope: &ProcessScope) -> Control {
|
||||||
|
if self.monitoring() && self.midi_ins().len() > 0 && self.midi_outs().len() > 0 {
|
||||||
|
self.process_monitoring(scope)
|
||||||
|
}
|
||||||
|
Control::Continue
|
||||||
|
}
|
||||||
|
fn process_monitoring (&mut self, scope: &ProcessScope) {
|
||||||
|
let notes_in = self.notes_in().clone(); // For highlighting keys and note repeat
|
||||||
|
let monitoring = self.monitoring();
|
||||||
|
for input in self.midi_ins.iter() {
|
||||||
|
for (sample, event, bytes) in input.parsed(scope) {
|
||||||
|
if let LiveEvent::Midi { message, .. } = event {
|
||||||
|
if monitoring {
|
||||||
|
self.midi_buf[sample].push(bytes.to_vec());
|
||||||
|
}
|
||||||
|
// FIXME: don't lock on every event!
|
||||||
|
update_keys(&mut notes_in.write().unwrap(), &message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Clear the section of the output buffer that we will be using,
|
||||||
|
/// emitting "all notes off" at start of buffer if requested.
|
||||||
|
fn process_clear (&mut self, scope: &ProcessScope, reset: bool) {
|
||||||
|
let n_frames = (scope.n_frames() as usize).min(self.midi_buf_mut().len());
|
||||||
|
for frame in &mut self.midi_buf_mut()[0..n_frames] {
|
||||||
|
frame.clear();
|
||||||
|
}
|
||||||
|
if reset {
|
||||||
|
all_notes_off(self.midi_buf_mut());
|
||||||
|
}
|
||||||
|
for port in self.midi_outs_mut().iter_mut() {
|
||||||
|
// Clear output buffer(s)
|
||||||
|
port.buffer_clear(scope, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn process_recording (&mut self, scope: &ProcessScope) {
|
||||||
|
if self.monitoring() {
|
||||||
|
self.monitor(scope);
|
||||||
|
}
|
||||||
|
if let Some((started, ref clip)) = self.play_clip.clone() {
|
||||||
|
self.record_clip(scope, started, clip);
|
||||||
|
}
|
||||||
|
if let Some((_start_at, _clip)) = &self.next_clip() {
|
||||||
|
self.record_next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn process_playback (&mut self, scope: &ProcessScope) -> bool {
|
||||||
|
// 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.
|
||||||
|
if let Some((started, clip)) = &self.play_clip {
|
||||||
|
// Length of clip, to repeat or stop on end.
|
||||||
|
let length = clip.as_ref().map_or(0, |p|p.read().unwrap().length);
|
||||||
|
// Index of first sample to populate.
|
||||||
|
let offset = self.clock().get_sample_offset(scope, &started);
|
||||||
|
// Write MIDI events from clip at sample offsets corresponding to pulses.
|
||||||
|
for (sample, pulse) in self.clock().get_pulses(scope, offset) {
|
||||||
|
// 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 };
|
||||||
|
// Is it time for switchover?
|
||||||
|
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(clip) = clip {
|
||||||
|
// 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() {
|
||||||
|
for port in self.midi_outs.iter_mut() {
|
||||||
|
port.buffer_write(sample, LiveEvent::Midi {
|
||||||
|
channel: 0.into(), /* TODO */
|
||||||
|
message: *message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Handle switchover from current to next playing clip.
|
||||||
|
fn process_switchover (&mut self, scope: &ProcessScope) {
|
||||||
|
let _midi_buf = self.midi_buf_mut();
|
||||||
|
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.process_playback(scope);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl HasMidiBuffers for Sequencer {
|
||||||
|
fn note_buf_mut (&mut self) -> &mut Vec<u8> { &mut self.note_buf }
|
||||||
|
fn midi_buf_mut (&mut self) -> &mut Vec<Vec<Vec<u8>>> { &mut self.midi_buf }
|
||||||
|
}
|
||||||
|
impl std::fmt::Debug for Sequencer {
|
||||||
|
fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||||
|
f.debug_struct("Sequencer")
|
||||||
|
.field("clock", &self.clock)
|
||||||
|
.field("play_clip", &self.play_clip)
|
||||||
|
.field("next_clip", &self.next_clip)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl MidiMonitor for Sequencer {
|
||||||
|
fn monitoring (&self) -> bool { self.monitoring }
|
||||||
|
fn monitoring_mut (&mut self) -> &mut bool { &mut self.monitoring }
|
||||||
|
fn notes_in (&self) -> &Arc<RwLock<[bool; 128]>> { &self.notes_in }
|
||||||
|
}
|
||||||
|
impl MidiRecord for Sequencer {
|
||||||
|
fn recording (&self) -> bool { self.recording }
|
||||||
|
fn recording_mut (&mut self) -> &mut bool { &mut self.recording }
|
||||||
|
fn overdub (&self) -> bool { self.overdub }
|
||||||
|
fn overdub_mut (&mut self) -> &mut bool { &mut self.overdub }
|
||||||
|
}
|
||||||
|
#[cfg(feature="clip")] impl HasPlayClip for Sequencer {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// JACK process callback for a sequencer's clip sequencer/recorder.
|
||||||
|
impl Audio for Sequencer {
|
||||||
|
fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
|
||||||
|
if self.clock().is_rolling() {
|
||||||
|
self.process_rolling(scope)
|
||||||
|
} else {
|
||||||
|
self.process_stopped(scope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Draw<Tui> for MidiEditor {
|
||||||
|
fn draw(self, to: &mut Tui) -> Usually<XYWH<u16>> {
|
||||||
|
self.tui().draw(to)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Draw<Tui> for PianoHorizontal {
|
||||||
|
fn draw(self, to: &mut Tui) -> Usually<XYWH<u16>> {
|
||||||
|
self.tui().draw(to)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::fmt::Debug for MidiEditor {
|
||||||
|
fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||||
|
f.debug_struct("MidiEditor").field("mode", &self.mode).finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl_from!(MidiEditor: |clip: &Arc<RwLock<MidiClip>>| {
|
||||||
|
let model = Self::from(Some(clip.clone()));
|
||||||
|
model.redraw();
|
||||||
|
model
|
||||||
|
});
|
||||||
|
impl_from!(MidiEditor: |clip: Option<Arc<RwLock<MidiClip>>>| {
|
||||||
|
let mut model = Self::default();
|
||||||
|
*model.clip_mut() = clip;
|
||||||
|
model.redraw();
|
||||||
|
model
|
||||||
|
});
|
||||||
|
impl_default!(MidiEditor: Self {
|
||||||
|
size: Measure::new(0, 0), mode: PianoHorizontal::new(None)
|
||||||
|
});
|
||||||
|
impl_default!(OctaveVertical: Self {
|
||||||
|
on: [false; 12], colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)]
|
||||||
|
});
|
||||||
|
impl MidiEditor {
|
||||||
|
/// Put note at current position
|
||||||
|
pub 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.get_time_pos();
|
||||||
|
let note_pos = self.get_note_pos();
|
||||||
|
let note_len = self.get_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 + 1) % clip.length);
|
||||||
|
}
|
||||||
|
redraw = true;
|
||||||
|
}
|
||||||
|
if redraw {
|
||||||
|
self.mode.redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn _todo_opt_clip_stub (&self) -> Option<Arc<RwLock<MidiClip>>> { todo!() }
|
||||||
|
fn clip_length (&self) -> usize { self.clip().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1) }
|
||||||
|
fn note_length (&self) -> usize { self.get_note_len() }
|
||||||
|
fn note_pos (&self) -> usize { self.get_note_pos() }
|
||||||
|
fn note_pos_next (&self) -> usize { self.get_note_pos() + 1 }
|
||||||
|
fn note_pos_next_octave (&self) -> usize { self.get_note_pos() + 12 }
|
||||||
|
fn note_pos_prev (&self) -> usize { self.get_note_pos().saturating_sub(1) }
|
||||||
|
fn note_pos_prev_octave (&self) -> usize { self.get_note_pos().saturating_sub(12) }
|
||||||
|
fn note_len (&self) -> usize { self.get_note_len() }
|
||||||
|
fn note_len_next (&self) -> usize { self.get_note_len() + 1 }
|
||||||
|
fn note_len_prev (&self) -> usize { self.get_note_len().saturating_sub(1) }
|
||||||
|
fn note_range (&self) -> usize { self.get_note_axis() }
|
||||||
|
fn note_range_next (&self) -> usize { self.get_note_axis() + 1 }
|
||||||
|
fn note_range_prev (&self) -> usize { self.get_note_axis().saturating_sub(1) }
|
||||||
|
fn time_zoom (&self) -> usize { self.get_time_zoom() }
|
||||||
|
fn time_zoom_next (&self) -> usize { self.get_time_zoom() + 1 }
|
||||||
|
fn time_zoom_next_fine (&self) -> usize { self.get_time_zoom() + 1 }
|
||||||
|
fn time_zoom_prev (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) }
|
||||||
|
fn time_zoom_prev_fine (&self) -> usize { self.get_time_zoom().saturating_sub(1).max(1) }
|
||||||
|
fn time_lock (&self) -> bool { self.get_time_lock() }
|
||||||
|
fn time_lock_toggled (&self) -> bool { !self.get_time_lock() }
|
||||||
|
fn time_pos (&self) -> usize { self.get_time_pos() }
|
||||||
|
fn time_pos_next (&self) -> usize { (self.get_time_pos() + self.get_note_len()) % self.clip_length() }
|
||||||
|
fn time_pos_next_fine (&self) -> usize { (self.get_time_pos() + 1) % self.clip_length() }
|
||||||
|
fn time_pos_prev (&self) -> usize {
|
||||||
|
let step = self.get_note_len();
|
||||||
|
self.get_time_pos().overflowing_sub(step)
|
||||||
|
.0.min(self.clip_length().saturating_sub(step))
|
||||||
|
}
|
||||||
|
fn time_pos_prev_fine (&self) -> usize {
|
||||||
|
self.get_time_pos().overflowing_sub(1)
|
||||||
|
.0.min(self.clip_length().saturating_sub(1))
|
||||||
|
}
|
||||||
|
pub fn clip_status (&self) -> impl Draw<Tui> + '_ {
|
||||||
|
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 { (ItemTheme::G[64], String::new().into(), 0, false) };
|
||||||
|
w_exact(20, south!(
|
||||||
|
w_full(origin_w(east(
|
||||||
|
button_2("f2", "name ", false),
|
||||||
|
w_full(origin_e(Tui::fg(Rgb(255, 255, 255), format!("{name} "))))))),
|
||||||
|
w_full(origin_w(east(
|
||||||
|
button_2("l", "ength ", false),
|
||||||
|
w_full(origin_e(Tui::fg(Rgb(255, 255, 255), format!("{length} "))))))),
|
||||||
|
w_full(origin_w(east(
|
||||||
|
button_2("r", "epeat ", false),
|
||||||
|
w_full(origin_e(Tui::fg(Rgb(255, 255, 255), format!("{looped} "))))))),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
pub fn edit_status (&self) -> impl Draw<Tui> + '_ {
|
||||||
|
let (_color, length) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) {
|
||||||
|
(clip.color, clip.length)
|
||||||
|
} else { (ItemTheme::G[64], 0) };
|
||||||
|
let time_pos = self.get_time_pos();
|
||||||
|
let time_zoom = self.get_time_zoom();
|
||||||
|
let time_lock = if self.get_time_lock() { "[lock]" } else { " " };
|
||||||
|
let note_pos = self.get_note_pos();
|
||||||
|
let note_name = format!("{:4}", note_pitch_to_name(note_pos));
|
||||||
|
let note_pos = format!("{:>3}", note_pos);
|
||||||
|
let note_len = format!("{:>4}", self.get_note_len());
|
||||||
|
w_exact(20, south!(
|
||||||
|
w_full(origin_w(east(
|
||||||
|
button_2("t", "ime ", false),
|
||||||
|
w_full(origin_e(Tui::fg(Rgb(255, 255, 255),
|
||||||
|
format!("{length} /{time_zoom} +{time_pos} "))))))),
|
||||||
|
w_full(origin_w(east(
|
||||||
|
button_2("z", "lock ", false),
|
||||||
|
w_full(origin_e(Tui::fg(Rgb(255, 255, 255),
|
||||||
|
format!("{time_lock}"))))))),
|
||||||
|
w_full(origin_w(east(
|
||||||
|
button_2("x", "note ", false),
|
||||||
|
w_full(origin_e(Tui::fg(Rgb(255, 255, 255),
|
||||||
|
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) -> &AtomicUsize { self.mode.note_len() }
|
||||||
|
fn note_pos (&self) -> &AtomicUsize { self.mode.note_pos() }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimePoint for MidiEditor {
|
||||||
|
fn time_pos (&self) -> &AtomicUsize { self.mode.time_pos() }
|
||||||
|
}
|
||||||
|
|
||||||
|
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) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MidiEditor {
|
||||||
|
fn tui (&self) -> impl Draw<Tui> { self.autoscroll(); /*self.autozoom();*/ self.size.of(&self.mode) }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl PianoHorizontal {
|
||||||
|
pub fn new (clip: Option<&Arc<RwLock<MidiClip>>>) -> Self {
|
||||||
|
let size = Measure::new(0, 0);
|
||||||
|
let mut range = MidiSelection::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: MidiCursor::default(),
|
||||||
|
clip: clip.cloned(),
|
||||||
|
color: clip.as_ref().map(|p|p.read().unwrap().color).unwrap_or(ItemTheme::G[64]),
|
||||||
|
};
|
||||||
|
piano.redraw();
|
||||||
|
piano
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PianoHorizontal {
|
||||||
|
fn tui (&self) -> impl Draw<Tui> {
|
||||||
|
south(
|
||||||
|
east(w_exact(5, format!("{}x{}", self.size.w(), self.size.h())), self.timeline()),
|
||||||
|
east(self.keys(), self.size.of(below(wh_full(self.notes()), wh_full(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, note_point: usize, time_point: 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();
|
||||||
|
if note == (127-note_point) || time == time_point {
|
||||||
|
cell.set_bg(Rgb(0,0,0));
|
||||||
|
} else {
|
||||||
|
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 Draw<Tui> {
|
||||||
|
let time_start = self.get_time_start();
|
||||||
|
let note_lo = self.get_note_lo();
|
||||||
|
let note_hi = self.get_note_hi();
|
||||||
|
let buffer = self.buffer.clone();
|
||||||
|
Thunk::new(move|to: &mut Tui|{
|
||||||
|
let source = buffer.read().unwrap();
|
||||||
|
let XYWH(x0, y0, w, _h) = to.area();
|
||||||
|
//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 Draw<Tui> {
|
||||||
|
let note_hi = self.get_note_hi();
|
||||||
|
let note_lo = self.get_note_lo();
|
||||||
|
let note_pos = self.get_note_pos();
|
||||||
|
let note_len = self.get_note_len();
|
||||||
|
let time_pos = self.get_time_pos();
|
||||||
|
let time_start = self.get_time_start();
|
||||||
|
let time_zoom = self.get_time_zoom();
|
||||||
|
let style = Some(Style::default().fg(self.color.lightest.rgb));
|
||||||
|
Thunk::new(move|to: &mut Tui|{
|
||||||
|
let XYWH(x0, y0, w, _) = to.area();
|
||||||
|
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 Draw<Tui> {
|
||||||
|
let state = self;
|
||||||
|
let color = state.color;
|
||||||
|
let note_lo = state.get_note_lo();
|
||||||
|
let note_hi = state.get_note_hi();
|
||||||
|
let note_pos = state.get_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());
|
||||||
|
h_full(w_exact(self.keys_width, Thunk::new(move|to: &mut Tui|{
|
||||||
|
let XYWH(x, y0, _w, _h) = to.area();
|
||||||
|
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(¬e_pitch_to_name(note), x, screen_y, off_style)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
fn timeline (&self) -> impl Draw<Tui> + '_ {
|
||||||
|
w_full(h_exact(1, Thunk::new(move|to: &mut Tui|{
|
||||||
|
let XYWH(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) -> &AtomicUsize { self.point.note_len() }
|
||||||
|
fn note_pos (&self) -> &AtomicUsize { self.point.note_pos() }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimePoint for PianoHorizontal {
|
||||||
|
fn time_pos (&self) -> &AtomicUsize { self.point.time_pos() }
|
||||||
|
}
|
||||||
|
|
||||||
|
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 time_zoom = self.get_time_zoom();
|
||||||
|
self.time_len().set(clip.length);
|
||||||
|
PianoHorizontal::draw_bg(&mut buffer, &clip, time_zoom,self.get_note_len(), self.get_note_pos(), self.get_time_pos());
|
||||||
|
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(ItemTheme::G[64]);
|
||||||
|
self.redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for PianoHorizontal {
|
||||||
|
fn fmt (&self, f: &mut 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 OctaveVertical {
|
||||||
|
fn tui (&self) -> impl Draw<Tui> {
|
||||||
|
east!(
|
||||||
|
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), "▟"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl_from!(MidiSelection: |data:(usize, bool)| 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()),
|
||||||
|
});
|
||||||
|
impl_default!(MidiCursor: Self {
|
||||||
|
time_pos: Arc::new(0.into()),
|
||||||
|
note_pos: Arc::new(36.into()),
|
||||||
|
note_len: Arc::new(24.into()),
|
||||||
|
});
|
||||||
|
|
||||||
|
impl NotePoint for MidiCursor {
|
||||||
|
fn note_len (&self) -> &AtomicUsize {
|
||||||
|
&self.note_len
|
||||||
|
}
|
||||||
|
fn note_pos (&self) -> &AtomicUsize {
|
||||||
|
&self.note_pos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimePoint for MidiCursor {
|
||||||
|
fn time_pos (&self) -> &AtomicUsize {
|
||||||
|
self.time_pos.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimeRange for MidiSelection {
|
||||||
|
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 MidiSelection {
|
||||||
|
fn note_lo (&self) -> &AtomicUsize { &self.note_lo }
|
||||||
|
fn note_axis (&self) -> &AtomicUsize { &self.note_axis }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for Ticker {
|
||||||
|
type Item = (usize, usize);
|
||||||
|
fn next (&mut self) -> Option<Self::Item> {
|
||||||
|
loop {
|
||||||
|
if self.sample > self.end { return None }
|
||||||
|
let spp = self.spp;
|
||||||
|
let sample = self.sample as f64;
|
||||||
|
let start = self.start;
|
||||||
|
let end = self.end;
|
||||||
|
self.sample += 1;
|
||||||
|
//println!("{spp} {sample} {start} {end}");
|
||||||
|
let jitter = sample.rem_euclid(spp); // ramps
|
||||||
|
let next_jitter = (sample + 1.0).rem_euclid(spp);
|
||||||
|
if jitter > next_jitter { // at crossing:
|
||||||
|
let time = (sample as usize) % (end as usize-start as usize);
|
||||||
|
let tick = (sample / spp) as usize;
|
||||||
|
return Some((time, tick))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
833
src/tek.rs
833
src/tek.rs
File diff suppressed because it is too large
Load diff
3586
src/tek_impls.rs
3586
src/tek_impls.rs
File diff suppressed because it is too large
Load diff
168
src/tick.rs
168
src/tick.rs
|
|
@ -1,168 +0,0 @@
|
||||||
pub trait HasClock: AsRef<Clock> + AsMut<Clock> {
|
|
||||||
fn clock (&self) -> &Clock { self.as_ref() }
|
|
||||||
fn clock_mut (&mut self) -> &mut Clock { self.as_mut() }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Temporal resolutions: sample rate, tempo, MIDI pulses per quaver (beat)
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let _ = tek::Timebase::default();
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Clone)] pub struct Timebase {
|
|
||||||
/// Audio samples per second
|
|
||||||
pub sr: SampleRate,
|
|
||||||
/// MIDI beats per minute
|
|
||||||
pub bpm: Bpm,
|
|
||||||
/// MIDI ticks per beat
|
|
||||||
pub ppq: Ppq,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Iterator that emits subsequent ticks within a range.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let iter = tek::Ticker::default();
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Default)] pub struct Ticker {
|
|
||||||
pub spp: f64,
|
|
||||||
pub sample: usize,
|
|
||||||
pub start: usize,
|
|
||||||
pub end: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A point in time in all time scales (microsecond, sample, MIDI pulse)
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let _ = tek::Moment::default();
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Default, Clone)] pub struct Moment {
|
|
||||||
pub timebase: Arc<Timebase>,
|
|
||||||
/// Current time in microseconds
|
|
||||||
pub usec: Microsecond,
|
|
||||||
/// Current time in audio samples
|
|
||||||
pub sample: SampleCount,
|
|
||||||
/// Current time in MIDI pulses
|
|
||||||
pub pulse: Pulse,
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let _ = tek::Moment2::default();
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Clone, Default)] pub enum Moment2 {
|
|
||||||
#[default] None,
|
|
||||||
Zero,
|
|
||||||
Usec(Microsecond),
|
|
||||||
Sample(SampleCount),
|
|
||||||
Pulse(Pulse),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// MIDI resolution in PPQ (pulses per quarter note)
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Default)] pub struct Ppq (pub(crate) AtomicF64);
|
|
||||||
|
|
||||||
/// Timestamp in MIDI pulses
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Default)] pub struct Pulse (pub(crate) AtomicF64);
|
|
||||||
|
|
||||||
/// Tempo in beats per minute
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Default)] pub struct Bpm (pub(crate) AtomicF64);
|
|
||||||
|
|
||||||
/// Quantization setting for launching clips
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Default)] pub struct LaunchSync (pub(crate) AtomicF64);
|
|
||||||
|
|
||||||
/// Quantization setting for notes
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Default)] pub struct Quantize (pub(crate) AtomicF64);
|
|
||||||
|
|
||||||
/// Timestamp in audio samples
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Default)] pub struct SampleCount (pub(crate) AtomicF64);
|
|
||||||
|
|
||||||
/// Audio sample rate in Hz (samples per second)
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Default)] pub struct SampleRate (pub(crate) AtomicF64);
|
|
||||||
|
|
||||||
/// Timestamp in microseconds
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Default)] pub struct Microsecond (pub(crate) AtomicF64);
|
|
||||||
|
|
||||||
/// The source of time.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let clock = tek::Clock::default();
|
|
||||||
/// ```
|
|
||||||
#[derive(Clone, Default)] pub struct Clock {
|
|
||||||
/// JACK transport handle.
|
|
||||||
pub transport: Arc<Option<Transport>>,
|
|
||||||
/// Global temporal resolution (shared by [Moment] fields)
|
|
||||||
pub timebase: Arc<Timebase>,
|
|
||||||
/// Current global sample and usec (monotonic from JACK clock)
|
|
||||||
pub global: Arc<Moment>,
|
|
||||||
/// Global sample and usec at which playback started
|
|
||||||
pub started: Arc<RwLock<Option<Moment>>>,
|
|
||||||
/// Playback offset (when playing not from start)
|
|
||||||
pub offset: Arc<Moment>,
|
|
||||||
/// Current playhead position
|
|
||||||
pub playhead: Arc<Moment>,
|
|
||||||
/// Note quantization factor
|
|
||||||
pub quant: Arc<Quantize>,
|
|
||||||
/// Launch quantization factor
|
|
||||||
pub sync: Arc<LaunchSync>,
|
|
||||||
/// Size of buffer in samples
|
|
||||||
pub chunk: Arc<AtomicUsize>,
|
|
||||||
// Cache of formatted strings
|
|
||||||
pub view_cache: Arc<RwLock<ClockView>>,
|
|
||||||
/// For syncing the clock to an external source
|
|
||||||
#[cfg(feature = "port")] pub midi_in: Arc<RwLock<Option<MidiInput>>>,
|
|
||||||
/// For syncing other devices to this clock
|
|
||||||
#[cfg(feature = "port")] pub midi_out: Arc<RwLock<Option<MidiOutput>>>,
|
|
||||||
/// For emitting a metronome
|
|
||||||
#[cfg(feature = "port")] pub click_out: Arc<RwLock<Option<AudioOutput>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A unit of time, represented as an atomic 64-bit float.
|
|
||||||
///
|
|
||||||
/// According to https://stackoverflow.com/a/873367, as per IEEE754,
|
|
||||||
/// every integer between 1 and 2^53 can be represented exactly.
|
|
||||||
/// This should mean that, even at 192kHz sampling rate, over 1 year of audio
|
|
||||||
/// can be clocked in microseconds with f64 without losing precision.
|
|
||||||
pub trait TimeUnit: InteriorMutable<f64> {}
|
|
||||||
|
|
||||||
/// Contains memoized renders of clock values.
|
|
||||||
///
|
|
||||||
/// Performance optimization.
|
|
||||||
#[derive(Debug)] pub struct ClockView {
|
|
||||||
pub sr: Memo<Option<(bool, f64)>, String>,
|
|
||||||
pub buf: Memo<Option<f64>, String>,
|
|
||||||
pub lat: Memo<Option<f64>, String>,
|
|
||||||
pub bpm: Memo<Option<f64>, String>,
|
|
||||||
pub beat: Memo<Option<f64>, String>,
|
|
||||||
pub time: Memo<Option<f64>, String>,
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue