mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-02-21 08:19:03 +01:00
1054 lines
38 KiB
Rust
1054 lines
38 KiB
Rust
use crate::*;
|
|
use std::sync::atomic::Ordering;
|
|
|
|
/// Things that can provide a [jack::Client] reference.
|
|
///
|
|
/// ```
|
|
/// use tek::{Jack, HasJack};
|
|
///
|
|
/// let jack: &Jack = Jacked::default().jack();
|
|
///
|
|
/// #[derive(Default)] struct Jacked<'j>(Jack<'j>);
|
|
///
|
|
/// impl<'j> tek::HasJack<'j> for Jacked<'j> {
|
|
/// fn jack (&self) -> &Jack<'j> { &self.0 }
|
|
/// }
|
|
/// ```
|
|
pub trait HasJack<'j>: Send + Sync {
|
|
|
|
/// Return the internal [jack::Client] handle
|
|
/// that lets you call the JACK API.
|
|
fn jack (&self) -> &Jack<'j>;
|
|
|
|
fn with_client <T> (&self, op: impl FnOnce(&Client)->T) -> T {
|
|
self.jack().with_client(op)
|
|
}
|
|
|
|
fn port_by_name (&self, name: &str) -> Option<Port<Unowned>> {
|
|
self.with_client(|client|client.port_by_name(name))
|
|
}
|
|
|
|
fn port_by_id (&self, id: u32) -> Option<Port<Unowned>> {
|
|
self.with_client(|c|c.port_by_id(id))
|
|
}
|
|
|
|
fn register_port <PS: PortSpec + Default> (&self, name: impl AsRef<str>) -> Usually<Port<PS>> {
|
|
self.with_client(|client|Ok(client.register_port(name.as_ref(), PS::default())?))
|
|
}
|
|
|
|
fn sync_lead (&self, enable: bool, callback: impl Fn(TimebaseInfo)->Position) -> Usually<()> {
|
|
if enable {
|
|
self.with_client(|client|match client.register_timebase_callback(false, callback) {
|
|
Ok(_) => Ok(()),
|
|
Err(e) => Err(e)
|
|
})?
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn sync_follow (&self, _enable: bool) -> Usually<()> {
|
|
// TODO: sync follow
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Trait for thing that has a JACK process callback.
|
|
pub trait Audio {
|
|
|
|
/// Handle a JACK event.
|
|
fn handle (&mut self, _event: JackEvent) {}
|
|
|
|
/// Projecss a JACK chunk.
|
|
fn process (&mut self, _: &Client, _: &ProcessScope) -> Control {
|
|
Control::Continue
|
|
}
|
|
|
|
/// The JACK process callback function passed to the server.
|
|
fn callback (
|
|
state: &Arc<RwLock<Self>>, client: &Client, scope: &ProcessScope
|
|
) -> Control where Self: Sized {
|
|
if let Ok(mut state) = state.write() {
|
|
state.process(client, scope)
|
|
} else {
|
|
Control::Quit
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
/// Implement [Audio]: provide JACK callbacks.
|
|
#[macro_export] macro_rules! audio {
|
|
|
|
(|
|
|
$self1:ident:
|
|
$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?,$c:ident,$s:ident
|
|
|$cb:expr$(;|$self2:ident,$e:ident|$cb2:expr)?) => {
|
|
impl $(<$($L),*$($T $(: $U)?),*>)? Audio for $Struct $(<$($L),*$($T),*>)? {
|
|
#[inline] fn process (&mut $self1, $c: &Client, $s: &ProcessScope) -> Control { $cb }
|
|
$(#[inline] fn handle (&mut $self2, $e: JackEvent) { $cb2 })?
|
|
}
|
|
};
|
|
|
|
($Struct:ident: $process:ident, $handle:ident) => {
|
|
impl Audio for $Struct {
|
|
#[inline] fn process (&mut self, c: &Client, s: &ProcessScope) -> Control {
|
|
$process(self, c, s)
|
|
}
|
|
#[inline] fn handle (&mut self, e: JackEvent) {
|
|
$handle(self, e)
|
|
}
|
|
}
|
|
};
|
|
|
|
($Struct:ident: $process:ident) => {
|
|
impl Audio for $Struct {
|
|
#[inline] fn process (&mut self, c: &Client, s: &ProcessScope) -> Control {
|
|
$process(self, c, s)
|
|
}
|
|
}
|
|
};
|
|
|
|
}
|
|
|
|
pub trait JackPerfModel {
|
|
fn update_from_jack_scope (&self, t0: Option<u64>, scope: &ProcessScope);
|
|
}
|
|
|
|
|
|
//pub trait MaybeHas<T>: Send + Sync {
|
|
//fn get (&self) -> Option<&T>;
|
|
//}
|
|
|
|
pub trait HasN<T>: Send + Sync {
|
|
fn get_nth (&self, key: usize) -> &T;
|
|
fn get_nth_mut (&mut self, key: usize) -> &mut T;
|
|
}
|
|
|
|
pub trait Gettable<T> {
|
|
/// Returns current value
|
|
fn get (&self) -> T;
|
|
}
|
|
|
|
pub trait Mutable<T>: Gettable<T> {
|
|
/// Sets new value, returns old
|
|
fn set (&mut self, value: T) -> T;
|
|
}
|
|
|
|
pub trait InteriorMutable<T>: Gettable<T> {
|
|
/// Sets new value, returns old
|
|
fn set (&self, value: T) -> T;
|
|
}
|
|
|
|
pub trait NotePoint {
|
|
fn note_len (&self) -> &AtomicUsize;
|
|
/// Get the current length of the note cursor.
|
|
fn get_note_len (&self) -> usize {
|
|
self.note_len().load(Relaxed)
|
|
}
|
|
/// Set the length of the note cursor, returning the previous value.
|
|
fn set_note_len (&self, x: usize) -> usize {
|
|
self.note_len().swap(x, Relaxed)
|
|
}
|
|
|
|
fn note_pos (&self) -> &AtomicUsize;
|
|
/// Get the current pitch of the note cursor.
|
|
fn get_note_pos (&self) -> usize {
|
|
self.note_pos().load(Relaxed).min(127)
|
|
}
|
|
/// Set the current pitch fo the note cursor, returning the previous value.
|
|
fn set_note_pos (&self, x: usize) -> usize {
|
|
self.note_pos().swap(x.min(127), Relaxed)
|
|
}
|
|
}
|
|
|
|
pub trait TimePoint {
|
|
fn time_pos (&self) -> &AtomicUsize;
|
|
/// Get the current time position of the note cursor.
|
|
fn get_time_pos (&self) -> usize {
|
|
self.time_pos().load(Relaxed)
|
|
}
|
|
/// Set the current time position of the note cursor, returning the previous value.
|
|
fn set_time_pos (&self, x: usize) -> usize {
|
|
self.time_pos().swap(x, Relaxed)
|
|
}
|
|
}
|
|
|
|
pub trait MidiPoint: NotePoint + TimePoint {
|
|
/// Get the current end of the note cursor.
|
|
fn get_note_end (&self) -> usize {
|
|
self.get_time_pos() + self.get_note_len()
|
|
}
|
|
}
|
|
|
|
pub trait TimeRange {
|
|
fn time_len (&self) -> &AtomicUsize;
|
|
fn get_time_len (&self) -> usize {
|
|
self.time_len().load(Ordering::Relaxed)
|
|
}
|
|
fn time_zoom (&self) -> &AtomicUsize;
|
|
fn get_time_zoom (&self) -> usize {
|
|
self.time_zoom().load(Ordering::Relaxed)
|
|
}
|
|
fn set_time_zoom (&self, value: usize) -> usize {
|
|
self.time_zoom().swap(value, Ordering::Relaxed)
|
|
}
|
|
fn time_lock (&self) -> &AtomicBool;
|
|
fn get_time_lock (&self) -> bool {
|
|
self.time_lock().load(Ordering::Relaxed)
|
|
}
|
|
fn set_time_lock (&self, value: bool) -> bool {
|
|
self.time_lock().swap(value, Ordering::Relaxed)
|
|
}
|
|
fn time_start (&self) -> &AtomicUsize;
|
|
fn get_time_start (&self) -> usize {
|
|
self.time_start().load(Ordering::Relaxed)
|
|
}
|
|
fn set_time_start (&self, value: usize) -> usize {
|
|
self.time_start().swap(value, Ordering::Relaxed)
|
|
}
|
|
fn time_axis (&self) -> &AtomicUsize;
|
|
fn get_time_axis (&self) -> usize {
|
|
self.time_axis().load(Ordering::Relaxed)
|
|
}
|
|
fn get_time_end (&self) -> usize {
|
|
self.time_start().get() + self.time_axis().get() * self.time_zoom().get()
|
|
}
|
|
}
|
|
|
|
pub trait NoteRange {
|
|
fn note_lo (&self) -> &AtomicUsize;
|
|
fn get_note_lo (&self) -> usize {
|
|
self.note_lo().load(Ordering::Relaxed)
|
|
}
|
|
fn set_note_lo (&self, x: usize) -> usize {
|
|
self.note_lo().swap(x, Ordering::Relaxed)
|
|
}
|
|
fn note_axis (&self) -> &AtomicUsize;
|
|
fn get_note_axis (&self) -> usize {
|
|
self.note_axis().load(Ordering::Relaxed)
|
|
}
|
|
fn get_note_hi (&self) -> usize {
|
|
(self.note_lo().get() + self.note_axis().get().saturating_sub(1)).min(127)
|
|
}
|
|
}
|
|
|
|
pub trait MidiRange: TimeRange + NoteRange {}
|
|
|
|
/// 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> {}
|
|
|
|
pub trait HasSceneScroll: HasScenes {
|
|
fn scene_scroll (&self) -> usize;
|
|
}
|
|
|
|
pub trait HasTrackScroll: HasTracks {
|
|
fn track_scroll (&self) -> usize;
|
|
}
|
|
|
|
pub trait HasMidiClip {
|
|
fn clip (&self) -> Option<Arc<RwLock<MidiClip>>>;
|
|
}
|
|
|
|
pub trait HasClipsSize {
|
|
fn clips_size (&self) -> &Measure<TuiOut>;
|
|
}
|
|
|
|
pub trait HasClock: Send + Sync {
|
|
fn clock (&self) -> &Clock;
|
|
fn clock_mut (&mut self) -> &mut Clock;
|
|
}
|
|
|
|
pub trait HasDevices {
|
|
fn devices (&self) -> &Vec<Device>;
|
|
fn devices_mut (&mut self) -> &mut Vec<Device>;
|
|
}
|
|
|
|
pub trait HasSelection: Has<Selection> {
|
|
fn selection (&self) -> &Selection { self.get() }
|
|
fn selection_mut (&mut self) -> &mut Selection { self.get_mut() }
|
|
}
|
|
|
|
pub trait HasWidth {
|
|
const MIN_WIDTH: usize;
|
|
/// Increment track width.
|
|
fn width_inc (&mut self);
|
|
/// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH].
|
|
fn width_dec (&mut self);
|
|
}
|
|
|
|
pub trait HasMidiBuffers {
|
|
fn note_buf_mut (&mut self) -> &mut Vec<u8>;
|
|
fn midi_buf_mut (&mut self) -> &mut Vec<Vec<Vec<u8>>>;
|
|
}
|
|
|
|
pub trait HasSequencer {
|
|
fn sequencer (&self) -> &Sequencer;
|
|
fn sequencer_mut (&mut self) -> &mut Sequencer;
|
|
}
|
|
|
|
pub trait HasScene: Has<Option<Scene>> + Send + Sync {
|
|
fn scene (&self) -> Option<&Scene> {
|
|
Has::<Option<Scene>>::get(self).as_ref()
|
|
}
|
|
fn scene_mut (&mut self) -> &mut Option<Scene> {
|
|
Has::<Option<Scene>>::get_mut(self)
|
|
}
|
|
}
|
|
|
|
pub trait HasScenes: Has<Vec<Scene>> + Send + Sync {
|
|
fn scenes (&self) -> &Vec<Scene> {
|
|
Has::<Vec<Scene>>::get(self)
|
|
}
|
|
fn scenes_mut (&mut self) -> &mut Vec<Scene> {
|
|
Has::<Vec<Scene>>::get_mut(self)
|
|
}
|
|
/// Generate the default name for a new scene
|
|
fn scene_default_name (&self) -> Arc<str> {
|
|
format!("s{:3>}", self.scenes().len() + 1).into()
|
|
}
|
|
fn scene_longest_name (&self) -> usize {
|
|
self.scenes().iter().map(|s|s.name.len()).fold(0, usize::max)
|
|
}
|
|
}
|
|
|
|
/// ```
|
|
/// use tek::{MidiEditor, HasEditor, tengri::Has};
|
|
/// struct TestEditorHost(Option<MidiEditor>);
|
|
/// tek::tengri::has!(Option<MidiEditor>: |self: TestEditorHost|self.0);
|
|
/// let mut host = TestEditorHost(Some(MidiEditor::default()));
|
|
/// let _ = host.editor();
|
|
/// let _ = host.editor_mut();
|
|
/// let _ = host.is_editing();
|
|
/// let _ = host.editor_w();
|
|
/// let _ = host.editor_h();
|
|
/// ```
|
|
pub trait HasEditor: Has<Option<MidiEditor>> {
|
|
fn editor (&self) -> Option<&MidiEditor> {
|
|
self.get().as_ref()
|
|
}
|
|
fn editor_mut (&mut self) -> Option<&mut MidiEditor> {
|
|
self.get_mut().as_mut()
|
|
}
|
|
fn is_editing (&self) -> bool {
|
|
self.editor().is_some()
|
|
}
|
|
fn editor_w (&self) -> usize {
|
|
self.editor().map(|e|e.size.w()).unwrap_or(0) as usize
|
|
}
|
|
fn editor_h (&self) -> usize {
|
|
self.editor().map(|e|e.size.h()).unwrap_or(0) as usize
|
|
}
|
|
}
|
|
|
|
pub trait HasClips {
|
|
fn clips <'a> (&'a self) -> std::sync::RwLockReadGuard<'a, ClipPool>;
|
|
fn clips_mut <'a> (&'a self) -> std::sync::RwLockWriteGuard<'a, ClipPool>;
|
|
fn add_clip (&self) -> (usize, Arc<RwLock<MidiClip>>) {
|
|
let clip = Arc::new(RwLock::new(MidiClip::new("Clip", true, 384, None, None)));
|
|
self.clips_mut().push(clip.clone());
|
|
(self.clips().len() - 1, clip)
|
|
}
|
|
}
|
|
|
|
/// Trait for thing that may receive MIDI.
|
|
pub trait HasMidiIns {
|
|
fn midi_ins (&self) -> &Vec<MidiInput>;
|
|
fn midi_ins_mut (&mut self) -> &mut Vec<MidiInput>;
|
|
/// Collect MIDI input from app ports (TODO preallocate large buffers)
|
|
fn midi_input_collect <'a> (&'a self, scope: &'a ProcessScope) -> CollectedMidiInput<'a> {
|
|
self.midi_ins().iter()
|
|
.map(|port|port.port().iter(scope)
|
|
.map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes)))
|
|
.collect::<Vec<_>>())
|
|
.collect::<Vec<_>>()
|
|
}
|
|
fn midi_ins_with_sizes <'a> (&'a self) ->
|
|
impl Iterator<Item=(usize, &'a Arc<str>, &'a [Connect], usize, usize)> + Send + Sync + 'a
|
|
{
|
|
let mut y = 0;
|
|
self.midi_ins().iter().enumerate().map(move|(i, input)|{
|
|
let height = 1 + input.connections().len();
|
|
let data = (i, input.port_name(), input.connections(), y, y + height);
|
|
y += height;
|
|
data
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Trait for thing that may output MIDI.
|
|
pub trait HasMidiOuts {
|
|
fn midi_outs (&self) -> &Vec<MidiOutput>;
|
|
fn midi_outs_mut (&mut self) -> &mut Vec<MidiOutput>;
|
|
fn midi_outs_with_sizes <'a> (&'a self) ->
|
|
impl Iterator<Item=(usize, &'a Arc<str>, &'a [Connect], usize, usize)> + Send + Sync + 'a
|
|
{
|
|
let mut y = 0;
|
|
self.midi_outs().iter().enumerate().map(move|(i, output)|{
|
|
let height = 1 + output.connections().len();
|
|
let data = (i, output.port_name(), output.connections(), y, y + height);
|
|
y += height;
|
|
data
|
|
})
|
|
}
|
|
fn midi_outs_emit (&mut self, scope: &ProcessScope) {
|
|
for port in self.midi_outs_mut().iter_mut() {
|
|
port.buffer_emit(scope)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub trait HasTracks: Has<Vec<Track>> + Send + Sync {
|
|
fn tracks (&self) -> &Vec<Track> { Has::<Vec<Track>>::get(self) }
|
|
fn tracks_mut (&mut self) -> &mut Vec<Track> { Has::<Vec<Track>>::get_mut(self) }
|
|
/// Run audio callbacks for every track and every device
|
|
fn process_tracks (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
|
for track in self.tracks_mut().iter_mut() {
|
|
if Control::Quit == Audio::process(&mut track.sequencer, client, scope) {
|
|
return Control::Quit
|
|
}
|
|
for device in track.devices.iter_mut() {
|
|
if Control::Quit == DeviceAudio(device).process(client, scope) {
|
|
return Control::Quit
|
|
}
|
|
}
|
|
}
|
|
Control::Continue
|
|
}
|
|
fn track_longest_name (&self) -> usize { self.tracks().iter().map(|s|s.name.len()).fold(0, usize::max) }
|
|
/// Stop all playing clips
|
|
fn tracks_stop_all (&mut self) { for track in self.tracks_mut().iter_mut() { track.sequencer.enqueue_next(None); } }
|
|
/// Stop all playing clips
|
|
fn tracks_launch (&mut self, clips: Option<Vec<Option<Arc<RwLock<MidiClip>>>>>) {
|
|
if let Some(clips) = clips {
|
|
for (clip, track) in clips.iter().zip(self.tracks_mut()) { track.sequencer.enqueue_next(clip.as_ref()); }
|
|
} else {
|
|
for track in self.tracks_mut().iter_mut() { track.sequencer.enqueue_next(None); }
|
|
}
|
|
}
|
|
/// Spacing between tracks.
|
|
const TRACK_SPACING: usize = 0;
|
|
}
|
|
|
|
pub trait HasTrack {
|
|
fn track (&self) -> Option<&Track>;
|
|
fn track_mut (&mut self) -> Option<&mut Track>;
|
|
#[cfg(feature = "port")] fn view_midi_ins_status <'a> (&'a self, theme: ItemTheme) -> impl Content<TuiOut> + 'a {
|
|
self.track().map(move|track|view_ports_status(theme, "MIDI ins: ", &track.sequencer.midi_ins))
|
|
}
|
|
#[cfg(feature = "port")] fn view_midi_outs_status (&self, theme: ItemTheme) -> impl Content<TuiOut> + '_ {
|
|
self.track().map(move|track|view_ports_status(theme, "MIDI outs: ", &track.sequencer.midi_outs))
|
|
}
|
|
#[cfg(feature = "port")] fn view_audio_ins_status (&self, theme: ItemTheme) -> impl Content<TuiOut> {
|
|
self.track().map(move|track|view_ports_status(theme, "Audio ins: ", &track.audio_ins()))
|
|
}
|
|
#[cfg(feature = "port")] fn view_audio_outs_status (&self, theme: ItemTheme) -> impl Content<TuiOut> {
|
|
self.track().map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs()))
|
|
}
|
|
}
|
|
|
|
pub trait HasPlayClip: HasClock {
|
|
|
|
fn reset (&self) -> bool;
|
|
|
|
fn reset_mut (&mut self) -> &mut bool;
|
|
|
|
fn play_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
|
|
|
fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
|
|
|
fn next_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
|
|
|
fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
|
|
|
|
fn pulses_since_start (&self) -> Option<f64> {
|
|
if let Some((started, Some(_))) = self.play_clip().as_ref() {
|
|
let elapsed = self.clock().playhead.pulse.get() - started.pulse.get();
|
|
return Some(elapsed)
|
|
}
|
|
None
|
|
}
|
|
|
|
fn pulses_since_start_looped (&self) -> Option<(f64, f64)> {
|
|
if let Some((started, Some(clip))) = self.play_clip().as_ref() {
|
|
let elapsed = self.clock().playhead.pulse.get() - started.pulse.get();
|
|
let length = clip.read().unwrap().length.max(1); // prevent div0 on empty clip
|
|
let times = (elapsed as usize / length) as f64;
|
|
let elapsed = (elapsed as usize % length) as f64;
|
|
return Some((times, elapsed))
|
|
}
|
|
None
|
|
}
|
|
|
|
fn enqueue_next (&mut self, clip: Option<&Arc<RwLock<MidiClip>>>) {
|
|
*self.next_clip_mut() = Some((self.clock().next_launch_instant(), clip.cloned()));
|
|
*self.reset_mut() = true;
|
|
}
|
|
|
|
fn play_status (&self) -> impl Content<TuiOut> {
|
|
let (name, color): (Arc<str>, ItemTheme) = if let Some((_, Some(clip))) = self.play_clip() {
|
|
let MidiClip { ref name, color, .. } = *clip.read().unwrap();
|
|
(name.clone(), color)
|
|
} else {
|
|
("".into(), Tui::g(64).into())
|
|
};
|
|
let time: String = self.pulses_since_start_looped()
|
|
.map(|(times, time)|format!("{:>3}x {:>}", times+1.0, self.clock().timebase.format_beats_1(time)))
|
|
.unwrap_or_else(||String::from(" ")).into();
|
|
FieldV(color, "Now:", format!("{} {}", time, name))
|
|
}
|
|
|
|
fn next_status (&self) -> impl Content<TuiOut> {
|
|
let mut time: Arc<str> = String::from("--.-.--").into();
|
|
let mut name: Arc<str> = String::from("").into();
|
|
let mut color = ItemTheme::G[64];
|
|
let clock = self.clock();
|
|
if let Some((t, Some(clip))) = self.next_clip() {
|
|
let clip = clip.read().unwrap();
|
|
name = clip.name.clone();
|
|
color = clip.color.clone();
|
|
time = {
|
|
let target = t.pulse.get();
|
|
let current = clock.playhead.pulse.get();
|
|
if target > current {
|
|
let remaining = target - current;
|
|
format!("-{:>}", clock.timebase.format_beats_1(remaining))
|
|
} else {
|
|
String::new()
|
|
}
|
|
}.into()
|
|
} else if let Some((t, Some(clip))) = self.play_clip() {
|
|
let clip = clip.read().unwrap();
|
|
if clip.looped {
|
|
name = clip.name.clone();
|
|
color = clip.color.clone();
|
|
let target = t.pulse.get() + clip.length as f64;
|
|
let current = clock.playhead.pulse.get();
|
|
if target > current {
|
|
time = format!("-{:>}", clock.timebase.format_beats_0(target - current)).into()
|
|
}
|
|
} else {
|
|
name = "Stop".to_string().into();
|
|
}
|
|
};
|
|
FieldV(color, "Next:", format!("{} {}", time, name))
|
|
}
|
|
}
|
|
|
|
pub trait MidiMonitor: HasMidiIns + HasMidiBuffers {
|
|
fn notes_in (&self) -> &Arc<RwLock<[bool;128]>>;
|
|
fn monitoring (&self) -> bool;
|
|
fn monitoring_mut (&mut self) -> &mut bool;
|
|
fn toggle_monitor (&mut self) {
|
|
*self.monitoring_mut() = !self.monitoring();
|
|
}
|
|
fn monitor (&mut self, scope: &ProcessScope) {
|
|
}
|
|
}
|
|
|
|
pub trait MidiRecord: MidiMonitor + HasClock + HasPlayClip {
|
|
fn recording (&self) -> bool;
|
|
fn recording_mut (&mut self) -> &mut bool;
|
|
fn toggle_record (&mut self) {
|
|
*self.recording_mut() = !self.recording();
|
|
}
|
|
|
|
fn overdub (&self) -> bool;
|
|
fn overdub_mut (&mut self) -> &mut bool;
|
|
fn toggle_overdub (&mut self) {
|
|
*self.overdub_mut() = !self.overdub();
|
|
}
|
|
|
|
fn record_clip (
|
|
&mut self,
|
|
scope: &ProcessScope,
|
|
started: Moment,
|
|
clip: &Option<Arc<RwLock<MidiClip>>>,
|
|
) {
|
|
if let Some(clip) = clip {
|
|
let sample0 = scope.last_frame_time() as usize;
|
|
let start = started.sample.get() as usize;
|
|
let _recording = self.recording();
|
|
let timebase = self.clock().timebase().clone();
|
|
let quant = self.clock().quant.get();
|
|
let mut clip = clip.write().unwrap();
|
|
let length = clip.length;
|
|
for input in self.midi_ins_mut().iter() {
|
|
for (sample, event, _bytes) in parse_midi_input(input.port().iter(scope)) {
|
|
if let LiveEvent::Midi { message, .. } = event {
|
|
clip.record_event({
|
|
let sample = (sample0 + sample - start) as f64;
|
|
let pulse = timebase.samples_to_pulse(sample);
|
|
let quantized = (pulse / quant).round() * quant;
|
|
quantized as usize % length
|
|
}, message);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn record_next (&mut self) {
|
|
// TODO switch to next clip and record into it
|
|
}
|
|
}
|
|
|
|
pub trait MidiViewer: Measured<TuiOut> + MidiRange + MidiPoint + Debug + Send + Sync {
|
|
fn buffer_size (&self, clip: &MidiClip) -> (usize, usize);
|
|
fn redraw (&self);
|
|
fn clip (&self) -> &Option<Arc<RwLock<MidiClip>>>;
|
|
fn clip_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>>;
|
|
fn set_clip (&mut self, clip: Option<&Arc<RwLock<MidiClip>>>) {
|
|
*self.clip_mut() = clip.cloned();
|
|
self.redraw();
|
|
}
|
|
/// Make sure cursor is within note range
|
|
fn autoscroll (&self) {
|
|
let note_pos = self.get_note_pos().min(127);
|
|
let note_lo = self.get_note_lo();
|
|
let note_hi = self.get_note_hi();
|
|
if note_pos < note_lo {
|
|
self.note_lo().set(note_pos);
|
|
} else if note_pos > note_hi {
|
|
self.note_lo().set((note_lo + note_pos).saturating_sub(note_hi));
|
|
}
|
|
}
|
|
/// Make sure time range is within display
|
|
fn autozoom (&self) {
|
|
if self.time_lock().get() {
|
|
let time_len = self.get_time_len();
|
|
let time_axis = self.get_time_axis();
|
|
let time_zoom = self.get_time_zoom();
|
|
loop {
|
|
let time_zoom = self.time_zoom().get();
|
|
let time_area = time_axis * time_zoom;
|
|
if time_area > time_len {
|
|
let next_time_zoom = note_duration_prev(time_zoom);
|
|
if next_time_zoom <= 1 {
|
|
break
|
|
}
|
|
let next_time_area = time_axis * next_time_zoom;
|
|
if next_time_area >= time_len {
|
|
self.time_zoom().set(next_time_zoom);
|
|
} else {
|
|
break
|
|
}
|
|
} else if time_area < time_len {
|
|
let prev_time_zoom = note_duration_next(time_zoom);
|
|
if prev_time_zoom > 384 {
|
|
break
|
|
}
|
|
let prev_time_area = time_axis * prev_time_zoom;
|
|
if prev_time_area <= time_len {
|
|
self.time_zoom().set(prev_time_zoom);
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if time_zoom != self.time_zoom().get() {
|
|
self.redraw()
|
|
}
|
|
}
|
|
//while time_len.div_ceil(time_zoom) > time_axis {
|
|
//println!("\r{time_len} {time_zoom} {time_axis}");
|
|
//time_zoom = Note::next(time_zoom);
|
|
//}
|
|
//self.time_zoom().set(time_zoom);
|
|
}
|
|
}
|
|
|
|
pub trait AddScene: HasScenes + HasTracks {
|
|
/// Add multiple scenes
|
|
fn scenes_add (&mut self, n: usize) -> Usually<()> {
|
|
let scene_color_1 = ItemColor::random();
|
|
let scene_color_2 = ItemColor::random();
|
|
for i in 0..n {
|
|
let _ = self.scene_add(None, Some(
|
|
scene_color_1.mix(scene_color_2, i as f32 / n as f32).into()
|
|
))?;
|
|
}
|
|
Ok(())
|
|
}
|
|
/// Add a scene
|
|
fn scene_add (&mut self, name: Option<&str>, color: Option<ItemTheme>)
|
|
-> Usually<(usize, &mut Scene)>
|
|
{
|
|
let scene = Scene {
|
|
name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()),
|
|
clips: vec![None;self.tracks().len()],
|
|
color: color.unwrap_or_else(ItemTheme::random),
|
|
};
|
|
self.scenes_mut().push(scene);
|
|
let index = self.scenes().len() - 1;
|
|
Ok((index, &mut self.scenes_mut()[index]))
|
|
}
|
|
}
|
|
|
|
pub trait ClipsView: TracksView + ScenesView {
|
|
|
|
fn view_scenes_clips <'a> (&'a self)
|
|
-> impl Content<TuiOut> + 'a
|
|
{
|
|
self.clips_size().of(Fill::XY(Bsp::a(
|
|
Fill::XY(Align::se(Tui::fg(Green, format!("{}x{}", self.clips_size().w(), self.clips_size().h())))),
|
|
Thunk::new(|to: &mut TuiOut|for (
|
|
track_index, track, _, _
|
|
) in self.tracks_with_sizes() {
|
|
to.place(&Fixed::X(track.width as u16,
|
|
Fill::Y(self.view_track_clips(track_index, track))))
|
|
}))))
|
|
}
|
|
|
|
fn view_track_clips <'a> (&'a self, track_index: usize, track: &'a Track) -> impl Content<TuiOut> + 'a {
|
|
Thunk::new(move|to: &mut TuiOut|for (
|
|
scene_index, scene, ..
|
|
) in self.scenes_with_sizes() {
|
|
let (name, theme): (Arc<str>, ItemTheme) = if let Some(Some(clip)) = &scene.clips.get(track_index) {
|
|
let clip = clip.read().unwrap();
|
|
(format!(" ⏹ {}", &clip.name).into(), clip.color)
|
|
} else {
|
|
(" ⏹ -- ".into(), ItemTheme::G[32])
|
|
};
|
|
let fg = theme.lightest.rgb;
|
|
let mut outline = theme.base.rgb;
|
|
let bg = if self.selection().track() == Some(track_index)
|
|
&& self.selection().scene() == Some(scene_index)
|
|
{
|
|
outline = theme.lighter.rgb;
|
|
theme.light.rgb
|
|
} else if self.selection().track() == Some(track_index)
|
|
|| self.selection().scene() == Some(scene_index)
|
|
{
|
|
outline = theme.darkest.rgb;
|
|
theme.base.rgb
|
|
} else {
|
|
theme.dark.rgb
|
|
};
|
|
let w = if self.selection().track() == Some(track_index)
|
|
&& let Some(editor) = self.editor ()
|
|
{
|
|
(editor.measure_width() as usize).max(24).max(track.width)
|
|
} else {
|
|
track.width
|
|
} as u16;
|
|
let y = if self.selection().scene() == Some(scene_index)
|
|
&& let Some(editor) = self.editor ()
|
|
{
|
|
(editor.measure_height() as usize).max(12)
|
|
} else {
|
|
Self::H_SCENE as usize
|
|
} as u16;
|
|
|
|
to.place(&Fixed::XY(w, y, Bsp::b(
|
|
Fill::XY(Outer(true, Style::default().fg(outline))),
|
|
Fill::XY(Bsp::b(
|
|
Bsp::b(
|
|
Tui::fg_bg(outline, bg, Fill::XY("")),
|
|
Fill::XY(Align::nw(Tui::fg_bg(fg, bg, Tui::bold(true, name)))),
|
|
),
|
|
Fill::XY(When::new(self.selection().track() == Some(track_index)
|
|
&& self.selection().scene() == Some(scene_index)
|
|
&& self.is_editing(), self.editor())))))));
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll + Measured<TuiOut> {
|
|
|
|
fn tracks_width_available (&self) -> u16 {
|
|
(self.measure_width() as u16).saturating_sub(40)
|
|
}
|
|
|
|
/// Iterate over tracks with their corresponding sizes.
|
|
fn tracks_with_sizes (&self) -> impl TracksSizes<'_> {
|
|
let _editor_width = self.editor().map(|e|e.measure_width());
|
|
let _active_track = self.selection().track();
|
|
let mut x = 0;
|
|
self.tracks().iter().enumerate().map_while(move |(index, track)|{
|
|
let width = track.width.max(8);
|
|
if x + width < self.clips_size().w() as usize {
|
|
let data = (index, track, x, x + width);
|
|
x += width + Self::TRACK_SPACING;
|
|
Some(data)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
}
|
|
|
|
fn view_track_names (&self, theme: ItemTheme) -> impl Content<TuiOut> {
|
|
let track_count = self.tracks().len();
|
|
let scene_count = self.scenes().len();
|
|
let selected = self.selection();
|
|
let button = Bsp::s(
|
|
button_3("t", "rack ", format!("{}{track_count}", selected.track()
|
|
.map(|track|format!("{track}/")).unwrap_or_default()), false),
|
|
button_3("s", "cene ", format!("{}{scene_count}", selected.scene()
|
|
.map(|scene|format!("{scene}/")).unwrap_or_default()), false));
|
|
let button_2 = Bsp::s(
|
|
button_2("T", "+", false),
|
|
button_2("S", "+", false));
|
|
view_track_row_section(theme, button, button_2, Tui::bg(theme.darker.rgb,
|
|
Fixed::Y(2, Thunk::new(|to: &mut TuiOut|{
|
|
for (index, track, x1, _x2) in self.tracks_with_sizes() {
|
|
to.place(&Push::X(x1 as u16, Fixed::X(track_width(index, track),
|
|
Tui::bg(if selected.track() == Some(index) {
|
|
track.color.light.rgb
|
|
} else {
|
|
track.color.base.rgb
|
|
}, Bsp::s(Fill::X(Align::nw(Bsp::e(
|
|
format!("·t{index:02} "),
|
|
Tui::fg(Rgb(255, 255, 255), Tui::bold(true, &track.name))
|
|
))), ""))) ));}}))))
|
|
}
|
|
|
|
fn view_track_outputs <'a> (&'a self, theme: ItemTheme, _h: u16) -> impl Content<TuiOut> {
|
|
view_track_row_section(theme,
|
|
Bsp::s(Fill::X(Align::w(button_2("o", "utput", false))),
|
|
Thunk::new(|to: &mut TuiOut|for port in self.midi_outs().iter() {
|
|
to.place(&Fill::X(Align::w(port.port_name())));
|
|
})),
|
|
button_2("O", "+", false),
|
|
Tui::bg(theme.darker.rgb, Align::w(Thunk::new(|to: &mut TuiOut|{
|
|
for (index, track, _x1, _x2) in self.tracks_with_sizes() {
|
|
to.place(&Fixed::X(track_width(index, track),
|
|
Align::nw(Fill::Y(Map::south(1, ||track.sequencer.midi_outs.iter(),
|
|
|port, index|Tui::fg(Rgb(255, 255, 255),
|
|
Fixed::Y(1, Tui::bg(track.color.dark.rgb, Fill::X(Align::w(
|
|
format!("·o{index:02} {}", port.port_name())))))))))));}}))))
|
|
}
|
|
|
|
fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Content<TuiOut> {
|
|
let mut h = 0u16;
|
|
for track in self.tracks().iter() {
|
|
h = h.max(track.sequencer.midi_ins.len() as u16);
|
|
}
|
|
let content = Thunk::new(move|to: &mut TuiOut|for (index, track, _x1, _x2) in self.tracks_with_sizes() {
|
|
to.place(&Fixed::XY(track_width(index, track), h + 1,
|
|
Align::nw(Bsp::s(
|
|
Tui::bg(track.color.base.rgb,
|
|
Fill::X(Align::w(row!(
|
|
Either::new(track.sequencer.monitoring, Tui::fg(Green, "●mon "), "·mon "),
|
|
Either::new(track.sequencer.recording, Tui::fg(Red, "●rec "), "·rec "),
|
|
Either::new(track.sequencer.overdub, Tui::fg(Yellow, "●dub "), "·dub "),
|
|
)))),
|
|
Map::south(1, ||track.sequencer.midi_ins.iter(),
|
|
|port, index|Tui::fg_bg(Rgb(255, 255, 255), track.color.dark.rgb,
|
|
Fill::X(Align::w(format!("·i{index:02} {}", port.port_name())))))))));
|
|
});
|
|
view_track_row_section(theme, button_2("i", "nput", false), button_2("I", "+", false),
|
|
Tui::bg(theme.darker.rgb, Align::w(content)))
|
|
}
|
|
|
|
}
|
|
|
|
pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + Send + Sync {
|
|
/// Default scene height.
|
|
const H_SCENE: usize = 2;
|
|
/// Default editor height.
|
|
const H_EDITOR: usize = 15;
|
|
fn h_scenes (&self) -> u16;
|
|
fn w_side (&self) -> u16;
|
|
fn w_mid (&self) -> u16;
|
|
fn scenes_with_sizes (&self) -> impl ScenesSizes<'_> {
|
|
let mut y = 0;
|
|
self.scenes().iter().enumerate().skip(self.scene_scroll()).map_while(move|(s, scene)|{
|
|
let height = if self.selection().scene() == Some(s) && self.editor().is_some() {
|
|
8
|
|
} else {
|
|
Self::H_SCENE
|
|
};
|
|
if y + height <= self.clips_size().h() as usize {
|
|
let data = (s, scene, y, y + height);
|
|
y += height;
|
|
Some(data)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
}
|
|
|
|
fn view_scenes_names (&self) -> impl Content<TuiOut> {
|
|
Fixed::X(20, Thunk::new(|to: &mut TuiOut|for (index, scene, ..) in self.scenes_with_sizes() {
|
|
to.place(&self.view_scene_name(index, scene));
|
|
}))
|
|
}
|
|
|
|
fn view_scene_name <'a> (&'a self, index: usize, scene: &'a Scene) -> impl Content<TuiOut> + 'a {
|
|
let h = if self.selection().scene() == Some(index) && let Some(_editor) = self.editor() {
|
|
7
|
|
} else {
|
|
Self::H_SCENE as u16
|
|
};
|
|
let bg = if self.selection().scene() == Some(index) {
|
|
scene.color.light.rgb
|
|
} else {
|
|
scene.color.base.rgb
|
|
};
|
|
let a = Fill::X(Align::w(Bsp::e(format!("·s{index:02} "),
|
|
Tui::fg(Tui::g(255), Tui::bold(true, &scene.name)))));
|
|
let b = When::new(self.selection().scene() == Some(index) && self.is_editing(),
|
|
Fill::XY(Align::nw(Bsp::s(
|
|
self.editor().as_ref().map(|e|e.clip_status()),
|
|
self.editor().as_ref().map(|e|e.edit_status())))));
|
|
Fixed::XY(20, h, Tui::bg(bg, Align::nw(Bsp::s(a, b))))
|
|
}
|
|
|
|
}
|
|
|
|
/// 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
|
|
}))
|
|
}
|
|
|
|
}
|