mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-07 12:16:42 +01:00
refactor some of the larger modules
This commit is contained in:
parent
417b097c6f
commit
1261b07aa2
24 changed files with 1070 additions and 1050 deletions
|
|
@ -1,8 +1,5 @@
|
||||||
mod jack; pub(crate) use self::jack::*;
|
|
||||||
mod phrase; pub(crate) use phrase::*;
|
mod phrase; pub(crate) use phrase::*;
|
||||||
mod clock; pub(crate) use clock::*;
|
mod clock; pub(crate) use clock::*;
|
||||||
mod note; pub(crate) use note::*;
|
|
||||||
mod player; pub(crate) use player::*;
|
|
||||||
mod scene; pub(crate) use scene::*;
|
mod scene; pub(crate) use scene::*;
|
||||||
mod track; pub(crate) use track::*;
|
mod track; pub(crate) use track::*;
|
||||||
mod sampler; pub(crate) use sampler::*;
|
mod sampler; pub(crate) use sampler::*;
|
||||||
|
|
|
||||||
|
|
@ -1,110 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
use Ordering::Relaxed;
|
|
||||||
|
|
||||||
pub trait MidiViewport<E: Engine>: MidiRange + MidiPoint + HasSize<E> {
|
|
||||||
/// Make sure cursor is within range
|
|
||||||
fn autoscroll (&self) {
|
|
||||||
let note_lo = self.note_lo();
|
|
||||||
let note_axis = self.note_axis();
|
|
||||||
let note_hi = self.note_hi();
|
|
||||||
let note_point = self.note_point().min(127);
|
|
||||||
if note_point < note_lo {
|
|
||||||
self.set_note_lo(note_point);
|
|
||||||
} else if note_point > note_hi {
|
|
||||||
self.set_note_lo((note_lo + note_point).saturating_sub(note_hi));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Make sure best usage of screen space is achieved by default
|
|
||||||
fn autozoom (&self) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct MidiRangeModel {
|
|
||||||
/// Length of visible time axis
|
|
||||||
pub time_axis: Arc<AtomicUsize>,
|
|
||||||
/// Earliest time displayed
|
|
||||||
pub time_start: Arc<AtomicUsize>,
|
|
||||||
/// Time step
|
|
||||||
pub time_zoom: Arc<AtomicUsize>,
|
|
||||||
/// Auto rezoom to fit in time axis
|
|
||||||
pub time_lock: Arc<AtomicBool>,
|
|
||||||
/// Length of visible note axis
|
|
||||||
pub note_axis: Arc<AtomicUsize>,
|
|
||||||
// Lowest note displayed
|
|
||||||
pub note_lo: Arc<AtomicUsize>,
|
|
||||||
}
|
|
||||||
impl From<(usize, bool)> for MidiRangeModel {
|
|
||||||
fn from ((time_zoom, time_lock): (usize, bool)) -> Self {
|
|
||||||
Self {
|
|
||||||
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(time_zoom.into()),
|
|
||||||
time_lock: Arc::new(time_lock.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub trait MidiRange {
|
|
||||||
fn time_zoom (&self) -> usize;
|
|
||||||
fn set_time_zoom (&mut self, x: usize);
|
|
||||||
fn time_lock (&self) -> bool;
|
|
||||||
fn set_time_lock (&self, x: bool);
|
|
||||||
fn time_start (&self) -> usize;
|
|
||||||
fn set_time_start (&self, x: usize);
|
|
||||||
fn note_lo (&self) -> usize;
|
|
||||||
fn set_note_lo (&self, x: usize);
|
|
||||||
fn note_axis (&self) -> usize;
|
|
||||||
fn time_axis (&self) -> usize;
|
|
||||||
fn note_hi (&self) -> usize { (self.note_lo() + self.note_axis().saturating_sub(1)).min(127) }
|
|
||||||
fn time_end (&self) -> usize { self.time_start() + self.time_axis() * self.time_zoom() }
|
|
||||||
}
|
|
||||||
impl MidiRange for MidiRangeModel {
|
|
||||||
fn time_zoom (&self) -> usize { self.time_zoom.load(Relaxed) }
|
|
||||||
fn set_time_zoom (&mut self, x: usize) { self.time_zoom.store(x, Relaxed); }
|
|
||||||
fn time_lock (&self) -> bool { self.time_lock.load(Relaxed) }
|
|
||||||
fn set_time_lock (&self, x: bool) { self.time_lock.store(x, Relaxed); }
|
|
||||||
fn time_start (&self) -> usize { self.time_start.load(Relaxed) }
|
|
||||||
fn set_time_start (&self, x: usize) { self.time_start.store(x, Relaxed); }
|
|
||||||
fn note_lo (&self) -> usize { self.note_lo.load(Relaxed).min(127) }
|
|
||||||
fn set_note_lo (&self, x: usize) { self.note_lo.store(x.min(127), Relaxed); }
|
|
||||||
fn note_axis (&self) -> usize { self.note_axis.load(Relaxed) }
|
|
||||||
fn time_axis (&self) -> usize { self.time_axis.load(Relaxed) }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct MidiPointModel {
|
|
||||||
/// Time coordinate of cursor
|
|
||||||
pub time_point: Arc<AtomicUsize>,
|
|
||||||
/// Note coordinate of cursor
|
|
||||||
pub note_point: Arc<AtomicUsize>,
|
|
||||||
/// Length of note that will be inserted, in pulses
|
|
||||||
pub note_len: Arc<AtomicUsize>,
|
|
||||||
}
|
|
||||||
impl Default for MidiPointModel {
|
|
||||||
fn default () -> Self {
|
|
||||||
Self {
|
|
||||||
time_point: Arc::new(0.into()),
|
|
||||||
note_point: Arc::new(36.into()),
|
|
||||||
note_len: Arc::new(24.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub trait MidiPoint {
|
|
||||||
fn note_len (&self) -> usize;
|
|
||||||
fn set_note_len (&self, x: usize);
|
|
||||||
fn note_point (&self) -> usize;
|
|
||||||
fn set_note_point (&self, x: usize);
|
|
||||||
fn time_point (&self) -> usize;
|
|
||||||
fn set_time_point (&self, x: usize);
|
|
||||||
fn note_end (&self) -> usize { self.note_point() + self.note_len() }
|
|
||||||
}
|
|
||||||
impl MidiPoint for MidiPointModel {
|
|
||||||
fn note_len (&self) -> usize { self.note_len.load(Relaxed)}
|
|
||||||
fn set_note_len (&self, x: usize) { self.note_len.store(x, Relaxed) }
|
|
||||||
fn note_point (&self) -> usize { self.note_point.load(Relaxed).min(127) }
|
|
||||||
fn set_note_point (&self, x: usize) { self.note_point.store(x.min(127), Relaxed) }
|
|
||||||
fn time_point (&self) -> usize { self.time_point.load(Relaxed) }
|
|
||||||
fn set_time_point (&self, x: usize) { self.time_point.store(x, Relaxed) }
|
|
||||||
}
|
|
||||||
|
|
@ -1,523 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
pub trait HasPlayer {
|
|
||||||
fn player (&self) -> &impl MidiPlayerApi;
|
|
||||||
fn player_mut (&mut self) -> &mut impl MidiPlayerApi;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export] macro_rules! has_player {
|
|
||||||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
|
||||||
impl $(<$($L),*$($T $(: $U)?),*>)? HasPlayer for $Struct $(<$($L),*$($T),*>)? {
|
|
||||||
fn player (&$self) -> &impl MidiPlayerApi { &$cb }
|
|
||||||
fn player_mut (&mut $self) -> &mut impl MidiPlayerApi { &mut$cb }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Contains state for playing a phrase
|
|
||||||
pub struct PhrasePlayerModel {
|
|
||||||
/// State of clock and playhead
|
|
||||||
pub(crate) clock: ClockModel,
|
|
||||||
/// Start time and phrase being played
|
|
||||||
pub(crate) play_phrase: Option<(Moment, Option<Arc<RwLock<Phrase>>>)>,
|
|
||||||
/// Start time and next phrase
|
|
||||||
pub(crate) next_phrase: Option<(Moment, Option<Arc<RwLock<Phrase>>>)>,
|
|
||||||
/// Play input through output.
|
|
||||||
pub(crate) monitoring: bool,
|
|
||||||
/// Write input to sequence.
|
|
||||||
pub(crate) recording: bool,
|
|
||||||
/// Overdub input to sequence.
|
|
||||||
pub(crate) overdub: bool,
|
|
||||||
/// Send all notes off
|
|
||||||
pub(crate) reset: bool, // TODO?: after Some(nframes)
|
|
||||||
/// Record from MIDI ports to current sequence.
|
|
||||||
pub midi_ins: Vec<Port<MidiIn>>,
|
|
||||||
/// Play from current sequence to MIDI ports
|
|
||||||
pub midi_outs: Vec<Port<MidiOut>>,
|
|
||||||
/// Notes currently held at input
|
|
||||||
pub(crate) notes_in: Arc<RwLock<[bool; 128]>>,
|
|
||||||
/// Notes currently held at output
|
|
||||||
pub(crate) notes_out: Arc<RwLock<[bool; 128]>>,
|
|
||||||
/// MIDI output buffer
|
|
||||||
pub note_buf: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Debug for PhrasePlayerModel {
|
|
||||||
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
|
||||||
f.debug_struct("PhrasePlayerModel")
|
|
||||||
.field("clock", &self.clock)
|
|
||||||
.field("play_phrase", &self.play_phrase)
|
|
||||||
.field("next_phrase", &self.next_phrase)
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&ClockModel> for PhrasePlayerModel {
|
|
||||||
fn from (clock: &ClockModel) -> Self {
|
|
||||||
Self {
|
|
||||||
clock: clock.clone(),
|
|
||||||
midi_ins: vec![],
|
|
||||||
midi_outs: vec![],
|
|
||||||
note_buf: vec![0;8],
|
|
||||||
reset: true,
|
|
||||||
recording: false,
|
|
||||||
monitoring: false,
|
|
||||||
overdub: false,
|
|
||||||
play_phrase: None,
|
|
||||||
next_phrase: None,
|
|
||||||
notes_in: RwLock::new([false;128]).into(),
|
|
||||||
notes_out: RwLock::new([false;128]).into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<(&ClockModel, &Arc<RwLock<Phrase>>)> for PhrasePlayerModel {
|
|
||||||
fn from ((clock, phrase): (&ClockModel, &Arc<RwLock<Phrase>>)) -> Self {
|
|
||||||
let mut model = Self::from(clock);
|
|
||||||
model.play_phrase = Some((Moment::zero(&clock.timebase), Some(phrase.clone())));
|
|
||||||
model
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
has_clock!(|self:PhrasePlayerModel|&self.clock);
|
|
||||||
|
|
||||||
impl HasMidiIns for PhrasePlayerModel {
|
|
||||||
fn midi_ins (&self) -> &Vec<Port<jack::MidiIn>> {
|
|
||||||
&self.midi_ins
|
|
||||||
}
|
|
||||||
fn midi_ins_mut (&mut self) -> &mut Vec<Port<jack::MidiIn>> {
|
|
||||||
&mut self.midi_ins
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HasMidiOuts for PhrasePlayerModel {
|
|
||||||
fn midi_outs (&self) -> &Vec<Port<jack::MidiOut>> {
|
|
||||||
&self.midi_outs
|
|
||||||
}
|
|
||||||
fn midi_outs_mut (&mut self) -> &mut Vec<Port<jack::MidiOut>> {
|
|
||||||
&mut self.midi_outs
|
|
||||||
}
|
|
||||||
fn midi_note (&mut self) -> &mut Vec<u8> {
|
|
||||||
&mut self.note_buf
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait MidiPlayerApi: MidiRecordApi + MidiPlaybackApi + Send + Sync {}
|
|
||||||
|
|
||||||
impl MidiPlayerApi for PhrasePlayerModel {}
|
|
||||||
|
|
||||||
pub trait MidiRecordApi: HasClock + HasPlayPhrase + HasMidiIns {
|
|
||||||
fn notes_in (&self) -> &Arc<RwLock<[bool;128]>>;
|
|
||||||
|
|
||||||
fn recording (&self) -> bool;
|
|
||||||
fn recording_mut (&mut self) -> &mut bool;
|
|
||||||
fn toggle_record (&mut self) {
|
|
||||||
*self.recording_mut() = !self.recording();
|
|
||||||
}
|
|
||||||
fn record (&mut self, scope: &ProcessScope, midi_buf: &mut Vec<Vec<Vec<u8>>>) {
|
|
||||||
let sample0 = scope.last_frame_time() as usize;
|
|
||||||
// For highlighting keys and note repeat
|
|
||||||
let notes_in = self.notes_in().clone();
|
|
||||||
if self.clock().is_rolling() {
|
|
||||||
if let Some((started, ref phrase)) = self.play_phrase().clone() {
|
|
||||||
let start = started.sample.get() as usize;
|
|
||||||
let quant = self.clock().quant.get();
|
|
||||||
let timebase = self.clock().timebase().clone();
|
|
||||||
let monitoring = self.monitoring();
|
|
||||||
let recording = self.recording();
|
|
||||||
for input in self.midi_ins_mut().iter() {
|
|
||||||
for (sample, event, bytes) in parse_midi_input(input.iter(scope)) {
|
|
||||||
if let LiveEvent::Midi { message, .. } = event {
|
|
||||||
if monitoring {
|
|
||||||
midi_buf[sample].push(bytes.to_vec())
|
|
||||||
}
|
|
||||||
if recording {
|
|
||||||
if let Some(phrase) = phrase {
|
|
||||||
let mut phrase = phrase.write().unwrap();
|
|
||||||
let length = phrase.length;
|
|
||||||
phrase.record_event({
|
|
||||||
let sample = (sample0 + sample - start) as f64;
|
|
||||||
let pulse = timebase.samples_to_pulse(sample);
|
|
||||||
let quantized = (pulse / quant).round() * quant;
|
|
||||||
let looped = quantized as usize % length;
|
|
||||||
looped
|
|
||||||
}, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
update_keys(&mut*notes_in.write().unwrap(), &message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some((start_at, phrase)) = &self.next_phrase() {
|
|
||||||
// TODO switch to next phrase and record into it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn monitoring (&self) -> bool;
|
|
||||||
fn monitoring_mut (&mut self) -> &mut bool;
|
|
||||||
fn toggle_monitor (&mut self) {
|
|
||||||
*self.monitoring_mut() = !self.monitoring();
|
|
||||||
}
|
|
||||||
fn monitor (&mut self, scope: &ProcessScope, midi_buf: &mut Vec<Vec<Vec<u8>>>) {
|
|
||||||
// For highlighting keys and note repeat
|
|
||||||
let notes_in = self.notes_in().clone();
|
|
||||||
for input in self.midi_ins_mut().iter() {
|
|
||||||
for (sample, event, bytes) in parse_midi_input(input.iter(scope)) {
|
|
||||||
if let LiveEvent::Midi { message, .. } = event {
|
|
||||||
midi_buf[sample].push(bytes.to_vec());
|
|
||||||
update_keys(&mut*notes_in.write().unwrap(), &message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn overdub (&self) -> bool;
|
|
||||||
fn overdub_mut (&mut self) -> &mut bool;
|
|
||||||
fn toggle_overdub (&mut self) {
|
|
||||||
*self.overdub_mut() = !self.overdub();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MidiRecordApi for PhrasePlayerModel {
|
|
||||||
fn recording (&self) -> bool {
|
|
||||||
self.recording
|
|
||||||
}
|
|
||||||
fn recording_mut (&mut self) -> &mut bool {
|
|
||||||
&mut self.recording
|
|
||||||
}
|
|
||||||
fn monitoring (&self) -> bool {
|
|
||||||
self.monitoring
|
|
||||||
}
|
|
||||||
fn monitoring_mut (&mut self) -> &mut bool {
|
|
||||||
&mut self.monitoring
|
|
||||||
}
|
|
||||||
fn overdub (&self) -> bool {
|
|
||||||
self.overdub
|
|
||||||
}
|
|
||||||
fn overdub_mut (&mut self) -> &mut bool {
|
|
||||||
&mut self.overdub
|
|
||||||
}
|
|
||||||
fn notes_in (&self) -> &Arc<RwLock<[bool; 128]>> {
|
|
||||||
&self.notes_in
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait MidiPlaybackApi: HasPlayPhrase + HasClock + HasMidiOuts {
|
|
||||||
|
|
||||||
fn notes_out (&self) -> &Arc<RwLock<[bool;128]>>;
|
|
||||||
|
|
||||||
/// Clear the section of the output buffer that we will be using,
|
|
||||||
/// emitting "all notes off" at start of buffer if requested.
|
|
||||||
fn clear (
|
|
||||||
&mut self, scope: &ProcessScope, out_buf: &mut Vec<Vec<Vec<u8>>>, reset: bool
|
|
||||||
) {
|
|
||||||
for frame in &mut out_buf[0..scope.n_frames() as usize] {
|
|
||||||
frame.clear();
|
|
||||||
}
|
|
||||||
if reset {
|
|
||||||
all_notes_off(out_buf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Output notes from phrase to MIDI output ports.
|
|
||||||
fn play (
|
|
||||||
&mut self, scope: &ProcessScope, note_buf: &mut Vec<u8>, out_buf: &mut Vec<Vec<Vec<u8>>>
|
|
||||||
) -> bool {
|
|
||||||
let mut next = false;
|
|
||||||
// Write MIDI events from currently playing phrase (if any) to MIDI output buffer
|
|
||||||
if self.clock().is_rolling() {
|
|
||||||
let sample0 = scope.last_frame_time() as usize;
|
|
||||||
let samples = scope.n_frames() as usize;
|
|
||||||
// If no phrase is playing, prepare for switchover immediately
|
|
||||||
next = self.play_phrase().is_none();
|
|
||||||
let phrase = self.play_phrase();
|
|
||||||
let started0 = &self.clock().started;
|
|
||||||
let timebase = self.clock().timebase();
|
|
||||||
let notes_out = self.notes_out();
|
|
||||||
let next_phrase = self.next_phrase();
|
|
||||||
if let Some((started, phrase)) = phrase {
|
|
||||||
// First sample to populate. Greater than 0 means that the first
|
|
||||||
// pulse of the phrase falls somewhere in the middle of the chunk.
|
|
||||||
let sample = started.sample.get() as usize;
|
|
||||||
let sample = sample + started0.read().unwrap().as_ref().unwrap().sample.get() as usize;
|
|
||||||
let sample = sample0.saturating_sub(sample);
|
|
||||||
// Iterator that emits sample (index into output buffer at which to write MIDI event)
|
|
||||||
// paired with pulse (index into phrase from which to take the MIDI event) for each
|
|
||||||
// sample of the output buffer that corresponds to a MIDI pulse.
|
|
||||||
let pulses = timebase.pulses_between_samples(sample, sample + samples);
|
|
||||||
// Notes active during current chunk.
|
|
||||||
let notes = &mut notes_out.write().unwrap();
|
|
||||||
for (sample, pulse) in pulses {
|
|
||||||
// If a next phrase is enqueued, and we're past the end of the current one,
|
|
||||||
// break the loop here (FIXME count pulse correctly)
|
|
||||||
next = next_phrase.is_some() && if let Some(ref phrase) = phrase {
|
|
||||||
pulse >= phrase.read().unwrap().length
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
};
|
|
||||||
if next {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
// If there's a currently playing phrase, output notes from it to buffer:
|
|
||||||
if let Some(ref phrase) = phrase {
|
|
||||||
// Source phrase from which the MIDI events will be taken.
|
|
||||||
let phrase = phrase.read().unwrap();
|
|
||||||
// Phrase with zero length is not processed
|
|
||||||
if phrase.length > 0 {
|
|
||||||
// Current pulse index in source phrase
|
|
||||||
let pulse = pulse % phrase.length;
|
|
||||||
// Output each MIDI event from phrase at appropriate frames of output buffer:
|
|
||||||
for message in phrase.notes[pulse].iter() {
|
|
||||||
// Clear output buffer for this MIDI event.
|
|
||||||
note_buf.clear();
|
|
||||||
// TODO: support MIDI channels other than CH1.
|
|
||||||
let channel = 0.into();
|
|
||||||
// Serialize MIDI event into message buffer.
|
|
||||||
LiveEvent::Midi { channel, message: *message }
|
|
||||||
.write(note_buf)
|
|
||||||
.unwrap();
|
|
||||||
// Append serialized message to output buffer.
|
|
||||||
out_buf[sample].push(note_buf.clone());
|
|
||||||
// Update the list of currently held notes.
|
|
||||||
update_keys(&mut*notes, &message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
next
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle switchover from current to next playing phrase.
|
|
||||||
fn switchover (
|
|
||||||
&mut self, scope: &ProcessScope, note_buf: &mut Vec<u8>, out_buf: &mut Vec<Vec<Vec<u8>>>
|
|
||||||
) {
|
|
||||||
if self.clock().is_rolling() {
|
|
||||||
let sample0 = scope.last_frame_time() as usize;
|
|
||||||
//let samples = scope.n_frames() as usize;
|
|
||||||
if let Some((start_at, phrase)) = &self.next_phrase() {
|
|
||||||
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 phrase:
|
|
||||||
if start <= sample0.saturating_sub(sample) {
|
|
||||||
// Samples elapsed since phrase was supposed to start
|
|
||||||
let skipped = sample0 - start;
|
|
||||||
// Switch over to enqueued phrase
|
|
||||||
let started = Moment::from_sample(&self.clock().timebase(), start as f64);
|
|
||||||
*self.play_phrase_mut() = Some((started, phrase.clone()));
|
|
||||||
// Unset enqueuement (TODO: where to implement looping?)
|
|
||||||
*self.next_phrase_mut() = None
|
|
||||||
}
|
|
||||||
// TODO fill in remaining ticks of chunk from next phrase.
|
|
||||||
// ?? just call self.play(scope) again, since enqueuement is off ???
|
|
||||||
self.play(scope, note_buf, out_buf);
|
|
||||||
// ?? or must it be with modified scope ??
|
|
||||||
// likely not because start time etc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write a chunk of MIDI notes to the output buffer.
|
|
||||||
fn write (
|
|
||||||
&mut self, scope: &ProcessScope, out_buf: &Vec<Vec<Vec<u8>>>
|
|
||||||
) {
|
|
||||||
let samples = scope.n_frames() as usize;
|
|
||||||
for port in self.midi_outs_mut().iter_mut() {
|
|
||||||
let writer = &mut port.writer(scope);
|
|
||||||
for time in 0..samples {
|
|
||||||
for event in out_buf[time].iter() {
|
|
||||||
writer.write(&RawMidi { time: time as u32, bytes: &event })
|
|
||||||
.expect(&format!("{event:?}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MidiPlaybackApi for PhrasePlayerModel {
|
|
||||||
fn notes_out (&self) -> &Arc<RwLock<[bool; 128]>> {
|
|
||||||
&self.notes_in
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait HasPlayPhrase: HasClock {
|
|
||||||
fn reset (&self) -> bool;
|
|
||||||
fn reset_mut (&mut self) -> &mut bool;
|
|
||||||
fn play_phrase (&self) -> &Option<(Moment, Option<Arc<RwLock<Phrase>>>)>;
|
|
||||||
fn play_phrase_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<Phrase>>>)>;
|
|
||||||
fn next_phrase (&self) -> &Option<(Moment, Option<Arc<RwLock<Phrase>>>)>;
|
|
||||||
fn next_phrase_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<Phrase>>>)>;
|
|
||||||
fn pulses_since_start (&self) -> Option<f64> {
|
|
||||||
if let Some((started, Some(_))) = self.play_phrase().as_ref() {
|
|
||||||
let elapsed = self.clock().playhead.pulse.get() - started.pulse.get();
|
|
||||||
Some(elapsed)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn pulses_since_start_looped (&self) -> Option<f64> {
|
|
||||||
if let Some((started, Some(phrase))) = self.play_phrase().as_ref() {
|
|
||||||
let elapsed = self.clock().playhead.pulse.get() - started.pulse.get();
|
|
||||||
let length = phrase.read().unwrap().length.max(1); // prevent div0 on empty phrase
|
|
||||||
let elapsed = (elapsed as usize % length) as f64;
|
|
||||||
Some(elapsed)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn enqueue_next (&mut self, phrase: Option<&Arc<RwLock<Phrase>>>) {
|
|
||||||
let start = self.clock().next_launch_pulse() as f64;
|
|
||||||
let instant = Moment::from_pulse(&self.clock().timebase(), start);
|
|
||||||
let phrase = phrase.map(|p|p.clone());
|
|
||||||
*self.next_phrase_mut() = Some((instant, phrase));
|
|
||||||
*self.reset_mut() = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HasPlayPhrase for PhrasePlayerModel {
|
|
||||||
fn reset (&self) -> bool {
|
|
||||||
self.reset
|
|
||||||
}
|
|
||||||
fn reset_mut (&mut self) -> &mut bool {
|
|
||||||
&mut self.reset
|
|
||||||
}
|
|
||||||
fn play_phrase (&self) -> &Option<(Moment, Option<Arc<RwLock<Phrase>>>)> {
|
|
||||||
&self.play_phrase
|
|
||||||
}
|
|
||||||
fn play_phrase_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<Phrase>>>)> {
|
|
||||||
&mut self.play_phrase
|
|
||||||
}
|
|
||||||
fn next_phrase (&self) -> &Option<(Moment, Option<Arc<RwLock<Phrase>>>)> {
|
|
||||||
&self.next_phrase
|
|
||||||
}
|
|
||||||
fn next_phrase_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<Phrase>>>)> {
|
|
||||||
&mut self.next_phrase
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add "all notes off" to the start of a buffer.
|
|
||||||
pub fn all_notes_off (output: &mut [Vec<Vec<u8>>]) {
|
|
||||||
let mut buf = vec![];
|
|
||||||
let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() };
|
|
||||||
let evt = LiveEvent::Midi { channel: 0.into(), message: msg };
|
|
||||||
evt.write(&mut buf).unwrap();
|
|
||||||
output[0].push(buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return boxed iterator of MIDI events
|
|
||||||
pub fn parse_midi_input (input: MidiIter) -> Box<dyn Iterator<Item=(usize, LiveEvent, &[u8])> + '_> {
|
|
||||||
Box::new(input.map(|RawMidi { time, bytes }|(
|
|
||||||
time as usize,
|
|
||||||
LiveEvent::parse(bytes).unwrap(),
|
|
||||||
bytes
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update notes_in array
|
|
||||||
pub fn update_keys (keys: &mut[bool;128], message: &MidiMessage) {
|
|
||||||
match message {
|
|
||||||
MidiMessage::NoteOn { key, .. } => { keys[key.as_int() as usize] = true; }
|
|
||||||
MidiMessage::NoteOff { key, .. } => { keys[key.as_int() as usize] = false; },
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Hosts the JACK callback for a single MIDI player
|
|
||||||
pub struct PlayerAudio<'a, T: MidiPlayerApi>(
|
|
||||||
/// Player
|
|
||||||
pub &'a mut T,
|
|
||||||
/// Note buffer
|
|
||||||
pub &'a mut Vec<u8>,
|
|
||||||
/// Note chunk buffer
|
|
||||||
pub &'a mut Vec<Vec<Vec<u8>>>,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// JACK process callback for a sequencer's phrase player/recorder.
|
|
||||||
impl<'a, T: MidiPlayerApi> Audio for PlayerAudio<'a, T> {
|
|
||||||
fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
|
|
||||||
let model = &mut self.0;
|
|
||||||
let note_buf = &mut self.1;
|
|
||||||
let midi_buf = &mut self.2;
|
|
||||||
// Clear output buffer(s)
|
|
||||||
model.clear(scope, midi_buf, false);
|
|
||||||
// Write chunk of phrase to output, handle switchover
|
|
||||||
if model.play(scope, note_buf, midi_buf) {
|
|
||||||
model.switchover(scope, note_buf, midi_buf);
|
|
||||||
}
|
|
||||||
if model.has_midi_ins() {
|
|
||||||
if model.recording() || model.monitoring() {
|
|
||||||
// Record and/or monitor input
|
|
||||||
model.record(scope, midi_buf)
|
|
||||||
} else if model.has_midi_outs() && model.monitoring() {
|
|
||||||
// Monitor input to output
|
|
||||||
model.monitor(scope, midi_buf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Write to output port(s)
|
|
||||||
model.write(scope, midi_buf);
|
|
||||||
Control::Continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//#[derive(Debug)]
|
|
||||||
//pub struct MIDIPlayer {
|
|
||||||
///// Global timebase
|
|
||||||
//pub clock: Arc<Clock>,
|
|
||||||
///// Start time and phrase being played
|
|
||||||
//pub play_phrase: Option<(Moment, Option<Arc<RwLock<Phrase>>>)>,
|
|
||||||
///// Start time and next phrase
|
|
||||||
//pub next_phrase: Option<(Moment, Option<Arc<RwLock<Phrase>>>)>,
|
|
||||||
///// Play input through output.
|
|
||||||
//pub monitoring: bool,
|
|
||||||
///// Write input to sequence.
|
|
||||||
//pub recording: bool,
|
|
||||||
///// Overdub input to sequence.
|
|
||||||
//pub overdub: bool,
|
|
||||||
///// Send all notes off
|
|
||||||
//pub reset: bool, // TODO?: after Some(nframes)
|
|
||||||
///// Record from MIDI ports to current sequence.
|
|
||||||
//pub midi_inputs: Vec<Port<MidiIn>>,
|
|
||||||
///// Play from current sequence to MIDI ports
|
|
||||||
//pub midi_outputs: Vec<Port<MidiOut>>,
|
|
||||||
///// MIDI output buffer
|
|
||||||
//pub midi_note: Vec<u8>,
|
|
||||||
///// MIDI output buffer
|
|
||||||
//pub midi_chunk: Vec<Vec<Vec<u8>>>,
|
|
||||||
///// Notes currently held at input
|
|
||||||
//pub notes_in: Arc<RwLock<[bool; 128]>>,
|
|
||||||
///// Notes currently held at output
|
|
||||||
//pub notes_out: Arc<RwLock<[bool; 128]>>,
|
|
||||||
//}
|
|
||||||
|
|
||||||
///// Methods used primarily by the process callback
|
|
||||||
//impl MIDIPlayer {
|
|
||||||
//pub fn new (
|
|
||||||
//jack: &Arc<RwLock<JackClient>>,
|
|
||||||
//clock: &Arc<Clock>,
|
|
||||||
//name: &str
|
|
||||||
//) -> Usually<Self> {
|
|
||||||
//let jack = jack.read().unwrap();
|
|
||||||
//Ok(Self {
|
|
||||||
//clock: clock.clone(),
|
|
||||||
//phrase: None,
|
|
||||||
//next_phrase: None,
|
|
||||||
//notes_in: Arc::new(RwLock::new([false;128])),
|
|
||||||
//notes_out: Arc::new(RwLock::new([false;128])),
|
|
||||||
//monitoring: false,
|
|
||||||
//recording: false,
|
|
||||||
//overdub: true,
|
|
||||||
//reset: true,
|
|
||||||
//midi_note: Vec::with_capacity(8),
|
|
||||||
//midi_chunk: vec![Vec::with_capacity(16);16384],
|
|
||||||
//midi_outputs: vec![
|
|
||||||
//jack.client().register_port(format!("{name}_out0").as_str(), MidiOut::default())?
|
|
||||||
//],
|
|
||||||
//midi_inputs: vec![
|
|
||||||
//jack.client().register_port(format!("{name}_in0").as_str(), MidiIn::default())?
|
|
||||||
//],
|
|
||||||
//})
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
pub(crate) mod audio; pub(crate) use audio::*;
|
|
||||||
pub(crate) mod color; pub(crate) use color::*;
|
pub(crate) mod color; pub(crate) use color::*;
|
||||||
pub(crate) mod command; pub(crate) use command::*;
|
pub(crate) mod command; pub(crate) use command::*;
|
||||||
pub(crate) mod engine; pub(crate) use engine::*;
|
pub(crate) mod engine; pub(crate) use engine::*;
|
||||||
|
|
|
||||||
|
|
@ -1,181 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
use jack::*;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
/// Event enum for JACK events.
|
|
||||||
pub enum JackEvent {
|
|
||||||
ThreadInit,
|
|
||||||
Shutdown(ClientStatus, String),
|
|
||||||
Freewheel(bool),
|
|
||||||
SampleRate(Frames),
|
|
||||||
ClientRegistration(String, bool),
|
|
||||||
PortRegistration(PortId, bool),
|
|
||||||
PortRename(PortId, String, String),
|
|
||||||
PortsConnected(PortId, PortId, bool),
|
|
||||||
GraphReorder,
|
|
||||||
XRun,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wraps [Client] or [DynamicAsyncClient] in place.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum JackClient {
|
|
||||||
/// Before activation.
|
|
||||||
Inactive(Client),
|
|
||||||
/// During activation.
|
|
||||||
Activating,
|
|
||||||
/// After activation. Must not be dropped for JACK thread to persist.
|
|
||||||
Active(DynamicAsyncClient),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Trait for things that wrap a JACK client.
|
|
||||||
pub trait AudioEngine {
|
|
||||||
|
|
||||||
fn transport (&self) -> Transport {
|
|
||||||
self.client().transport()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn port_by_name (&self, name: &str) -> Option<Port<Unowned>> {
|
|
||||||
self.client().port_by_name(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn register_port <PS: PortSpec> (&self, name: &str, spec: PS) -> Usually<Port<PS>> {
|
|
||||||
Ok(self.client().register_port(name, spec)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn client (&self) -> &Client;
|
|
||||||
|
|
||||||
fn activate (
|
|
||||||
self,
|
|
||||||
process: impl FnMut(&Arc<RwLock<Self>>, &Client, &ProcessScope) -> Control + Send + 'static
|
|
||||||
) -> Usually<Arc<RwLock<Self>>> where Self: Send + Sync + 'static;
|
|
||||||
|
|
||||||
fn thread_init (&self, _: &Client) {}
|
|
||||||
|
|
||||||
unsafe fn shutdown (&mut self, _status: ClientStatus, _reason: &str) {}
|
|
||||||
|
|
||||||
fn freewheel (&mut self, _: &Client, _enabled: bool) {}
|
|
||||||
|
|
||||||
fn client_registration (&mut self, _: &Client, _name: &str, _reg: bool) {}
|
|
||||||
|
|
||||||
fn port_registration (&mut self, _: &Client, _id: PortId, _reg: bool) {}
|
|
||||||
|
|
||||||
fn ports_connected (&mut self, _: &Client, _a: PortId, _b: PortId, _are: bool) {}
|
|
||||||
|
|
||||||
fn sample_rate (&mut self, _: &Client, _frames: Frames) -> Control {
|
|
||||||
Control::Continue
|
|
||||||
}
|
|
||||||
|
|
||||||
fn port_rename (&mut self, _: &Client, _id: PortId, _old: &str, _new: &str) -> Control {
|
|
||||||
Control::Continue
|
|
||||||
}
|
|
||||||
|
|
||||||
fn graph_reorder (&mut self, _: &Client) -> Control {
|
|
||||||
Control::Continue
|
|
||||||
}
|
|
||||||
|
|
||||||
fn xrun (&mut self, _: &Client) -> Control {
|
|
||||||
Control::Continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AudioEngine for JackClient {
|
|
||||||
fn client(&self) -> &Client {
|
|
||||||
match self {
|
|
||||||
Self::Inactive(ref client) => client,
|
|
||||||
Self::Activating => panic!("jack client has not finished activation"),
|
|
||||||
Self::Active(ref client) => client.as_client(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn activate(
|
|
||||||
self,
|
|
||||||
mut cb: impl FnMut(&Arc<RwLock<Self>>, &Client, &ProcessScope) -> Control + Send + 'static,
|
|
||||||
) -> Usually<Arc<RwLock<Self>>>
|
|
||||||
where
|
|
||||||
Self: Send + Sync + 'static
|
|
||||||
{
|
|
||||||
let client = Client::from(self);
|
|
||||||
let state = Arc::new(RwLock::new(Self::Activating));
|
|
||||||
let event = Box::new(move|_|{/*TODO*/}) as Box<dyn Fn(JackEvent) + Send + Sync>;
|
|
||||||
let events = Notifications(event);
|
|
||||||
let frame = Box::new({let state = state.clone(); move|c: &_, s: &_|cb(&state, c, s)});
|
|
||||||
let frames = contrib::ClosureProcessHandler::new(frame as BoxedAudioHandler);
|
|
||||||
*state.write().unwrap() = Self::Active(client.activate_async(events, frames)?);
|
|
||||||
Ok(state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type DynamicAsyncClient = AsyncClient<DynamicNotifications, DynamicAudioHandler>;
|
|
||||||
|
|
||||||
pub type DynamicAudioHandler = contrib::ClosureProcessHandler<(), BoxedAudioHandler>;
|
|
||||||
|
|
||||||
pub type BoxedAudioHandler = Box<dyn FnMut(&Client, &ProcessScope) -> Control + Send>;
|
|
||||||
|
|
||||||
impl JackClient {
|
|
||||||
pub fn new (name: &str) -> Usually<Self> {
|
|
||||||
let (client, _) = Client::new(name, ClientOptions::NO_START_SERVER)?;
|
|
||||||
Ok(Self::Inactive(client))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<JackClient> for Client {
|
|
||||||
fn from (jack: JackClient) -> Client {
|
|
||||||
match jack {
|
|
||||||
JackClient::Inactive(client) => client,
|
|
||||||
JackClient::Activating => panic!("jack client still activating"),
|
|
||||||
JackClient::Active(_) => panic!("jack client already activated"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Notification handler used by the [Jack] factory
|
|
||||||
/// when constructing [JackDevice]s.
|
|
||||||
pub type DynamicNotifications = Notifications<Box<dyn Fn(JackEvent) + Send + Sync>>;
|
|
||||||
|
|
||||||
/// Generic notification handler that emits [JackEvent]
|
|
||||||
pub struct Notifications<T: Fn(JackEvent) + Send>(pub T);
|
|
||||||
|
|
||||||
impl<T: Fn(JackEvent) + Send> NotificationHandler for Notifications<T> {
|
|
||||||
fn thread_init(&self, _: &Client) {
|
|
||||||
self.0(JackEvent::ThreadInit);
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe fn shutdown(&mut self, status: ClientStatus, reason: &str) {
|
|
||||||
self.0(JackEvent::Shutdown(status, reason.into()));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn freewheel(&mut self, _: &Client, enabled: bool) {
|
|
||||||
self.0(JackEvent::Freewheel(enabled));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sample_rate(&mut self, _: &Client, frames: Frames) -> Control {
|
|
||||||
self.0(JackEvent::SampleRate(frames));
|
|
||||||
Control::Quit
|
|
||||||
}
|
|
||||||
|
|
||||||
fn client_registration(&mut self, _: &Client, name: &str, reg: bool) {
|
|
||||||
self.0(JackEvent::ClientRegistration(name.into(), reg));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn port_registration(&mut self, _: &Client, id: PortId, reg: bool) {
|
|
||||||
self.0(JackEvent::PortRegistration(id, reg));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn port_rename(&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control {
|
|
||||||
self.0(JackEvent::PortRename(id, old.into(), new.into()));
|
|
||||||
Control::Continue
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ports_connected(&mut self, _: &Client, a: PortId, b: PortId, are: bool) {
|
|
||||||
self.0(JackEvent::PortsConnected(a, b, are));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn graph_reorder(&mut self, _: &Client) -> Control {
|
|
||||||
self.0(JackEvent::GraphReorder);
|
|
||||||
Control::Continue
|
|
||||||
}
|
|
||||||
|
|
||||||
fn xrun(&mut self, _: &Client) -> Control {
|
|
||||||
self.0(JackEvent::XRun);
|
|
||||||
Control::Continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -29,7 +29,7 @@ impl PerfModel {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn update (&self, t0: Option<u64>, scope: &jack::ProcessScope) {
|
pub fn update (&self, t0: Option<u64>, scope: &ProcessScope) {
|
||||||
if let Some(t0) = t0 {
|
if let Some(t0) = t0 {
|
||||||
let t1 = self.clock.raw();
|
let t1 = self.clock.raw();
|
||||||
self.used.store(
|
self.used.store(
|
||||||
|
|
|
||||||
|
|
@ -1,151 +1,14 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
/// Implement [TryFrom<&Arc<RwLock<JackClient>>>]: create app state from wrapped JACK handle.
|
pub(crate) mod activate; pub(crate) use activate::*;
|
||||||
#[macro_export] macro_rules! from_jack {
|
pub(crate) mod audio; pub(crate) use audio::*;
|
||||||
(|$jack:ident|$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)? $cb:expr) => {
|
pub(crate) mod client; pub(crate) use client::*;
|
||||||
impl $(<$($L),*$($T $(: $U)?),*>)? TryFrom<&Arc<RwLock<JackClient>>> for $Struct $(<$($L),*$($T),*>)? {
|
pub(crate) mod from_jack; pub(crate) use from_jack::*;
|
||||||
type Error = Box<dyn std::error::Error>;
|
pub(crate) mod jack_event; pub(crate) use jack_event::*;
|
||||||
fn try_from ($jack: &Arc<RwLock<JackClient>>) -> Usually<Self> {
|
pub(crate) mod ports; pub(crate) use ports::*;
|
||||||
Ok($cb)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Trait for thing that has a JACK process callback.
|
|
||||||
pub trait Audio: Send + Sync {
|
|
||||||
fn process (&mut self, _: &Client, _: &ProcessScope) -> Control {
|
|
||||||
Control::Continue
|
|
||||||
}
|
|
||||||
fn callback (
|
|
||||||
state: &Arc<RwLock<Self>>, client: &Client, scope: &ProcessScope
|
|
||||||
) -> Control where Self: Sized {
|
|
||||||
if let Ok(mut state) = state.write() {
|
|
||||||
state.process(client, scope)
|
|
||||||
} else {
|
|
||||||
Control::Quit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Implement [Audio]: provide JACK callbacks.
|
|
||||||
#[macro_export] macro_rules! audio {
|
|
||||||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?,$c:ident,$s:ident|$cb:expr) => {
|
|
||||||
impl $(<$($L),*$($T $(: $U)?),*>)? Audio for $Struct $(<$($L),*$($T),*>)? {
|
|
||||||
#[inline] fn process (&mut $self, $c: &Client, $s: &ProcessScope) -> Control { $cb }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Trait for thing that may receive MIDI.
|
|
||||||
pub trait HasMidiIns {
|
|
||||||
fn midi_ins (&self) -> &Vec<Port<MidiIn>>;
|
|
||||||
fn midi_ins_mut (&mut self) -> &mut Vec<Port<MidiIn>>;
|
|
||||||
fn has_midi_ins (&self) -> bool {
|
|
||||||
self.midi_ins().len() > 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Trait for thing that may output MIDI.
|
|
||||||
pub trait HasMidiOuts {
|
|
||||||
fn midi_outs (&self) -> &Vec<Port<MidiOut>>;
|
|
||||||
fn midi_outs_mut (&mut self) -> &mut Vec<Port<MidiOut>>;
|
|
||||||
fn has_midi_outs (&self) -> bool {
|
|
||||||
self.midi_outs().len() > 0
|
|
||||||
}
|
|
||||||
/// Buffer for serializing a MIDI event. FIXME rename
|
|
||||||
fn midi_note (&mut self) -> &mut Vec<u8>;
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
pub trait JackActivate: Sized {
|
|
||||||
fn activate_with <T: Audio + 'static> (
|
|
||||||
self,
|
|
||||||
init: impl FnOnce(&Arc<RwLock<JackClient>>)->Usually<T>
|
|
||||||
)
|
|
||||||
-> Usually<Arc<RwLock<T>>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl JackActivate for JackClient {
|
|
||||||
fn activate_with <T: Audio + 'static> (
|
|
||||||
self,
|
|
||||||
init: impl FnOnce(&Arc<RwLock<JackClient>>)->Usually<T>
|
|
||||||
)
|
|
||||||
-> Usually<Arc<RwLock<T>>>
|
|
||||||
{
|
|
||||||
let client = Arc::new(RwLock::new(self));
|
|
||||||
let target = Arc::new(RwLock::new(init(&client)?));
|
|
||||||
let event = Box::new(move|_|{/*TODO*/}) as Box<dyn Fn(JackEvent) + Send + Sync>;
|
|
||||||
let events = Notifications(event);
|
|
||||||
let frame = Box::new({
|
|
||||||
let target = target.clone();
|
|
||||||
move|c: &_, s: &_|if let Ok(mut target) = target.write() {
|
|
||||||
target.process(c, s)
|
|
||||||
} else {
|
|
||||||
Control::Quit
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let frames = ClosureProcessHandler::new(frame as BoxedAudioHandler);
|
|
||||||
let mut buffer = Self::Activating;
|
|
||||||
std::mem::swap(&mut*client.write().unwrap(), &mut buffer);
|
|
||||||
*client.write().unwrap() = Self::Active(Client::from(buffer).activate_async(events, frames)?);
|
|
||||||
Ok(target)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A UI component that may be associated with a JACK client by the `Jack` factory.
|
|
||||||
pub trait AudioComponent<E: Engine>: Component<E> + Audio {
|
|
||||||
/// Perform type erasure for collecting heterogeneous devices.
|
|
||||||
fn boxed(self) -> Box<dyn AudioComponent<E>>
|
|
||||||
where
|
|
||||||
Self: Sized + 'static,
|
|
||||||
{
|
|
||||||
Box::new(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// All things that implement the required traits can be treated as `AudioComponent`.
|
|
||||||
impl<E: Engine, W: Component<E> + Audio> AudioComponent<E> for W {}
|
|
||||||
|
|
||||||
/// Trait for things that may expose JACK ports.
|
|
||||||
pub trait Ports {
|
|
||||||
fn audio_ins(&self) -> Usually<Vec<&Port<Unowned>>> {
|
|
||||||
Ok(vec![])
|
|
||||||
}
|
|
||||||
fn audio_outs(&self) -> Usually<Vec<&Port<Unowned>>> {
|
|
||||||
Ok(vec![])
|
|
||||||
}
|
|
||||||
fn midi_ins(&self) -> Usually<Vec<&Port<Unowned>>> {
|
|
||||||
Ok(vec![])
|
|
||||||
}
|
|
||||||
fn midi_outs(&self) -> Usually<Vec<&Port<Unowned>>> {
|
|
||||||
Ok(vec![])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn register_ports<T: PortSpec + Copy>(
|
|
||||||
client: &Client,
|
|
||||||
names: Vec<String>,
|
|
||||||
spec: T,
|
|
||||||
) -> Usually<BTreeMap<String, Port<T>>> {
|
|
||||||
names
|
|
||||||
.into_iter()
|
|
||||||
.try_fold(BTreeMap::new(), |mut ports, name| {
|
|
||||||
let port = client.register_port(&name, spec)?;
|
|
||||||
ports.insert(name, port);
|
|
||||||
Ok(ports)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn query_ports(client: &Client, names: Vec<String>) -> BTreeMap<String, Port<Unowned>> {
|
|
||||||
names.into_iter().fold(BTreeMap::new(), |mut ports, name| {
|
|
||||||
let port = client.port_by_name(&name).unwrap();
|
|
||||||
ports.insert(name, port);
|
|
||||||
ports
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
///// A [AudioComponent] bound to a JACK client and a set of ports.
|
///// A [AudioComponent] bound to a JACK client and a set of ports.
|
||||||
//pub struct JackDevice<E: Engine> {
|
//pub struct JackDevice<E: Engine> {
|
||||||
///// The active JACK client of this device.
|
///// The active JACK client of this device.
|
||||||
|
|
@ -373,7 +236,7 @@ fn query_ports(client: &Client, names: Vec<String>) -> BTreeMap<String, Port<Uno
|
||||||
////state.lock().unwrap().handle(&event).unwrap();
|
////state.lock().unwrap().handle(&event).unwrap();
|
||||||
//}
|
//}
|
||||||
//}) as Box<dyn Fn(JackEvent) + Send + Sync>),
|
//}) as Box<dyn Fn(JackEvent) + Send + Sync>),
|
||||||
//contrib::ClosureProcessHandler::new(Box::new({
|
//ClosureProcessHandler::new(Box::new({
|
||||||
//let state = state.clone();
|
//let state = state.clone();
|
||||||
//move |c: &Client, s: &ProcessScope| state.write().unwrap().process(c, s)
|
//move |c: &Client, s: &ProcessScope| state.write().unwrap().process(c, s)
|
||||||
//}) as BoxedAudioHandler),
|
//}) as BoxedAudioHandler),
|
||||||
|
|
@ -406,78 +269,3 @@ fn query_ports(client: &Client, names: Vec<String>) -> BTreeMap<String, Port<Uno
|
||||||
//self
|
//self
|
||||||
//}
|
//}
|
||||||
//}
|
//}
|
||||||
|
|
||||||
//impl Command<ArrangerModel> for ArrangerSceneCommand {
|
|
||||||
//}
|
|
||||||
//Edit(phrase) => { state.state.phrase = phrase.clone() },
|
|
||||||
//ToggleViewMode => { state.state.mode.to_next(); },
|
|
||||||
//Delete => { state.state.delete(); },
|
|
||||||
//Activate => { state.state.activate(); },
|
|
||||||
//ZoomIn => { state.state.zoom_in(); },
|
|
||||||
//ZoomOut => { state.state.zoom_out(); },
|
|
||||||
//MoveBack => { state.state.move_back(); },
|
|
||||||
//MoveForward => { state.state.move_forward(); },
|
|
||||||
//RandomColor => { state.state.randomize_color(); },
|
|
||||||
//Put => { state.state.phrase_put(); },
|
|
||||||
//Get => { state.state.phrase_get(); },
|
|
||||||
//AddScene => { state.state.scene_add(None, None)?; },
|
|
||||||
//AddTrack => { state.state.track_add(None, None)?; },
|
|
||||||
//ToggleLoop => { state.state.toggle_loop() },
|
|
||||||
//pub fn zoom_in (&mut self) {
|
|
||||||
//if let ArrangerEditorMode::Vertical(factor) = self.mode {
|
|
||||||
//self.mode = ArrangerEditorMode::Vertical(factor + 1)
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//pub fn zoom_out (&mut self) {
|
|
||||||
//if let ArrangerEditorMode::Vertical(factor) = self.mode {
|
|
||||||
//self.mode = ArrangerEditorMode::Vertical(factor.saturating_sub(1))
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//pub fn move_back (&mut self) {
|
|
||||||
//match self.selected {
|
|
||||||
//ArrangerEditorFocus::Scene(s) => {
|
|
||||||
//if s > 0 {
|
|
||||||
//self.scenes.swap(s, s - 1);
|
|
||||||
//self.selected = ArrangerEditorFocus::Scene(s - 1);
|
|
||||||
//}
|
|
||||||
//},
|
|
||||||
//ArrangerEditorFocus::Track(t) => {
|
|
||||||
//if t > 0 {
|
|
||||||
//self.tracks.swap(t, t - 1);
|
|
||||||
//self.selected = ArrangerEditorFocus::Track(t - 1);
|
|
||||||
//// FIXME: also swap clip order in scenes
|
|
||||||
//}
|
|
||||||
//},
|
|
||||||
//_ => todo!("arrangement: move forward")
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//pub fn move_forward (&mut self) {
|
|
||||||
//match self.selected {
|
|
||||||
//ArrangerEditorFocus::Scene(s) => {
|
|
||||||
//if s < self.scenes.len().saturating_sub(1) {
|
|
||||||
//self.scenes.swap(s, s + 1);
|
|
||||||
//self.selected = ArrangerEditorFocus::Scene(s + 1);
|
|
||||||
//}
|
|
||||||
//},
|
|
||||||
//ArrangerEditorFocus::Track(t) => {
|
|
||||||
//if t < self.tracks.len().saturating_sub(1) {
|
|
||||||
//self.tracks.swap(t, t + 1);
|
|
||||||
//self.selected = ArrangerEditorFocus::Track(t + 1);
|
|
||||||
//// FIXME: also swap clip order in scenes
|
|
||||||
//}
|
|
||||||
//},
|
|
||||||
//_ => todo!("arrangement: move forward")
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
|
|
||||||
//impl From<Moment> for Clock {
|
|
||||||
//fn from (current: Moment) -> Self {
|
|
||||||
//Self {
|
|
||||||
//playing: Some(TransportState::Stopped).into(),
|
|
||||||
//started: None.into(),
|
|
||||||
//quant: 24.into(),
|
|
||||||
//sync: (current.timebase.ppq.get() * 4.).into(),
|
|
||||||
//current,
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
50
crates/tek/src/jack/activate.rs
Normal file
50
crates/tek/src/jack/activate.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
pub trait JackActivate: Sized {
|
||||||
|
fn activate_with <T: Audio + 'static> (
|
||||||
|
self,
|
||||||
|
init: impl FnOnce(&Arc<RwLock<JackClient>>)->Usually<T>
|
||||||
|
)
|
||||||
|
-> Usually<Arc<RwLock<T>>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JackActivate for JackClient {
|
||||||
|
fn activate_with <T: Audio + 'static> (
|
||||||
|
self,
|
||||||
|
init: impl FnOnce(&Arc<RwLock<JackClient>>)->Usually<T>
|
||||||
|
)
|
||||||
|
-> Usually<Arc<RwLock<T>>>
|
||||||
|
{
|
||||||
|
let client = Arc::new(RwLock::new(self));
|
||||||
|
let target = Arc::new(RwLock::new(init(&client)?));
|
||||||
|
let event = Box::new(move|_|{/*TODO*/}) as Box<dyn Fn(JackEvent) + Send + Sync>;
|
||||||
|
let events = Notifications(event);
|
||||||
|
let frame = Box::new({
|
||||||
|
let target = target.clone();
|
||||||
|
move|c: &_, s: &_|if let Ok(mut target) = target.write() {
|
||||||
|
target.process(c, s)
|
||||||
|
} else {
|
||||||
|
Control::Quit
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let frames = ClosureProcessHandler::new(frame as BoxedAudioHandler);
|
||||||
|
let mut buffer = Self::Activating;
|
||||||
|
std::mem::swap(&mut*client.write().unwrap(), &mut buffer);
|
||||||
|
*client.write().unwrap() = Self::Active(Client::from(buffer).activate_async(events, frames)?);
|
||||||
|
Ok(target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A UI component that may be associated with a JACK client by the `Jack` factory.
|
||||||
|
pub trait AudioComponent<E: Engine>: Component<E> + Audio {
|
||||||
|
/// Perform type erasure for collecting heterogeneous devices.
|
||||||
|
fn boxed(self) -> Box<dyn AudioComponent<E>>
|
||||||
|
where
|
||||||
|
Self: Sized + 'static,
|
||||||
|
{
|
||||||
|
Box::new(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All things that implement the required traits can be treated as `AudioComponent`.
|
||||||
|
impl<E: Engine, W: Component<E> + Audio> AudioComponent<E> for W {}
|
||||||
78
crates/tek/src/jack/audio.rs
Normal file
78
crates/tek/src/jack/audio.rs
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// Implement [Audio]: provide JACK callbacks.
|
||||||
|
#[macro_export] macro_rules! audio {
|
||||||
|
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?,$c:ident,$s:ident|$cb:expr) => {
|
||||||
|
impl $(<$($L),*$($T $(: $U)?),*>)? Audio for $Struct $(<$($L),*$($T),*>)? {
|
||||||
|
#[inline] fn process (&mut $self, $c: &Client, $s: &ProcessScope) -> Control { $cb }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait for thing that has a JACK process callback.
|
||||||
|
pub trait Audio: Send + Sync {
|
||||||
|
fn process (&mut self, _: &Client, _: &ProcessScope) -> Control {
|
||||||
|
Control::Continue
|
||||||
|
}
|
||||||
|
fn callback (
|
||||||
|
state: &Arc<RwLock<Self>>, client: &Client, scope: &ProcessScope
|
||||||
|
) -> Control where Self: Sized {
|
||||||
|
if let Ok(mut state) = state.write() {
|
||||||
|
state.process(client, scope)
|
||||||
|
} else {
|
||||||
|
Control::Quit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Trait for things that wrap a JACK client.
|
||||||
|
pub trait AudioEngine {
|
||||||
|
|
||||||
|
fn transport (&self) -> Transport {
|
||||||
|
self.client().transport()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn port_by_name (&self, name: &str) -> Option<Port<Unowned>> {
|
||||||
|
self.client().port_by_name(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_port <PS: PortSpec> (&self, name: &str, spec: PS) -> Usually<Port<PS>> {
|
||||||
|
Ok(self.client().register_port(name, spec)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn client (&self) -> &Client;
|
||||||
|
|
||||||
|
fn activate (
|
||||||
|
self,
|
||||||
|
process: impl FnMut(&Arc<RwLock<Self>>, &Client, &ProcessScope) -> Control + Send + 'static
|
||||||
|
) -> Usually<Arc<RwLock<Self>>> where Self: Send + Sync + 'static;
|
||||||
|
|
||||||
|
fn thread_init (&self, _: &Client) {}
|
||||||
|
|
||||||
|
unsafe fn shutdown (&mut self, _status: ClientStatus, _reason: &str) {}
|
||||||
|
|
||||||
|
fn freewheel (&mut self, _: &Client, _enabled: bool) {}
|
||||||
|
|
||||||
|
fn client_registration (&mut self, _: &Client, _name: &str, _reg: bool) {}
|
||||||
|
|
||||||
|
fn port_registration (&mut self, _: &Client, _id: PortId, _reg: bool) {}
|
||||||
|
|
||||||
|
fn ports_connected (&mut self, _: &Client, _a: PortId, _b: PortId, _are: bool) {}
|
||||||
|
|
||||||
|
fn sample_rate (&mut self, _: &Client, _frames: Frames) -> Control {
|
||||||
|
Control::Continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fn port_rename (&mut self, _: &Client, _id: PortId, _old: &str, _new: &str) -> Control {
|
||||||
|
Control::Continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fn graph_reorder (&mut self, _: &Client) -> Control {
|
||||||
|
Control::Continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fn xrun (&mut self, _: &Client) -> Control {
|
||||||
|
Control::Continue
|
||||||
|
}
|
||||||
|
}
|
||||||
61
crates/tek/src/jack/client.rs
Normal file
61
crates/tek/src/jack/client.rs
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// Wraps [Client] or [DynamicAsyncClient] in place.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum JackClient {
|
||||||
|
/// Before activation.
|
||||||
|
Inactive(Client),
|
||||||
|
/// During activation.
|
||||||
|
Activating,
|
||||||
|
/// After activation. Must not be dropped for JACK thread to persist.
|
||||||
|
Active(DynamicAsyncClient),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AudioEngine for JackClient {
|
||||||
|
fn client(&self) -> &Client {
|
||||||
|
match self {
|
||||||
|
Self::Inactive(ref client) => client,
|
||||||
|
Self::Activating => panic!("jack client has not finished activation"),
|
||||||
|
Self::Active(ref client) => client.as_client(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn activate(
|
||||||
|
self,
|
||||||
|
mut cb: impl FnMut(&Arc<RwLock<Self>>, &Client, &ProcessScope) -> Control + Send + 'static,
|
||||||
|
) -> Usually<Arc<RwLock<Self>>>
|
||||||
|
where
|
||||||
|
Self: Send + Sync + 'static
|
||||||
|
{
|
||||||
|
let client = Client::from(self);
|
||||||
|
let state = Arc::new(RwLock::new(Self::Activating));
|
||||||
|
let event = Box::new(move|_|{/*TODO*/}) as Box<dyn Fn(JackEvent) + Send + Sync>;
|
||||||
|
let events = Notifications(event);
|
||||||
|
let frame = Box::new({let state = state.clone(); move|c: &_, s: &_|cb(&state, c, s)});
|
||||||
|
let frames = ClosureProcessHandler::new(frame as BoxedAudioHandler);
|
||||||
|
*state.write().unwrap() = Self::Active(client.activate_async(events, frames)?);
|
||||||
|
Ok(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type DynamicAsyncClient = AsyncClient<DynamicNotifications, DynamicAudioHandler>;
|
||||||
|
|
||||||
|
pub type DynamicAudioHandler = ClosureProcessHandler<(), BoxedAudioHandler>;
|
||||||
|
|
||||||
|
pub type BoxedAudioHandler = Box<dyn FnMut(&Client, &ProcessScope) -> Control + Send>;
|
||||||
|
|
||||||
|
impl JackClient {
|
||||||
|
pub fn new (name: &str) -> Usually<Self> {
|
||||||
|
let (client, _) = Client::new(name, ClientOptions::NO_START_SERVER)?;
|
||||||
|
Ok(Self::Inactive(client))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<JackClient> for Client {
|
||||||
|
fn from (jack: JackClient) -> Client {
|
||||||
|
match jack {
|
||||||
|
JackClient::Inactive(client) => client,
|
||||||
|
JackClient::Activating => panic!("jack client still activating"),
|
||||||
|
JackClient::Active(_) => panic!("jack client already activated"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
crates/tek/src/jack/from_jack.rs
Normal file
13
crates/tek/src/jack/from_jack.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
|
||||||
|
|
||||||
|
/// Implement [TryFrom<&Arc<RwLock<JackClient>>>]: create app state from wrapped JACK handle.
|
||||||
|
#[macro_export] macro_rules! from_jack {
|
||||||
|
(|$jack:ident|$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)? $cb:expr) => {
|
||||||
|
impl $(<$($L),*$($T $(: $U)?),*>)? TryFrom<&Arc<RwLock<JackClient>>> for $Struct $(<$($L),*$($T),*>)? {
|
||||||
|
type Error = Box<dyn std::error::Error>;
|
||||||
|
fn try_from ($jack: &Arc<RwLock<JackClient>>) -> Usually<Self> {
|
||||||
|
Ok($cb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
69
crates/tek/src/jack/jack_event.rs
Normal file
69
crates/tek/src/jack/jack_event.rs
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
/// Event enum for JACK events.
|
||||||
|
pub enum JackEvent {
|
||||||
|
ThreadInit,
|
||||||
|
Shutdown(ClientStatus, String),
|
||||||
|
Freewheel(bool),
|
||||||
|
SampleRate(Frames),
|
||||||
|
ClientRegistration(String, bool),
|
||||||
|
PortRegistration(PortId, bool),
|
||||||
|
PortRename(PortId, String, String),
|
||||||
|
PortsConnected(PortId, PortId, bool),
|
||||||
|
GraphReorder,
|
||||||
|
XRun,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Notification handler used by the [Jack] factory
|
||||||
|
/// when constructing [JackDevice]s.
|
||||||
|
pub type DynamicNotifications = Notifications<Box<dyn Fn(JackEvent) + Send + Sync>>;
|
||||||
|
|
||||||
|
/// Generic notification handler that emits [JackEvent]
|
||||||
|
pub struct Notifications<T: Fn(JackEvent) + Send>(pub T);
|
||||||
|
|
||||||
|
impl<T: Fn(JackEvent) + Send> NotificationHandler for Notifications<T> {
|
||||||
|
fn thread_init(&self, _: &Client) {
|
||||||
|
self.0(JackEvent::ThreadInit);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn shutdown(&mut self, status: ClientStatus, reason: &str) {
|
||||||
|
self.0(JackEvent::Shutdown(status, reason.into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn freewheel(&mut self, _: &Client, enabled: bool) {
|
||||||
|
self.0(JackEvent::Freewheel(enabled));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sample_rate(&mut self, _: &Client, frames: Frames) -> Control {
|
||||||
|
self.0(JackEvent::SampleRate(frames));
|
||||||
|
Control::Quit
|
||||||
|
}
|
||||||
|
|
||||||
|
fn client_registration(&mut self, _: &Client, name: &str, reg: bool) {
|
||||||
|
self.0(JackEvent::ClientRegistration(name.into(), reg));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn port_registration(&mut self, _: &Client, id: PortId, reg: bool) {
|
||||||
|
self.0(JackEvent::PortRegistration(id, reg));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn port_rename(&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control {
|
||||||
|
self.0(JackEvent::PortRename(id, old.into(), new.into()));
|
||||||
|
Control::Continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ports_connected(&mut self, _: &Client, a: PortId, b: PortId, are: bool) {
|
||||||
|
self.0(JackEvent::PortsConnected(a, b, are));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn graph_reorder(&mut self, _: &Client) -> Control {
|
||||||
|
self.0(JackEvent::GraphReorder);
|
||||||
|
Control::Continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fn xrun(&mut self, _: &Client) -> Control {
|
||||||
|
self.0(JackEvent::XRun);
|
||||||
|
Control::Continue
|
||||||
|
}
|
||||||
|
}
|
||||||
39
crates/tek/src/jack/ports.rs
Normal file
39
crates/tek/src/jack/ports.rs
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// Trait for things that may expose JACK ports.
|
||||||
|
pub trait Ports {
|
||||||
|
fn audio_ins(&self) -> Usually<Vec<&Port<Unowned>>> {
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
|
fn audio_outs(&self) -> Usually<Vec<&Port<Unowned>>> {
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
|
fn midi_ins(&self) -> Usually<Vec<&Port<Unowned>>> {
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
|
fn midi_outs(&self) -> Usually<Vec<&Port<Unowned>>> {
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_ports<T: PortSpec + Copy>(
|
||||||
|
client: &Client,
|
||||||
|
names: Vec<String>,
|
||||||
|
spec: T,
|
||||||
|
) -> Usually<BTreeMap<String, Port<T>>> {
|
||||||
|
names
|
||||||
|
.into_iter()
|
||||||
|
.try_fold(BTreeMap::new(), |mut ports, name| {
|
||||||
|
let port = client.register_port(&name, spec)?;
|
||||||
|
ports.insert(name, port);
|
||||||
|
Ok(ports)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn query_ports(client: &Client, names: Vec<String>) -> BTreeMap<String, Port<Unowned>> {
|
||||||
|
names.into_iter().fold(BTreeMap::new(), |mut ports, name| {
|
||||||
|
let port = client.port_by_name(&name).unwrap();
|
||||||
|
ports.insert(name, port);
|
||||||
|
ports
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -34,14 +34,20 @@ pub(crate) use tui::*;
|
||||||
pub mod edn;
|
pub mod edn;
|
||||||
pub(crate) use edn::*;
|
pub(crate) use edn::*;
|
||||||
|
|
||||||
|
pub mod jack;
|
||||||
|
pub(crate) use jack::*;
|
||||||
|
|
||||||
|
pub mod midi;
|
||||||
|
pub(crate) use midi::*;
|
||||||
|
|
||||||
testmod! { test }
|
testmod! { test }
|
||||||
|
|
||||||
pub(crate) use clap::{self, Parser};
|
pub(crate) use clap::{self, Parser};
|
||||||
|
|
||||||
pub use better_panic;
|
pub use ::better_panic;
|
||||||
pub(crate) use better_panic::{Settings, Verbosity};
|
pub(crate) use better_panic::{Settings, Verbosity};
|
||||||
|
|
||||||
pub use atomic_float;
|
pub use ::atomic_float;
|
||||||
pub(crate) use atomic_float::*;
|
pub(crate) use atomic_float::*;
|
||||||
|
|
||||||
pub(crate) use std::sync::{Arc, Mutex, RwLock};
|
pub(crate) use std::sync::{Arc, Mutex, RwLock};
|
||||||
|
|
@ -58,28 +64,32 @@ pub(crate) use std::ops::{Add, Sub, Mul, Div, Rem};
|
||||||
pub(crate) use std::cmp::{Ord, Eq, PartialEq};
|
pub(crate) use std::cmp::{Ord, Eq, PartialEq};
|
||||||
pub(crate) use std::fmt::{Debug, Display};
|
pub(crate) use std::fmt::{Debug, Display};
|
||||||
|
|
||||||
pub use crossterm;
|
pub use ::crossterm;
|
||||||
pub(crate) use crossterm::{ExecutableCommand};
|
pub(crate) use crossterm::{ExecutableCommand};
|
||||||
pub(crate) use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode};
|
pub(crate) use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode};
|
||||||
pub(crate) use crossterm::event::{KeyCode, KeyModifiers, KeyEvent, KeyEventKind, KeyEventState};
|
pub(crate) use crossterm::event::{KeyCode, KeyModifiers, KeyEvent, KeyEventKind, KeyEventState};
|
||||||
|
|
||||||
pub use ratatui;
|
pub use ::ratatui;
|
||||||
pub(crate) use ratatui::{
|
pub(crate) use ratatui::{
|
||||||
prelude::{Style, Color, Buffer},
|
prelude::{Style, Color, Buffer},
|
||||||
style::{Stylize, Modifier},
|
style::{Stylize, Modifier},
|
||||||
backend::{Backend, CrosstermBackend, ClearType}
|
backend::{Backend, CrosstermBackend, ClearType}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use jack;
|
pub use ::jack as libjack;
|
||||||
pub(crate) use jack::{
|
pub(crate) use ::jack::{
|
||||||
contrib::ClosureProcessHandler,
|
contrib::ClosureProcessHandler,
|
||||||
Client, ProcessScope, Control, CycleTimes,
|
Client, AsyncClient, ClientOptions, ClientStatus,
|
||||||
Port, PortSpec, MidiIn, MidiOut, AudioIn, AudioOut, Unowned,
|
ProcessScope, Control, CycleTimes,
|
||||||
|
Port, PortId,
|
||||||
|
PortSpec, MidiIn, MidiOut, AudioIn, AudioOut, Unowned,
|
||||||
Transport, TransportState, MidiIter, RawMidi,
|
Transport, TransportState, MidiIter, RawMidi,
|
||||||
|
Frames,
|
||||||
|
NotificationHandler,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use midly;
|
pub use ::midly;
|
||||||
pub(crate) use midly::{
|
pub(crate) use ::midly::{
|
||||||
Smf,
|
Smf,
|
||||||
MidiMessage,
|
MidiMessage,
|
||||||
TrackEventKind,
|
TrackEventKind,
|
||||||
|
|
@ -87,8 +97,8 @@ pub(crate) use midly::{
|
||||||
num::u7
|
num::u7
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use palette;
|
pub use ::palette;
|
||||||
pub(crate) use palette::{
|
pub(crate) use ::palette::{
|
||||||
*,
|
*,
|
||||||
convert::*,
|
convert::*,
|
||||||
okhsl::*
|
okhsl::*
|
||||||
|
|
|
||||||
36
crates/tek/src/midi.rs
Normal file
36
crates/tek/src/midi.rs
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
pub(crate) mod midi_note; pub(crate) use midi_note::*;
|
||||||
|
pub(crate) mod midi_in; pub(crate) use midi_in::*;
|
||||||
|
pub(crate) mod midi_out; pub(crate) use midi_out::*;
|
||||||
|
pub(crate) mod midi_player; pub(crate) use midi_player::*;
|
||||||
|
pub(crate) mod midi_launch; pub(crate) use midi_launch::*;
|
||||||
|
pub(crate) mod midi_play; pub(crate) use midi_play::*;
|
||||||
|
pub(crate) mod midi_rec; pub(crate) use midi_rec::*;
|
||||||
|
|
||||||
|
/// Add "all notes off" to the start of a buffer.
|
||||||
|
pub fn all_notes_off (output: &mut [Vec<Vec<u8>>]) {
|
||||||
|
let mut buf = vec![];
|
||||||
|
let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() };
|
||||||
|
let evt = LiveEvent::Midi { channel: 0.into(), message: msg };
|
||||||
|
evt.write(&mut buf).unwrap();
|
||||||
|
output[0].push(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return boxed iterator of MIDI events
|
||||||
|
pub fn parse_midi_input (input: MidiIter) -> Box<dyn Iterator<Item=(usize, LiveEvent, &[u8])> + '_> {
|
||||||
|
Box::new(input.map(|RawMidi { time, bytes }|(
|
||||||
|
time as usize,
|
||||||
|
LiveEvent::parse(bytes).unwrap(),
|
||||||
|
bytes
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update notes_in array
|
||||||
|
pub fn update_keys (keys: &mut[bool;128], message: &MidiMessage) {
|
||||||
|
match message {
|
||||||
|
MidiMessage::NoteOn { key, .. } => { keys[key.as_int() as usize] = true; }
|
||||||
|
MidiMessage::NoteOff { key, .. } => { keys[key.as_int() as usize] = false; },
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
crates/tek/src/midi/midi_in.rs
Normal file
10
crates/tek/src/midi/midi_in.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// Trait for thing that may receive MIDI.
|
||||||
|
pub trait HasMidiIns {
|
||||||
|
fn midi_ins (&self) -> &Vec<Port<MidiIn>>;
|
||||||
|
fn midi_ins_mut (&mut self) -> &mut Vec<Port<MidiIn>>;
|
||||||
|
fn has_midi_ins (&self) -> bool {
|
||||||
|
self.midi_ins().len() > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
35
crates/tek/src/midi/midi_launch.rs
Normal file
35
crates/tek/src/midi/midi_launch.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
pub trait HasPlayPhrase: HasClock {
|
||||||
|
fn reset (&self) -> bool;
|
||||||
|
fn reset_mut (&mut self) -> &mut bool;
|
||||||
|
fn play_phrase (&self) -> &Option<(Moment, Option<Arc<RwLock<Phrase>>>)>;
|
||||||
|
fn play_phrase_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<Phrase>>>)>;
|
||||||
|
fn next_phrase (&self) -> &Option<(Moment, Option<Arc<RwLock<Phrase>>>)>;
|
||||||
|
fn next_phrase_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<Phrase>>>)>;
|
||||||
|
fn pulses_since_start (&self) -> Option<f64> {
|
||||||
|
if let Some((started, Some(_))) = self.play_phrase().as_ref() {
|
||||||
|
let elapsed = self.clock().playhead.pulse.get() - started.pulse.get();
|
||||||
|
Some(elapsed)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn pulses_since_start_looped (&self) -> Option<f64> {
|
||||||
|
if let Some((started, Some(phrase))) = self.play_phrase().as_ref() {
|
||||||
|
let elapsed = self.clock().playhead.pulse.get() - started.pulse.get();
|
||||||
|
let length = phrase.read().unwrap().length.max(1); // prevent div0 on empty phrase
|
||||||
|
let elapsed = (elapsed as usize % length) as f64;
|
||||||
|
Some(elapsed)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn enqueue_next (&mut self, phrase: Option<&Arc<RwLock<Phrase>>>) {
|
||||||
|
let start = self.clock().next_launch_pulse() as f64;
|
||||||
|
let instant = Moment::from_pulse(&self.clock().timebase(), start);
|
||||||
|
let phrase = phrase.map(|p|p.clone());
|
||||||
|
*self.next_phrase_mut() = Some((instant, phrase));
|
||||||
|
*self.reset_mut() = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
110
crates/tek/src/midi/midi_note.rs
Normal file
110
crates/tek/src/midi/midi_note.rs
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
use crate::*;
|
||||||
|
use Ordering::Relaxed;
|
||||||
|
|
||||||
|
pub trait MidiViewport<E: Engine>: MidiRange + MidiPoint + HasSize<E> {
|
||||||
|
/// Make sure cursor is within range
|
||||||
|
fn autoscroll (&self) {
|
||||||
|
let note_lo = self.note_lo();
|
||||||
|
let note_axis = self.note_axis();
|
||||||
|
let note_hi = self.note_hi();
|
||||||
|
let note_point = self.note_point().min(127);
|
||||||
|
if note_point < note_lo {
|
||||||
|
self.set_note_lo(note_point);
|
||||||
|
} else if note_point > note_hi {
|
||||||
|
self.set_note_lo((note_lo + note_point).saturating_sub(note_hi));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Make sure best usage of screen space is achieved by default
|
||||||
|
fn autozoom (&self) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct MidiRangeModel {
|
||||||
|
/// Length of visible time axis
|
||||||
|
pub time_axis: Arc<AtomicUsize>,
|
||||||
|
/// Earliest time displayed
|
||||||
|
pub time_start: Arc<AtomicUsize>,
|
||||||
|
/// Time step
|
||||||
|
pub time_zoom: Arc<AtomicUsize>,
|
||||||
|
/// Auto rezoom to fit in time axis
|
||||||
|
pub time_lock: Arc<AtomicBool>,
|
||||||
|
/// Length of visible note axis
|
||||||
|
pub note_axis: Arc<AtomicUsize>,
|
||||||
|
// Lowest note displayed
|
||||||
|
pub note_lo: Arc<AtomicUsize>,
|
||||||
|
}
|
||||||
|
impl From<(usize, bool)> for MidiRangeModel {
|
||||||
|
fn from ((time_zoom, time_lock): (usize, bool)) -> Self {
|
||||||
|
Self {
|
||||||
|
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(time_zoom.into()),
|
||||||
|
time_lock: Arc::new(time_lock.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub trait MidiRange {
|
||||||
|
fn time_zoom (&self) -> usize;
|
||||||
|
fn set_time_zoom (&mut self, x: usize);
|
||||||
|
fn time_lock (&self) -> bool;
|
||||||
|
fn set_time_lock (&self, x: bool);
|
||||||
|
fn time_start (&self) -> usize;
|
||||||
|
fn set_time_start (&self, x: usize);
|
||||||
|
fn note_lo (&self) -> usize;
|
||||||
|
fn set_note_lo (&self, x: usize);
|
||||||
|
fn note_axis (&self) -> usize;
|
||||||
|
fn time_axis (&self) -> usize;
|
||||||
|
fn note_hi (&self) -> usize { (self.note_lo() + self.note_axis().saturating_sub(1)).min(127) }
|
||||||
|
fn time_end (&self) -> usize { self.time_start() + self.time_axis() * self.time_zoom() }
|
||||||
|
}
|
||||||
|
impl MidiRange for MidiRangeModel {
|
||||||
|
fn time_zoom (&self) -> usize { self.time_zoom.load(Relaxed) }
|
||||||
|
fn set_time_zoom (&mut self, x: usize) { self.time_zoom.store(x, Relaxed); }
|
||||||
|
fn time_lock (&self) -> bool { self.time_lock.load(Relaxed) }
|
||||||
|
fn set_time_lock (&self, x: bool) { self.time_lock.store(x, Relaxed); }
|
||||||
|
fn time_start (&self) -> usize { self.time_start.load(Relaxed) }
|
||||||
|
fn set_time_start (&self, x: usize) { self.time_start.store(x, Relaxed); }
|
||||||
|
fn note_lo (&self) -> usize { self.note_lo.load(Relaxed).min(127) }
|
||||||
|
fn set_note_lo (&self, x: usize) { self.note_lo.store(x.min(127), Relaxed); }
|
||||||
|
fn note_axis (&self) -> usize { self.note_axis.load(Relaxed) }
|
||||||
|
fn time_axis (&self) -> usize { self.time_axis.load(Relaxed) }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct MidiPointModel {
|
||||||
|
/// Time coordinate of cursor
|
||||||
|
pub time_point: Arc<AtomicUsize>,
|
||||||
|
/// Note coordinate of cursor
|
||||||
|
pub note_point: Arc<AtomicUsize>,
|
||||||
|
/// Length of note that will be inserted, in pulses
|
||||||
|
pub note_len: Arc<AtomicUsize>,
|
||||||
|
}
|
||||||
|
impl Default for MidiPointModel {
|
||||||
|
fn default () -> Self {
|
||||||
|
Self {
|
||||||
|
time_point: Arc::new(0.into()),
|
||||||
|
note_point: Arc::new(36.into()),
|
||||||
|
note_len: Arc::new(24.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub trait MidiPoint {
|
||||||
|
fn note_len (&self) -> usize;
|
||||||
|
fn set_note_len (&self, x: usize);
|
||||||
|
fn note_point (&self) -> usize;
|
||||||
|
fn set_note_point (&self, x: usize);
|
||||||
|
fn time_point (&self) -> usize;
|
||||||
|
fn set_time_point (&self, x: usize);
|
||||||
|
fn note_end (&self) -> usize { self.note_point() + self.note_len() }
|
||||||
|
}
|
||||||
|
impl MidiPoint for MidiPointModel {
|
||||||
|
fn note_len (&self) -> usize { self.note_len.load(Relaxed)}
|
||||||
|
fn set_note_len (&self, x: usize) { self.note_len.store(x, Relaxed) }
|
||||||
|
fn note_point (&self) -> usize { self.note_point.load(Relaxed).min(127) }
|
||||||
|
fn set_note_point (&self, x: usize) { self.note_point.store(x.min(127), Relaxed) }
|
||||||
|
fn time_point (&self) -> usize { self.time_point.load(Relaxed) }
|
||||||
|
fn set_time_point (&self, x: usize) { self.time_point.store(x, Relaxed) }
|
||||||
|
}
|
||||||
12
crates/tek/src/midi/midi_out.rs
Normal file
12
crates/tek/src/midi/midi_out.rs
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// Trait for thing that may output MIDI.
|
||||||
|
pub trait HasMidiOuts {
|
||||||
|
fn midi_outs (&self) -> &Vec<Port<MidiOut>>;
|
||||||
|
fn midi_outs_mut (&mut self) -> &mut Vec<Port<MidiOut>>;
|
||||||
|
fn has_midi_outs (&self) -> bool {
|
||||||
|
self.midi_outs().len() > 0
|
||||||
|
}
|
||||||
|
/// Buffer for serializing a MIDI event. FIXME rename
|
||||||
|
fn midi_note (&mut self) -> &mut Vec<u8>;
|
||||||
|
}
|
||||||
134
crates/tek/src/midi/midi_play.rs
Normal file
134
crates/tek/src/midi/midi_play.rs
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
pub trait MidiPlaybackApi: HasPlayPhrase + HasClock + HasMidiOuts {
|
||||||
|
|
||||||
|
fn notes_out (&self) -> &Arc<RwLock<[bool;128]>>;
|
||||||
|
|
||||||
|
/// Clear the section of the output buffer that we will be using,
|
||||||
|
/// emitting "all notes off" at start of buffer if requested.
|
||||||
|
fn clear (
|
||||||
|
&mut self, scope: &ProcessScope, out_buf: &mut Vec<Vec<Vec<u8>>>, reset: bool
|
||||||
|
) {
|
||||||
|
for frame in &mut out_buf[0..scope.n_frames() as usize] {
|
||||||
|
frame.clear();
|
||||||
|
}
|
||||||
|
if reset {
|
||||||
|
all_notes_off(out_buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Output notes from phrase to MIDI output ports.
|
||||||
|
fn play (
|
||||||
|
&mut self, scope: &ProcessScope, note_buf: &mut Vec<u8>, out_buf: &mut Vec<Vec<Vec<u8>>>
|
||||||
|
) -> bool {
|
||||||
|
let mut next = false;
|
||||||
|
// Write MIDI events from currently playing phrase (if any) to MIDI output buffer
|
||||||
|
if self.clock().is_rolling() {
|
||||||
|
let sample0 = scope.last_frame_time() as usize;
|
||||||
|
let samples = scope.n_frames() as usize;
|
||||||
|
// If no phrase is playing, prepare for switchover immediately
|
||||||
|
next = self.play_phrase().is_none();
|
||||||
|
let phrase = self.play_phrase();
|
||||||
|
let started0 = &self.clock().started;
|
||||||
|
let timebase = self.clock().timebase();
|
||||||
|
let notes_out = self.notes_out();
|
||||||
|
let next_phrase = self.next_phrase();
|
||||||
|
if let Some((started, phrase)) = phrase {
|
||||||
|
// First sample to populate. Greater than 0 means that the first
|
||||||
|
// pulse of the phrase falls somewhere in the middle of the chunk.
|
||||||
|
let sample = started.sample.get() as usize;
|
||||||
|
let sample = sample + started0.read().unwrap().as_ref().unwrap().sample.get() as usize;
|
||||||
|
let sample = sample0.saturating_sub(sample);
|
||||||
|
// Iterator that emits sample (index into output buffer at which to write MIDI event)
|
||||||
|
// paired with pulse (index into phrase from which to take the MIDI event) for each
|
||||||
|
// sample of the output buffer that corresponds to a MIDI pulse.
|
||||||
|
let pulses = timebase.pulses_between_samples(sample, sample + samples);
|
||||||
|
// Notes active during current chunk.
|
||||||
|
let notes = &mut notes_out.write().unwrap();
|
||||||
|
for (sample, pulse) in pulses {
|
||||||
|
// If a next phrase is enqueued, and we're past the end of the current one,
|
||||||
|
// break the loop here (FIXME count pulse correctly)
|
||||||
|
next = next_phrase.is_some() && if let Some(ref phrase) = phrase {
|
||||||
|
pulse >= phrase.read().unwrap().length
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
};
|
||||||
|
if next {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// If there's a currently playing phrase, output notes from it to buffer:
|
||||||
|
if let Some(ref phrase) = phrase {
|
||||||
|
// Source phrase from which the MIDI events will be taken.
|
||||||
|
let phrase = phrase.read().unwrap();
|
||||||
|
// Phrase with zero length is not processed
|
||||||
|
if phrase.length > 0 {
|
||||||
|
// Current pulse index in source phrase
|
||||||
|
let pulse = pulse % phrase.length;
|
||||||
|
// Output each MIDI event from phrase at appropriate frames of output buffer:
|
||||||
|
for message in phrase.notes[pulse].iter() {
|
||||||
|
// Clear output buffer for this MIDI event.
|
||||||
|
note_buf.clear();
|
||||||
|
// TODO: support MIDI channels other than CH1.
|
||||||
|
let channel = 0.into();
|
||||||
|
// Serialize MIDI event into message buffer.
|
||||||
|
LiveEvent::Midi { channel, message: *message }
|
||||||
|
.write(note_buf)
|
||||||
|
.unwrap();
|
||||||
|
// Append serialized message to output buffer.
|
||||||
|
out_buf[sample].push(note_buf.clone());
|
||||||
|
// Update the list of currently held notes.
|
||||||
|
update_keys(&mut*notes, &message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle switchover from current to next playing phrase.
|
||||||
|
fn switchover (
|
||||||
|
&mut self, scope: &ProcessScope, note_buf: &mut Vec<u8>, out_buf: &mut Vec<Vec<Vec<u8>>>
|
||||||
|
) {
|
||||||
|
if self.clock().is_rolling() {
|
||||||
|
let sample0 = scope.last_frame_time() as usize;
|
||||||
|
//let samples = scope.n_frames() as usize;
|
||||||
|
if let Some((start_at, phrase)) = &self.next_phrase() {
|
||||||
|
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 phrase:
|
||||||
|
if start <= sample0.saturating_sub(sample) {
|
||||||
|
// Samples elapsed since phrase was supposed to start
|
||||||
|
let skipped = sample0 - start;
|
||||||
|
// Switch over to enqueued phrase
|
||||||
|
let started = Moment::from_sample(&self.clock().timebase(), start as f64);
|
||||||
|
*self.play_phrase_mut() = Some((started, phrase.clone()));
|
||||||
|
// Unset enqueuement (TODO: where to implement looping?)
|
||||||
|
*self.next_phrase_mut() = None
|
||||||
|
}
|
||||||
|
// TODO fill in remaining ticks of chunk from next phrase.
|
||||||
|
// ?? just call self.play(scope) again, since enqueuement is off ???
|
||||||
|
self.play(scope, note_buf, out_buf);
|
||||||
|
// ?? or must it be with modified scope ??
|
||||||
|
// likely not because start time etc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write a chunk of MIDI notes to the output buffer.
|
||||||
|
fn write (
|
||||||
|
&mut self, scope: &ProcessScope, out_buf: &Vec<Vec<Vec<u8>>>
|
||||||
|
) {
|
||||||
|
let samples = scope.n_frames() as usize;
|
||||||
|
for port in self.midi_outs_mut().iter_mut() {
|
||||||
|
let writer = &mut port.writer(scope);
|
||||||
|
for time in 0..samples {
|
||||||
|
for event in out_buf[time].iter() {
|
||||||
|
writer.write(&RawMidi { time: time as u32, bytes: &event })
|
||||||
|
.expect(&format!("{event:?}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
255
crates/tek/src/midi/midi_player.rs
Normal file
255
crates/tek/src/midi/midi_player.rs
Normal file
|
|
@ -0,0 +1,255 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
pub trait MidiPlayerApi: MidiRecordApi + MidiPlaybackApi + Send + Sync {}
|
||||||
|
|
||||||
|
impl MidiPlayerApi for PhrasePlayerModel {}
|
||||||
|
|
||||||
|
pub trait HasPlayer {
|
||||||
|
fn player (&self) -> &impl MidiPlayerApi;
|
||||||
|
fn player_mut (&mut self) -> &mut impl MidiPlayerApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export] macro_rules! has_player {
|
||||||
|
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||||||
|
impl $(<$($L),*$($T $(: $U)?),*>)? HasPlayer for $Struct $(<$($L),*$($T),*>)? {
|
||||||
|
fn player (&$self) -> &impl MidiPlayerApi { &$cb }
|
||||||
|
fn player_mut (&mut $self) -> &mut impl MidiPlayerApi { &mut$cb }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Contains state for playing a phrase
|
||||||
|
pub struct PhrasePlayerModel {
|
||||||
|
/// State of clock and playhead
|
||||||
|
pub(crate) clock: ClockModel,
|
||||||
|
/// Start time and phrase being played
|
||||||
|
pub(crate) play_phrase: Option<(Moment, Option<Arc<RwLock<Phrase>>>)>,
|
||||||
|
/// Start time and next phrase
|
||||||
|
pub(crate) next_phrase: Option<(Moment, Option<Arc<RwLock<Phrase>>>)>,
|
||||||
|
/// Play input through output.
|
||||||
|
pub(crate) monitoring: bool,
|
||||||
|
/// Write input to sequence.
|
||||||
|
pub(crate) recording: bool,
|
||||||
|
/// Overdub input to sequence.
|
||||||
|
pub(crate) overdub: bool,
|
||||||
|
/// Send all notes off
|
||||||
|
pub(crate) reset: bool, // TODO?: after Some(nframes)
|
||||||
|
/// Record from MIDI ports to current sequence.
|
||||||
|
pub midi_ins: Vec<Port<MidiIn>>,
|
||||||
|
/// Play from current sequence to MIDI ports
|
||||||
|
pub midi_outs: Vec<Port<MidiOut>>,
|
||||||
|
/// Notes currently held at input
|
||||||
|
pub(crate) notes_in: Arc<RwLock<[bool; 128]>>,
|
||||||
|
/// Notes currently held at output
|
||||||
|
pub(crate) notes_out: Arc<RwLock<[bool; 128]>>,
|
||||||
|
/// MIDI output buffer
|
||||||
|
pub note_buf: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for PhrasePlayerModel {
|
||||||
|
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||||
|
f.debug_struct("PhrasePlayerModel")
|
||||||
|
.field("clock", &self.clock)
|
||||||
|
.field("play_phrase", &self.play_phrase)
|
||||||
|
.field("next_phrase", &self.next_phrase)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&ClockModel> for PhrasePlayerModel {
|
||||||
|
fn from (clock: &ClockModel) -> Self {
|
||||||
|
Self {
|
||||||
|
clock: clock.clone(),
|
||||||
|
midi_ins: vec![],
|
||||||
|
midi_outs: vec![],
|
||||||
|
note_buf: vec![0;8],
|
||||||
|
reset: true,
|
||||||
|
recording: false,
|
||||||
|
monitoring: false,
|
||||||
|
overdub: false,
|
||||||
|
play_phrase: None,
|
||||||
|
next_phrase: None,
|
||||||
|
notes_in: RwLock::new([false;128]).into(),
|
||||||
|
notes_out: RwLock::new([false;128]).into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(&ClockModel, &Arc<RwLock<Phrase>>)> for PhrasePlayerModel {
|
||||||
|
fn from ((clock, phrase): (&ClockModel, &Arc<RwLock<Phrase>>)) -> Self {
|
||||||
|
let mut model = Self::from(clock);
|
||||||
|
model.play_phrase = Some((Moment::zero(&clock.timebase), Some(phrase.clone())));
|
||||||
|
model
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
has_clock!(|self:PhrasePlayerModel|&self.clock);
|
||||||
|
|
||||||
|
impl HasMidiIns for PhrasePlayerModel {
|
||||||
|
fn midi_ins (&self) -> &Vec<Port<MidiIn>> {
|
||||||
|
&self.midi_ins
|
||||||
|
}
|
||||||
|
fn midi_ins_mut (&mut self) -> &mut Vec<Port<MidiIn>> {
|
||||||
|
&mut self.midi_ins
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasMidiOuts for PhrasePlayerModel {
|
||||||
|
fn midi_outs (&self) -> &Vec<Port<MidiOut>> {
|
||||||
|
&self.midi_outs
|
||||||
|
}
|
||||||
|
fn midi_outs_mut (&mut self) -> &mut Vec<Port<MidiOut>> {
|
||||||
|
&mut self.midi_outs
|
||||||
|
}
|
||||||
|
fn midi_note (&mut self) -> &mut Vec<u8> {
|
||||||
|
&mut self.note_buf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hosts the JACK callback for a single MIDI player
|
||||||
|
pub struct PlayerAudio<'a, T: MidiPlayerApi>(
|
||||||
|
/// Player
|
||||||
|
pub &'a mut T,
|
||||||
|
/// Note buffer
|
||||||
|
pub &'a mut Vec<u8>,
|
||||||
|
/// Note chunk buffer
|
||||||
|
pub &'a mut Vec<Vec<Vec<u8>>>,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// JACK process callback for a sequencer's phrase player/recorder.
|
||||||
|
impl<'a, T: MidiPlayerApi> Audio for PlayerAudio<'a, T> {
|
||||||
|
fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
|
||||||
|
let model = &mut self.0;
|
||||||
|
let note_buf = &mut self.1;
|
||||||
|
let midi_buf = &mut self.2;
|
||||||
|
// Clear output buffer(s)
|
||||||
|
model.clear(scope, midi_buf, false);
|
||||||
|
// Write chunk of phrase to output, handle switchover
|
||||||
|
if model.play(scope, note_buf, midi_buf) {
|
||||||
|
model.switchover(scope, note_buf, midi_buf);
|
||||||
|
}
|
||||||
|
if model.has_midi_ins() {
|
||||||
|
if model.recording() || model.monitoring() {
|
||||||
|
// Record and/or monitor input
|
||||||
|
model.record(scope, midi_buf)
|
||||||
|
} else if model.has_midi_outs() && model.monitoring() {
|
||||||
|
// Monitor input to output
|
||||||
|
model.monitor(scope, midi_buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Write to output port(s)
|
||||||
|
model.write(scope, midi_buf);
|
||||||
|
Control::Continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MidiRecordApi for PhrasePlayerModel {
|
||||||
|
fn recording (&self) -> bool {
|
||||||
|
self.recording
|
||||||
|
}
|
||||||
|
fn recording_mut (&mut self) -> &mut bool {
|
||||||
|
&mut self.recording
|
||||||
|
}
|
||||||
|
fn monitoring (&self) -> bool {
|
||||||
|
self.monitoring
|
||||||
|
}
|
||||||
|
fn monitoring_mut (&mut self) -> &mut bool {
|
||||||
|
&mut self.monitoring
|
||||||
|
}
|
||||||
|
fn overdub (&self) -> bool {
|
||||||
|
self.overdub
|
||||||
|
}
|
||||||
|
fn overdub_mut (&mut self) -> &mut bool {
|
||||||
|
&mut self.overdub
|
||||||
|
}
|
||||||
|
fn notes_in (&self) -> &Arc<RwLock<[bool; 128]>> {
|
||||||
|
&self.notes_in
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MidiPlaybackApi for PhrasePlayerModel {
|
||||||
|
fn notes_out (&self) -> &Arc<RwLock<[bool; 128]>> {
|
||||||
|
&self.notes_in
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasPlayPhrase for PhrasePlayerModel {
|
||||||
|
fn reset (&self) -> bool {
|
||||||
|
self.reset
|
||||||
|
}
|
||||||
|
fn reset_mut (&mut self) -> &mut bool {
|
||||||
|
&mut self.reset
|
||||||
|
}
|
||||||
|
fn play_phrase (&self) -> &Option<(Moment, Option<Arc<RwLock<Phrase>>>)> {
|
||||||
|
&self.play_phrase
|
||||||
|
}
|
||||||
|
fn play_phrase_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<Phrase>>>)> {
|
||||||
|
&mut self.play_phrase
|
||||||
|
}
|
||||||
|
fn next_phrase (&self) -> &Option<(Moment, Option<Arc<RwLock<Phrase>>>)> {
|
||||||
|
&self.next_phrase
|
||||||
|
}
|
||||||
|
fn next_phrase_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<Phrase>>>)> {
|
||||||
|
&mut self.next_phrase
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//#[derive(Debug)]
|
||||||
|
//pub struct MIDIPlayer {
|
||||||
|
///// Global timebase
|
||||||
|
//pub clock: Arc<Clock>,
|
||||||
|
///// Start time and phrase being played
|
||||||
|
//pub play_phrase: Option<(Moment, Option<Arc<RwLock<Phrase>>>)>,
|
||||||
|
///// Start time and next phrase
|
||||||
|
//pub next_phrase: Option<(Moment, Option<Arc<RwLock<Phrase>>>)>,
|
||||||
|
///// Play input through output.
|
||||||
|
//pub monitoring: bool,
|
||||||
|
///// Write input to sequence.
|
||||||
|
//pub recording: bool,
|
||||||
|
///// Overdub input to sequence.
|
||||||
|
//pub overdub: bool,
|
||||||
|
///// Send all notes off
|
||||||
|
//pub reset: bool, // TODO?: after Some(nframes)
|
||||||
|
///// Record from MIDI ports to current sequence.
|
||||||
|
//pub midi_inputs: Vec<Port<MidiIn>>,
|
||||||
|
///// Play from current sequence to MIDI ports
|
||||||
|
//pub midi_outputs: Vec<Port<MidiOut>>,
|
||||||
|
///// MIDI output buffer
|
||||||
|
//pub midi_note: Vec<u8>,
|
||||||
|
///// MIDI output buffer
|
||||||
|
//pub midi_chunk: Vec<Vec<Vec<u8>>>,
|
||||||
|
///// Notes currently held at input
|
||||||
|
//pub notes_in: Arc<RwLock<[bool; 128]>>,
|
||||||
|
///// Notes currently held at output
|
||||||
|
//pub notes_out: Arc<RwLock<[bool; 128]>>,
|
||||||
|
//}
|
||||||
|
|
||||||
|
///// Methods used primarily by the process callback
|
||||||
|
//impl MIDIPlayer {
|
||||||
|
//pub fn new (
|
||||||
|
//jack: &Arc<RwLock<JackClient>>,
|
||||||
|
//clock: &Arc<Clock>,
|
||||||
|
//name: &str
|
||||||
|
//) -> Usually<Self> {
|
||||||
|
//let jack = jack.read().unwrap();
|
||||||
|
//Ok(Self {
|
||||||
|
//clock: clock.clone(),
|
||||||
|
//phrase: None,
|
||||||
|
//next_phrase: None,
|
||||||
|
//notes_in: Arc::new(RwLock::new([false;128])),
|
||||||
|
//notes_out: Arc::new(RwLock::new([false;128])),
|
||||||
|
//monitoring: false,
|
||||||
|
//recording: false,
|
||||||
|
//overdub: true,
|
||||||
|
//reset: true,
|
||||||
|
//midi_note: Vec::with_capacity(8),
|
||||||
|
//midi_chunk: vec![Vec::with_capacity(16);16384],
|
||||||
|
//midi_outputs: vec![
|
||||||
|
//jack.client().register_port(format!("{name}_out0").as_str(), MidiOut::default())?
|
||||||
|
//],
|
||||||
|
//midi_inputs: vec![
|
||||||
|
//jack.client().register_port(format!("{name}_in0").as_str(), MidiIn::default())?
|
||||||
|
//],
|
||||||
|
//})
|
||||||
|
//}
|
||||||
|
//}
|
||||||
75
crates/tek/src/midi/midi_rec.rs
Normal file
75
crates/tek/src/midi/midi_rec.rs
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
pub trait MidiRecordApi: HasClock + HasPlayPhrase + HasMidiIns {
|
||||||
|
fn notes_in (&self) -> &Arc<RwLock<[bool;128]>>;
|
||||||
|
|
||||||
|
fn recording (&self) -> bool;
|
||||||
|
fn recording_mut (&mut self) -> &mut bool;
|
||||||
|
fn toggle_record (&mut self) {
|
||||||
|
*self.recording_mut() = !self.recording();
|
||||||
|
}
|
||||||
|
fn record (&mut self, scope: &ProcessScope, midi_buf: &mut Vec<Vec<Vec<u8>>>) {
|
||||||
|
let sample0 = scope.last_frame_time() as usize;
|
||||||
|
// For highlighting keys and note repeat
|
||||||
|
let notes_in = self.notes_in().clone();
|
||||||
|
if self.clock().is_rolling() {
|
||||||
|
if let Some((started, ref phrase)) = self.play_phrase().clone() {
|
||||||
|
let start = started.sample.get() as usize;
|
||||||
|
let quant = self.clock().quant.get();
|
||||||
|
let timebase = self.clock().timebase().clone();
|
||||||
|
let monitoring = self.monitoring();
|
||||||
|
let recording = self.recording();
|
||||||
|
for input in self.midi_ins_mut().iter() {
|
||||||
|
for (sample, event, bytes) in parse_midi_input(input.iter(scope)) {
|
||||||
|
if let LiveEvent::Midi { message, .. } = event {
|
||||||
|
if monitoring {
|
||||||
|
midi_buf[sample].push(bytes.to_vec())
|
||||||
|
}
|
||||||
|
if recording {
|
||||||
|
if let Some(phrase) = phrase {
|
||||||
|
let mut phrase = phrase.write().unwrap();
|
||||||
|
let length = phrase.length;
|
||||||
|
phrase.record_event({
|
||||||
|
let sample = (sample0 + sample - start) as f64;
|
||||||
|
let pulse = timebase.samples_to_pulse(sample);
|
||||||
|
let quantized = (pulse / quant).round() * quant;
|
||||||
|
let looped = quantized as usize % length;
|
||||||
|
looped
|
||||||
|
}, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
update_keys(&mut*notes_in.write().unwrap(), &message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some((start_at, phrase)) = &self.next_phrase() {
|
||||||
|
// TODO switch to next phrase and record into it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn monitoring (&self) -> bool;
|
||||||
|
fn monitoring_mut (&mut self) -> &mut bool;
|
||||||
|
fn toggle_monitor (&mut self) {
|
||||||
|
*self.monitoring_mut() = !self.monitoring();
|
||||||
|
}
|
||||||
|
fn monitor (&mut self, scope: &ProcessScope, midi_buf: &mut Vec<Vec<Vec<u8>>>) {
|
||||||
|
// For highlighting keys and note repeat
|
||||||
|
let notes_in = self.notes_in().clone();
|
||||||
|
for input in self.midi_ins_mut().iter() {
|
||||||
|
for (sample, event, bytes) in parse_midi_input(input.iter(scope)) {
|
||||||
|
if let LiveEvent::Midi { message, .. } = event {
|
||||||
|
midi_buf[sample].push(bytes.to_vec());
|
||||||
|
update_keys(&mut*notes_in.write().unwrap(), &message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overdub (&self) -> bool;
|
||||||
|
fn overdub_mut (&mut self) -> &mut bool;
|
||||||
|
fn toggle_overdub (&mut self) {
|
||||||
|
*self.overdub_mut() = !self.overdub();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1121,3 +1121,66 @@ pub fn arranger_content_horizontal (
|
||||||
//)
|
//)
|
||||||
//)
|
//)
|
||||||
//}
|
//}
|
||||||
|
|
||||||
|
//impl Command<ArrangerModel> for ArrangerSceneCommand {
|
||||||
|
//}
|
||||||
|
//Edit(phrase) => { state.state.phrase = phrase.clone() },
|
||||||
|
//ToggleViewMode => { state.state.mode.to_next(); },
|
||||||
|
//Delete => { state.state.delete(); },
|
||||||
|
//Activate => { state.state.activate(); },
|
||||||
|
//ZoomIn => { state.state.zoom_in(); },
|
||||||
|
//ZoomOut => { state.state.zoom_out(); },
|
||||||
|
//MoveBack => { state.state.move_back(); },
|
||||||
|
//MoveForward => { state.state.move_forward(); },
|
||||||
|
//RandomColor => { state.state.randomize_color(); },
|
||||||
|
//Put => { state.state.phrase_put(); },
|
||||||
|
//Get => { state.state.phrase_get(); },
|
||||||
|
//AddScene => { state.state.scene_add(None, None)?; },
|
||||||
|
//AddTrack => { state.state.track_add(None, None)?; },
|
||||||
|
//ToggleLoop => { state.state.toggle_loop() },
|
||||||
|
//pub fn zoom_in (&mut self) {
|
||||||
|
//if let ArrangerEditorMode::Vertical(factor) = self.mode {
|
||||||
|
//self.mode = ArrangerEditorMode::Vertical(factor + 1)
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//pub fn zoom_out (&mut self) {
|
||||||
|
//if let ArrangerEditorMode::Vertical(factor) = self.mode {
|
||||||
|
//self.mode = ArrangerEditorMode::Vertical(factor.saturating_sub(1))
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//pub fn move_back (&mut self) {
|
||||||
|
//match self.selected {
|
||||||
|
//ArrangerEditorFocus::Scene(s) => {
|
||||||
|
//if s > 0 {
|
||||||
|
//self.scenes.swap(s, s - 1);
|
||||||
|
//self.selected = ArrangerEditorFocus::Scene(s - 1);
|
||||||
|
//}
|
||||||
|
//},
|
||||||
|
//ArrangerEditorFocus::Track(t) => {
|
||||||
|
//if t > 0 {
|
||||||
|
//self.tracks.swap(t, t - 1);
|
||||||
|
//self.selected = ArrangerEditorFocus::Track(t - 1);
|
||||||
|
//// FIXME: also swap clip order in scenes
|
||||||
|
//}
|
||||||
|
//},
|
||||||
|
//_ => todo!("arrangement: move forward")
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//pub fn move_forward (&mut self) {
|
||||||
|
//match self.selected {
|
||||||
|
//ArrangerEditorFocus::Scene(s) => {
|
||||||
|
//if s < self.scenes.len().saturating_sub(1) {
|
||||||
|
//self.scenes.swap(s, s + 1);
|
||||||
|
//self.selected = ArrangerEditorFocus::Scene(s + 1);
|
||||||
|
//}
|
||||||
|
//},
|
||||||
|
//ArrangerEditorFocus::Track(t) => {
|
||||||
|
//if t < self.tracks.len().saturating_sub(1) {
|
||||||
|
//self.tracks.swap(t, t + 1);
|
||||||
|
//self.selected = ArrangerEditorFocus::Track(t + 1);
|
||||||
|
//// FIXME: also swap clip order in scenes
|
||||||
|
//}
|
||||||
|
//},
|
||||||
|
//_ => todo!("arrangement: move forward")
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue