diff --git a/src/config.rs b/src/config.rs index e3ee9014..99327030 100644 --- a/src/config.rs +++ b/src/config.rs @@ -34,13 +34,10 @@ pub struct Config { pub watch: RwLock> } -impl AsRef for Config { - fn as_ref (&self) -> &Config { - self - } -} +impl_as_ref!(Config: |self: Config| self); impl Config { + /// Write initial contents of configuration. pub fn init > (config: C) -> Usually { if config.as_ref().find_file().is_none() { @@ -110,21 +107,6 @@ impl Config { Err(format!("no config path").into()) } } -} - -impl Config { - - /// Default configuration directory. - const CONFIG_DIR: &'static str = "tek"; - - /// Default configuration subdirectory. - const CONFIG_SUB: &'static str = "v0"; - - /// Default configuration file name. - const CONFIG: &'static str = "tek.edn"; - - /// Default configuration contents. - const DEFAULTS: &'static str = include_str!("tek.edn"); /// Create a new app configuration from a set of XDG base directories, pub fn new (dirs: Option) -> Self { @@ -186,4 +168,16 @@ impl Config { self.views.try_read().map(|views|views.get(name.as_ref()).cloned()).flatten() } + /// Default configuration directory. + const CONFIG_DIR: &'static str = "tek"; + + /// Default configuration subdirectory. + const CONFIG_SUB: &'static str = "v0"; + + /// Default configuration file name. + const CONFIG: &'static str = "tek.edn"; + + /// Default configuration contents. + const DEFAULTS: &'static str = include_str!("tek.edn"); + } diff --git a/src/midi_clip.rs b/src/midi_clip.rs new file mode 100644 index 00000000..9d83b97c --- /dev/null +++ b/src/midi_clip.rs @@ -0,0 +1,149 @@ +use crate::*; + +/// A MIDI sequence. +/// +/// ``` +/// let clip = tek::MidiClip::default(); +/// ``` +#[derive(Debug, Clone, Default)] pub struct MidiClip { + pub uuid: uuid::Uuid, + /// Name of clip + pub name: Arc, + /// Temporal resolution in pulses per quarter note + pub ppq: usize, + /// Length of clip in pulses + pub length: usize, + /// Notes in clip + pub notes: MidiData, + /// Whether to loop the clip or play it once + pub looped: bool, + /// Start of loop + pub loop_start: usize, + /// Length of loop + pub loop_length: usize, + /// All notes are displayed with minimum length + pub percussive: bool, + /// Identifying color of clip + pub color: ItemTheme, +} + +impl MidiClip { + pub fn new ( + name: impl AsRef, + looped: bool, + length: usize, + notes: Option, + color: Option, + ) -> Self { + Self { + uuid: uuid::Uuid::new_v4(), + name: name.as_ref().into(), + ppq: PPQ, + length, + notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]), + looped, + loop_start: 0, + loop_length: length, + percussive: true, + color: color.unwrap_or_else(ItemTheme::random) + } + } + pub fn count_midi_messages (&self) -> usize { + let mut count = 0; + for tick in self.notes.iter() { + count += tick.len(); + } + count + } + pub fn set_length (&mut self, length: usize) { + self.length = length; + self.notes = vec![Vec::with_capacity(16);length]; + } + pub fn duplicate (&self) -> Self { + let mut clone = self.clone(); + clone.uuid = uuid::Uuid::new_v4(); + clone + } + pub fn toggle_loop (&mut self) { self.looped = !self.looped; } + pub fn record_event (&mut self, pulse: usize, message: MidiMessage) { + if pulse >= self.length { panic!("extend clip first") } + self.notes[pulse].push(message); + } + /// Check if a range `start..end` contains MIDI Note On `k` + pub fn contains_note_on (&self, k: u7, start: usize, end: usize) -> bool { + for events in self.notes[start.max(0)..end.min(self.notes.len())].iter() { + for event in events.iter() { + if let MidiMessage::NoteOn {key,..} = event { if *key == k { return true } } + } + } + false + } + pub fn stop_all () -> Self { + Self::new( + "Stop", + false, + 1, + Some(vec![vec![MidiMessage::Controller { + controller: 123.into(), + value: 0.into() + }]]), + Some(ItemColor::from_tui(Color::Rgb(32, 32, 32)).into()) + ) + } +} + +pub type MidiData = Vec>; + +#[macro_export] macro_rules! has_clip { + (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? HasMidiClip for $Struct $(<$($L),*$($T),*>)? { + fn clip (&$self) -> Option>> { $cb } + } + } +} + +pub trait HasMidiClip { + fn clip (&self) -> Option>>; +} + +impl HasMidiClip for App { + fn clip (&self) -> Option>> { + None + } +} + +impl> + + Namespace> +> MidiClipController for T {} + +#[tek_proc::commands(MidiClipCommand)] +pub trait MidiClipController: HasMidiClip + + Namespace> + + Namespace> +{ + + #[command(SetColor = "clip/color")] + fn clip_set_color (&mut self, _color: Option) -> Perhaps { + //(SetColor [t: usize, s: usize, c: ItemTheme] + //clip.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o))))); + //("color" [a: usize, b: usize] Some(Self::SetColor(a.unwrap(), b.unwrap(), ItemTheme::random()))) + todo!() + } + + #[command(SetLoop = "clip/loop")] + fn clip_toggle_loop (&mut self, _looping: Option) -> Perhaps { + //(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}")) + //("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap()))) + todo!() + } + +} + +impl PartialEq for MidiClip { + fn eq (&self, other: &Self) -> bool { + self.uuid == other.uuid + } +} + +impl Eq for MidiClip {} diff --git a/src/midi_clips.rs b/src/midi_clips.rs new file mode 100644 index 00000000..9ff62652 --- /dev/null +++ b/src/midi_clips.rs @@ -0,0 +1,28 @@ +use crate::*; + +pub type ClipPool = Vec>>; + +pub trait HasClips { + fn clips <'a> (&'a self) -> RwLockReadGuard<'a, ClipPool>; + fn clips_mut <'a> (&'a self) -> RwLockWriteGuard<'a, ClipPool>; + fn add_clip (&self) -> (usize, Arc>) { + let clip = Arc::new(RwLock::new(MidiClip::new("Clip", true, 384, None, None))); + self.clips_mut().push(clip.clone()); + (self.clips().len() - 1, clip) + } +} + +/// TODO: Preserve the generic passthru syntax; +/// remove this macro (only used twice) and potentially the trait. +#[macro_export] macro_rules! impl_has_clips { + (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? { + fn clips <'a> (&'a $self) -> ::tengri::parking_lot::RwLockReadGuard<'a, ClipPool> { + $cb.try_read().unwrap() + } + fn clips_mut <'a> (&'a $self) -> ::tengri::parking_lot::RwLockWriteGuard<'a, ClipPool> { + $cb.try_write().unwrap() + } + } + } +} diff --git a/src/midi_mon.rs b/src/midi_mon.rs new file mode 100644 index 00000000..bbf7a4ec --- /dev/null +++ b/src/midi_mon.rs @@ -0,0 +1,25 @@ +use crate::*; + +pub trait HasMidiBuffers { + fn note_buf_mut (&mut self) -> &mut Vec; + fn midi_buf_mut (&mut self) -> &mut Vec>>; +} + +pub trait MidiMonitor: HasMidiIns + HasMidiBuffers { + /// Input note flags. + fn notes_in (&self) -> &Arc>; + /// Current monitoring status. + fn monitoring (&self) -> bool; + /// Mutable monitoring status. + fn monitoring_mut (&mut self) -> &mut bool; + /// Enable or disable monitoring. + fn toggle_monitor (&mut self) { *self.monitoring_mut() = !self.monitoring(); } + /// Perform monitoring. + fn monitor (&mut self, _scope: &ProcessScope) { /* do nothing by default */ } +} + +impl MidiMonitor for Sequencer { + fn monitoring (&self) -> bool { self.monitoring } + fn monitoring_mut (&mut self) -> &mut bool { &mut self.monitoring } + fn notes_in (&self) -> &Arc> { &self.notes_in } +} diff --git a/src/midi_play.rs b/src/midi_play.rs new file mode 100644 index 00000000..0ceaa90d --- /dev/null +++ b/src/midi_play.rs @@ -0,0 +1,104 @@ +use crate::*; + +pub trait HasPlayClip: HasClock { + fn reset (&self) -> bool; + fn reset_mut (&mut self) -> &mut bool; + fn play_clip (&self) -> &Option<(Moment, Option>>)>; + fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)>; + fn next_clip (&self) -> &Option<(Moment, Option>>)>; + fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)>; + + fn pulses_since_start (&self) -> Option { + if let Some((started, Some(_))) = self.play_clip().as_ref() { + let elapsed = self.clock().playhead.pulse.get() - started.pulse.get(); + return Some(elapsed) + } + None + } + + fn pulses_since_start_looped (&self) -> Option<(f64, f64)> { + if let Some((started, Some(clip))) = self.play_clip().as_ref() { + let elapsed = self.clock().playhead.pulse.get() - started.pulse.get(); + let length = clip.try_read().unwrap().length.max(1); // prevent div0 on empty clip + let times = (elapsed as usize / length) as f64; + let elapsed = (elapsed as usize % length) as f64; + return Some((times, elapsed)) + } + None + } + + fn enqueue_next (&mut self, clip: Option<&Arc>>) { + *self.next_clip_mut() = Some((self.clock().next_launch_instant(), clip.cloned())); + *self.reset_mut() = true; + } + + fn play_status (&self) -> impl Draw { + let (name, color): (Arc, ItemTheme) = if let Some((_, Some(clip))) = self.play_clip() { + let MidiClip { ref name, color, .. } = *clip.try_read().unwrap(); + (name.clone(), color) + } else { + ("".into(), ItemTheme::G[64].into()) + }; + field_v(color, "Now:", format!("{} {}", self.play_status_time(), name)) + } + + fn play_status_time (&self) -> String { + self.pulses_since_start_looped() + .map(|(times, time)|format!("{:>3}x {:>}", times+1.0, self.clock().timebase.format_beats_1(time))) + .unwrap_or_else(||String::from(" ")) + .into() + } + + fn next_status (&self) -> impl Draw { + let mut time: Arc = String::from("--.-.--").into(); + let mut name: Arc = String::from("").into(); + let mut color = ItemTheme::G[64]; + let clock = self.clock(); + if let Some((t, Some(clip))) = self.next_clip() { + let clip = clip.try_read().unwrap(); + name = clip.name.clone(); + color = clip.color.clone(); + time = { + let target = t.pulse.get(); + let current = clock.playhead.pulse.get(); + if target > current { + let remaining = target - current; + format!("-{:>}", clock.timebase.format_beats_1(remaining)) + } else { + String::new() + } + }.into() + } else if let Some((t, Some(clip))) = self.play_clip() { + let clip = clip.try_read().unwrap(); + if clip.looped { + name = clip.name.clone(); + color = clip.color.clone(); + let target = t.pulse.get() + clip.length as f64; + let current = clock.playhead.pulse.get(); + if target > current { + time = format!("-{:>}", clock.timebase.format_beats_0(target - current)).into() + } + } else { + name = "Stop".to_string().into(); + } + }; + field_v(color, "Next:", format!("{} {}", time, name)) + } +} + +#[cfg(feature="clip")] impl HasPlayClip for Sequencer { + fn reset (&self) -> bool { self.reset } + fn reset_mut (&mut self) -> &mut bool { &mut self.reset } + fn play_clip (&self) -> &Option<(Moment, Option>>)> { + &self.play_clip + } + fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)> { + &mut self.play_clip + } + fn next_clip (&self) -> &Option<(Moment, Option>>)> { + &self.next_clip + } + fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)> { + &mut self.next_clip + } +} diff --git a/src/midi_rec.rs b/src/midi_rec.rs new file mode 100644 index 00000000..91e469b3 --- /dev/null +++ b/src/midi_rec.rs @@ -0,0 +1,55 @@ +use crate::*; + +pub trait MidiRecord: MidiMonitor + HasClock + HasPlayClip { + fn recording (&self) -> bool; + fn recording_mut (&mut self) -> &mut bool; + fn toggle_record (&mut self) { + *self.recording_mut() = !self.recording(); + } + + fn overdub (&self) -> bool; + fn overdub_mut (&mut self) -> &mut bool; + fn toggle_overdub (&mut self) { + *self.overdub_mut() = !self.overdub(); + } + + fn record_clip ( + &mut self, + scope: &ProcessScope, + started: Moment, + clip: &Option>>, + ) { + if let Some(clip) = clip { + let sample0 = scope.last_frame_time() as usize; + let start = started.sample.get() as usize; + let _recording = self.recording(); + let timebase = self.clock().timebase().clone(); + let quant = self.clock().quant.get(); + let mut clip = clip.try_write().unwrap(); + let length = clip.length; + for input in self.midi_ins_mut().iter() { + for (sample, event, _bytes) in parse_midi_input(input.port().iter(scope)) { + if let LiveEvent::Midi { message, .. } = event { + clip.record_event({ + let sample = (sample0 + sample - start) as f64; + let pulse = timebase.samples_to_pulse(sample); + let quantized = (pulse / quant).round() * quant; + quantized as usize % length + }, message); + } + } + } + } + } + + fn record_next (&mut self) { + // TODO switch to next clip and record into it + } +} + +impl MidiRecord for Sequencer { + fn recording (&self) -> bool { self.recording } + fn recording_mut (&mut self) -> &mut bool { &mut self.recording } + fn overdub (&self) -> bool { self.overdub } + fn overdub_mut (&mut self) -> &mut bool { &mut self.overdub } +} diff --git a/src/midi_seq.rs b/src/midi_seq.rs new file mode 100644 index 00000000..f40bb97d --- /dev/null +++ b/src/midi_seq.rs @@ -0,0 +1,293 @@ +use crate::*; + +impl +AsMut> HasSequencer for T {} + +pub trait HasSequencer: AsRef + AsMut { + fn sequencer_mut (&mut self) -> &mut Sequencer { self.as_mut() } + fn sequencer (&self) -> &Sequencer { self.as_ref() } +} + +/// Contains state for playing a clip +/// +/// ``` +/// let clip = tek::MidiClip::default(); +/// println!("Empty clip: {clip:?}"); +/// +/// let clip = tek::MidiClip::stop_all(); +/// println!("Panic clip: {clip:?}"); +/// +/// let mut clip = tek::MidiClip::new("clip", true, 1, None, None); +/// clip.set_length(96); +/// clip.toggle_loop(); +/// clip.record_event(12, ::tengri::midly::MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }); +/// assert!(clip.contains_note_on(36.into(), 6, 18)); +/// assert_eq!(&clip.notes, &clip.duplicate().notes); +/// +/// let clip = std::sync::Arc::new(clip); +/// assert_eq!(clip.clone(), clip); +/// +/// let sequencer = tek::Sequencer::default(); +/// println!("{sequencer:?}"); +/// ``` +pub struct Sequencer { + /// State of clock and playhead + #[cfg(feature = "clock")] pub clock: Clock, + /// Start time and clip being played + #[cfg(feature = "clip")] pub play_clip: Option<(Moment, Option>>)>, + /// Start time and next clip + #[cfg(feature = "clip")] pub next_clip: Option<(Moment, Option>>)>, + /// Record from MIDI ports to current sequence. + #[cfg(feature = "port")] pub midi_ins: Vec, + /// Play from current sequence to MIDI ports + #[cfg(feature = "port")] pub midi_outs: Vec, + /// 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) + /// Notes currently held at input + pub notes_in: Arc>, + /// Notes currently held at output + pub notes_out: Arc>, + /// MIDI output buffer + pub note_buf: Vec, + /// MIDI output buffer + pub midi_buf: Vec>>, +} + +impl_has!(Clock: |self: Sequencer| self.clock); +impl_has!(Vec: |self: Sequencer| self.midi_ins); +impl_has!(Vec: |self: Sequencer| self.midi_outs); +impl_default!(Sequencer: Self { + clock: Clock::default(), + play_clip: None, + next_clip: None, + midi_ins: vec![], + midi_outs: vec![], + recording: false, + monitoring: true, + overdub: false, + notes_in: RwLock::new([false;128]).into(), + notes_out: RwLock::new([false;128]).into(), + note_buf: vec![0;8], + midi_buf: vec![], + reset: true, +}); + +impl Sequencer { + pub fn new ( + name: impl AsRef, + jack: &Jack<'static>, + #[cfg(feature = "clock")] clock: Option<&Clock>, + #[cfg(feature = "clip")] clip: Option<&Arc>>, + #[cfg(feature = "port")] midi_from: &[Connect], + #[cfg(feature = "port")] midi_to: &[Connect], + ) -> Usually { + let _name = name.as_ref(); + #[cfg(feature = "clock")] let clock = clock.cloned().unwrap_or_default(); + Ok(Self { + reset: true, + notes_in: RwLock::new([false;128]).into(), + notes_out: RwLock::new([false;128]).into(), + #[cfg(feature = "port")] midi_ins: vec![MidiInput::new(jack, &format!("M/{}", name.as_ref()), midi_from)?,], + #[cfg(feature = "port")] midi_outs: vec![MidiOutput::new(jack, &format!("{}/M", name.as_ref()), midi_to)?, ], + #[cfg(feature = "clip")] play_clip: clip.map(|clip|(Moment::zero(&clock.timebase), Some(clip.clone()))), + #[cfg(feature = "clock")] clock, + ..Default::default() + }) + } + fn process_rolling (&mut self, scope: &ProcessScope) -> Control { + self.process_clear(scope, false); + // Write chunk of clip to output, handle switchover + if self.process_playback(scope) { + self.process_switchover(scope); + } + // Monitor input to output + self.process_monitoring(scope); + // Record and/or monitor input + self.process_recording(scope); + // Emit contents of MIDI buffers to JACK MIDI output ports. + self.midi_outs_emit(scope); + Control::Continue + } + fn process_stopped (&mut self, scope: &ProcessScope) -> Control { + if self.monitoring() && self.midi_ins().len() > 0 && self.midi_outs().len() > 0 { + self.process_monitoring(scope) + } + Control::Continue + } + fn process_monitoring (&mut self, scope: &ProcessScope) { + let notes_in = self.notes_in().clone(); // For highlighting keys and note repeat + let monitoring = self.monitoring(); + for input in self.midi_ins.iter() { + for (sample, event, bytes) in input.parsed(scope) { + if let LiveEvent::Midi { message, .. } = event { + if monitoring { + self.midi_buf[sample].push(bytes.to_vec()); + } + // FIXME: don't lock on every event! + update_keys(&mut notes_in.try_write().unwrap(), &message); + } + } + } + } + /// Clear the section of the output buffer that we will be using, + /// emitting "all notes off" at start of buffer if requested. + fn process_clear (&mut self, scope: &ProcessScope, reset: bool) { + let n_frames = (scope.n_frames() as usize).min(self.midi_buf_mut().len()); + for frame in &mut self.midi_buf_mut()[0..n_frames] { + frame.clear(); + } + if reset { + all_notes_off(self.midi_buf_mut()); + } + for port in self.midi_outs_mut().iter_mut() { + // Clear output buffer(s) + port.buffer_clear(scope, false); + } + } + fn process_recording (&mut self, scope: &ProcessScope) { + if self.monitoring() { + self.monitor(scope); + } + if let Some((started, ref clip)) = self.play_clip.clone() { + self.record_clip(scope, started, clip); + } + if let Some((_start_at, _clip)) = &self.next_clip() { + self.record_next(); + } + } + fn process_playback (&mut self, scope: &ProcessScope) -> bool { + // If a clip is playing, write a chunk of MIDI events from it to the output buffer. + // If no clip is playing, prepare for switchover immediately. + if let Some((started, clip)) = &self.play_clip { + // Length of clip, to repeat or stop on end. + let length = clip.as_ref().map_or(0, |p|p.try_read().unwrap().length); + // Index of first sample to populate. + let offset = self.clock().get_sample_offset(scope, &started); + // Write MIDI events from clip at sample offsets corresponding to pulses. + for (sample, pulse) in self.clock().get_pulses(scope, offset) { + // If a next clip is enqueued, and we're past the end of the current one, + // break the loop here (FIXME count pulse correctly) + let past_end = if clip.is_some() { pulse >= length } else { true }; + // Is it time for switchover? + if self.next_clip().is_some() && past_end { + return true + } + // If there's a currently playing clip, output notes from it to buffer: + if let Some(clip) = clip { + // Source clip from which the MIDI events will be taken. + let clip = clip.try_read().unwrap(); + // Clip with zero length is not processed + if clip.length > 0 { + // Current pulse index in source clip + let pulse = pulse % clip.length; + // Output each MIDI event from clip at appropriate frames of output buffer: + for message in clip.notes[pulse].iter() { + for port in self.midi_outs.iter_mut() { + port.buffer_write(sample, LiveEvent::Midi { + channel: 0.into(), /* TODO */ + message: *message + }); + } + } + } + } + } + false + } else { + true + } + } + /// Handle switchover from current to next playing clip. + fn process_switchover (&mut self, scope: &ProcessScope) { + let _midi_buf = self.midi_buf_mut(); + let sample0 = scope.last_frame_time() as usize; + //let samples = scope.n_frames() as usize; + if let Some((start_at, clip)) = &self.next_clip() { + let start = start_at.sample.get() as usize; + let sample = self.clock().started.try_read().unwrap() + .as_ref().unwrap().sample.get() as usize; + // If it's time to switch to the next clip: + if start <= sample0.saturating_sub(sample) { + // Samples elapsed since clip was supposed to start + let _skipped = sample0 - start; + // Switch over to enqueued clip + let started = Moment::from_sample(self.clock().timebase(), start as f64); + // Launch enqueued clip + *self.play_clip_mut() = Some((started, clip.clone())); + // Unset enqueuement (TODO: where to implement looping?) + *self.next_clip_mut() = None; + // Fill in remaining ticks of chunk from next clip. + self.process_playback(scope); + } + } + } +} + +impl HasMidiBuffers for Sequencer { + fn note_buf_mut (&mut self) -> &mut Vec { &mut self.note_buf } + fn midi_buf_mut (&mut self) -> &mut Vec>> { &mut self.midi_buf } +} + +impl std::fmt::Debug for Sequencer { + fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("Sequencer") + .field("clock", &self.clock) + .field("play_clip", &self.play_clip) + .field("next_clip", &self.next_clip) + .finish() + } +} + +/// JACK process callback for a sequencer's clip sequencer/recorder. +impl Audio for Sequencer { + fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { + if self.clock().is_rolling() { + self.process_rolling(scope) + } else { + self.process_stopped(scope) + } + } +} + +pub fn to_key (note: usize) -> &'static str { + match note % 12 { + 11 | 9 | 7 | 5 | 4 | 2 | 0 => "████▌", + 10 | 8 | 6 | 3 | 1 => " ", + _ => unreachable!(), + } +} + +pub(crate) fn note_y_iter (note_lo: usize, note_hi: usize, y0: u16) + -> impl Iterator +{ + (note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n)) +} + +/// Returns the next shorter length +pub fn note_duration_prev (pulses: usize) -> usize { + for (length, _) in NOTE_DURATIONS.iter().rev() { if *length < pulses { return *length } } + pulses +} + +/// Returns the next longer length +pub fn note_duration_next (pulses: usize) -> usize { + for (length, _) in NOTE_DURATIONS.iter() { if *length > pulses { return *length } } + pulses +} + +pub fn note_duration_to_name (pulses: usize) -> &'static str { + for (length, name) in NOTE_DURATIONS.iter() { if *length == pulses { return name } } + "" +} + +pub fn note_pitch_to_name (n: usize) -> &'static str { + if n > 127 { + panic!("to_note_name({n}): must be 0-127"); + } + NOTE_NAMES[n] +} diff --git a/src/sequence.rs b/src/sequence.rs deleted file mode 100644 index 125c64a4..00000000 --- a/src/sequence.rs +++ /dev/null @@ -1,666 +0,0 @@ -use crate::*; - -impl +AsMut> HasSequencer for T {} - -/// A MIDI sequence. -/// -/// ``` -/// let clip = tek::MidiClip::default(); -/// ``` -#[derive(Debug, Clone, Default)] pub struct MidiClip { - pub uuid: uuid::Uuid, - /// Name of clip - pub name: Arc, - /// Temporal resolution in pulses per quarter note - pub ppq: usize, - /// Length of clip in pulses - pub length: usize, - /// Notes in clip - pub notes: MidiData, - /// Whether to loop the clip or play it once - pub looped: bool, - /// Start of loop - pub loop_start: usize, - /// Length of loop - pub loop_length: usize, - /// All notes are displayed with minimum length - pub percussive: bool, - /// Identifying color of clip - pub color: ItemTheme, -} - -/// Contains state for playing a clip -/// -/// ``` -/// let clip = tek::MidiClip::default(); -/// println!("Empty clip: {clip:?}"); -/// -/// let clip = tek::MidiClip::stop_all(); -/// println!("Panic clip: {clip:?}"); -/// -/// let mut clip = tek::MidiClip::new("clip", true, 1, None, None); -/// clip.set_length(96); -/// clip.toggle_loop(); -/// clip.record_event(12, ::tengri::midly::MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }); -/// assert!(clip.contains_note_on(36.into(), 6, 18)); -/// assert_eq!(&clip.notes, &clip.duplicate().notes); -/// -/// let clip = std::sync::Arc::new(clip); -/// assert_eq!(clip.clone(), clip); -/// -/// let sequencer = tek::Sequencer::default(); -/// println!("{sequencer:?}"); -/// ``` -pub struct Sequencer { - /// State of clock and playhead - #[cfg(feature = "clock")] pub clock: Clock, - /// Start time and clip being played - #[cfg(feature = "clip")] pub play_clip: Option<(Moment, Option>>)>, - /// Start time and next clip - #[cfg(feature = "clip")] pub next_clip: Option<(Moment, Option>>)>, - /// Record from MIDI ports to current sequence. - #[cfg(feature = "port")] pub midi_ins: Vec, - /// Play from current sequence to MIDI ports - #[cfg(feature = "port")] pub midi_outs: Vec, - /// 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) - /// Notes currently held at input - pub notes_in: Arc>, - /// Notes currently held at output - pub notes_out: Arc>, - /// MIDI output buffer - pub note_buf: Vec, - /// MIDI output buffer - pub midi_buf: Vec>>, -} - - -pub trait HasPlayClip: HasClock { - fn reset (&self) -> bool; - fn reset_mut (&mut self) -> &mut bool; - fn play_clip (&self) -> &Option<(Moment, Option>>)>; - fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)>; - fn next_clip (&self) -> &Option<(Moment, Option>>)>; - fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)>; - - fn pulses_since_start (&self) -> Option { - if let Some((started, Some(_))) = self.play_clip().as_ref() { - let elapsed = self.clock().playhead.pulse.get() - started.pulse.get(); - return Some(elapsed) - } - None - } - - fn pulses_since_start_looped (&self) -> Option<(f64, f64)> { - if let Some((started, Some(clip))) = self.play_clip().as_ref() { - let elapsed = self.clock().playhead.pulse.get() - started.pulse.get(); - let length = clip.try_read().unwrap().length.max(1); // prevent div0 on empty clip - let times = (elapsed as usize / length) as f64; - let elapsed = (elapsed as usize % length) as f64; - return Some((times, elapsed)) - } - None - } - - fn enqueue_next (&mut self, clip: Option<&Arc>>) { - *self.next_clip_mut() = Some((self.clock().next_launch_instant(), clip.cloned())); - *self.reset_mut() = true; - } - - fn play_status (&self) -> impl Draw { - let (name, color): (Arc, ItemTheme) = if let Some((_, Some(clip))) = self.play_clip() { - let MidiClip { ref name, color, .. } = *clip.try_read().unwrap(); - (name.clone(), color) - } else { - ("".into(), ItemTheme::G[64].into()) - }; - field_v(color, "Now:", format!("{} {}", self.play_status_time(), name)) - } - - fn play_status_time (&self) -> String { - self.pulses_since_start_looped() - .map(|(times, time)|format!("{:>3}x {:>}", times+1.0, self.clock().timebase.format_beats_1(time))) - .unwrap_or_else(||String::from(" ")) - .into() - } - - fn next_status (&self) -> impl Draw { - let mut time: Arc = String::from("--.-.--").into(); - let mut name: Arc = String::from("").into(); - let mut color = ItemTheme::G[64]; - let clock = self.clock(); - if let Some((t, Some(clip))) = self.next_clip() { - let clip = clip.try_read().unwrap(); - name = clip.name.clone(); - color = clip.color.clone(); - time = { - let target = t.pulse.get(); - let current = clock.playhead.pulse.get(); - if target > current { - let remaining = target - current; - format!("-{:>}", clock.timebase.format_beats_1(remaining)) - } else { - String::new() - } - }.into() - } else if let Some((t, Some(clip))) = self.play_clip() { - let clip = clip.try_read().unwrap(); - if clip.looped { - name = clip.name.clone(); - color = clip.color.clone(); - let target = t.pulse.get() + clip.length as f64; - let current = clock.playhead.pulse.get(); - if target > current { - time = format!("-{:>}", clock.timebase.format_beats_0(target - current)).into() - } - } else { - name = "Stop".to_string().into(); - } - }; - field_v(color, "Next:", format!("{} {}", time, name)) - } -} - -pub trait MidiMonitor: HasMidiIns + HasMidiBuffers { - /// Input note flags. - fn notes_in (&self) -> &Arc>; - /// Current monitoring status. - fn monitoring (&self) -> bool; - /// Mutable monitoring status. - fn monitoring_mut (&mut self) -> &mut bool; - /// Enable or disable monitoring. - fn toggle_monitor (&mut self) { *self.monitoring_mut() = !self.monitoring(); } - /// Perform monitoring. - fn monitor (&mut self, _scope: &ProcessScope) { /* do nothing by default */ } -} - -pub trait MidiRecord: MidiMonitor + HasClock + HasPlayClip { - fn recording (&self) -> bool; - fn recording_mut (&mut self) -> &mut bool; - fn toggle_record (&mut self) { - *self.recording_mut() = !self.recording(); - } - - fn overdub (&self) -> bool; - fn overdub_mut (&mut self) -> &mut bool; - fn toggle_overdub (&mut self) { - *self.overdub_mut() = !self.overdub(); - } - - fn record_clip ( - &mut self, - scope: &ProcessScope, - started: Moment, - clip: &Option>>, - ) { - if let Some(clip) = clip { - let sample0 = scope.last_frame_time() as usize; - let start = started.sample.get() as usize; - let _recording = self.recording(); - let timebase = self.clock().timebase().clone(); - let quant = self.clock().quant.get(); - let mut clip = clip.try_write().unwrap(); - let length = clip.length; - for input in self.midi_ins_mut().iter() { - for (sample, event, _bytes) in parse_midi_input(input.port().iter(scope)) { - if let LiveEvent::Midi { message, .. } = event { - clip.record_event({ - let sample = (sample0 + sample - start) as f64; - let pulse = timebase.samples_to_pulse(sample); - let quantized = (pulse / quant).round() * quant; - quantized as usize % length - }, message); - } - } - } - } - } - - fn record_next (&mut self) { - // TODO switch to next clip and record into it - } -} - -pub type MidiData = Vec>; - -pub type ClipPool = Vec>>; - -pub trait HasClips { - fn clips <'a> (&'a self) -> RwLockReadGuard<'a, ClipPool>; - fn clips_mut <'a> (&'a self) -> RwLockWriteGuard<'a, ClipPool>; - fn add_clip (&self) -> (usize, Arc>) { - let clip = Arc::new(RwLock::new(MidiClip::new("Clip", true, 384, None, None))); - self.clips_mut().push(clip.clone()); - (self.clips().len() - 1, clip) - } -} - -/// TODO: Preserve the generic passthru syntax; -/// remove this macro (only used twice) and potentially the trait. -#[macro_export] macro_rules! impl_has_clips { - (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? { - fn clips <'a> (&'a $self) -> ::tengri::parking_lot::RwLockReadGuard<'a, ClipPool> { - $cb.try_read().unwrap() - } - fn clips_mut <'a> (&'a $self) -> ::tengri::parking_lot::RwLockWriteGuard<'a, ClipPool> { - $cb.try_write().unwrap() - } - } - } -} - -#[macro_export] macro_rules! has_clip { - (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? HasMidiClip for $Struct $(<$($L),*$($T),*>)? { - fn clip (&$self) -> Option>> { $cb } - } - } -} - -pub trait HasMidiClip { - fn clip (&self) -> Option>>; -} - -impl HasMidiClip for App { - fn clip (&self) -> Option>> { - None - } -} - -impl> - + Namespace> -> MidiClipController for T {} - -#[tek_proc::commands(MidiClipCommand)] -pub trait MidiClipController: HasMidiClip - + Namespace> - + Namespace> -{ - - #[command(SetColor = "clip/color")] - fn clip_set_color (&mut self, _color: Option) -> Perhaps { - //(SetColor [t: usize, s: usize, c: ItemTheme] - //clip.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o))))); - //("color" [a: usize, b: usize] Some(Self::SetColor(a.unwrap(), b.unwrap(), ItemTheme::random()))) - todo!() - } - - #[command(SetLoop = "clip/loop")] - fn clip_toggle_loop (&mut self, _looping: Option) -> Perhaps { - //(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}")) - //("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap()))) - todo!() - } - -} - -pub trait HasSequencer: AsRef + AsMut { - fn sequencer_mut (&mut self) -> &mut Sequencer { self.as_mut() } - fn sequencer (&self) -> &Sequencer { self.as_ref() } -} - -pub trait HasMidiBuffers { - fn note_buf_mut (&mut self) -> &mut Vec; - fn midi_buf_mut (&mut self) -> &mut Vec>>; -} - -impl MidiClip { - pub fn new ( - name: impl AsRef, - looped: bool, - length: usize, - notes: Option, - color: Option, - ) -> Self { - Self { - uuid: uuid::Uuid::new_v4(), - name: name.as_ref().into(), - ppq: PPQ, - length, - notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]), - looped, - loop_start: 0, - loop_length: length, - percussive: true, - color: color.unwrap_or_else(ItemTheme::random) - } - } - pub fn count_midi_messages (&self) -> usize { - let mut count = 0; - for tick in self.notes.iter() { - count += tick.len(); - } - count - } - pub fn set_length (&mut self, length: usize) { - self.length = length; - self.notes = vec![Vec::with_capacity(16);length]; - } - pub fn duplicate (&self) -> Self { - let mut clone = self.clone(); - clone.uuid = uuid::Uuid::new_v4(); - clone - } - pub fn toggle_loop (&mut self) { self.looped = !self.looped; } - pub fn record_event (&mut self, pulse: usize, message: MidiMessage) { - if pulse >= self.length { panic!("extend clip first") } - self.notes[pulse].push(message); - } - /// Check if a range `start..end` contains MIDI Note On `k` - pub fn contains_note_on (&self, k: u7, start: usize, end: usize) -> bool { - for events in self.notes[start.max(0)..end.min(self.notes.len())].iter() { - for event in events.iter() { - if let MidiMessage::NoteOn {key,..} = event { if *key == k { return true } } - } - } - false - } - pub fn stop_all () -> Self { - Self::new( - "Stop", - false, - 1, - Some(vec![vec![MidiMessage::Controller { - controller: 123.into(), - value: 0.into() - }]]), - Some(ItemColor::from_tui(Color::Rgb(32, 32, 32)).into()) - ) - } -} - -impl PartialEq for MidiClip { - fn eq (&self, other: &Self) -> bool { - self.uuid == other.uuid - } -} - -impl Eq for MidiClip {} - -impl_has!(Sequencer: |self: Track| self.sequencer); -impl_has!(Clock: |self: Sequencer| self.clock); -impl_has!(Vec: |self: Sequencer| self.midi_ins); -impl_has!(Vec: |self: Sequencer| self.midi_outs); -impl_default!(Sequencer: Self { - clock: Clock::default(), - play_clip: None, - next_clip: None, - midi_ins: vec![], - midi_outs: vec![], - recording: false, - monitoring: true, - overdub: false, - notes_in: RwLock::new([false;128]).into(), - notes_out: RwLock::new([false;128]).into(), - note_buf: vec![0;8], - midi_buf: vec![], - reset: true, -}); -impl Sequencer { - pub fn new ( - name: impl AsRef, - jack: &Jack<'static>, - #[cfg(feature = "clock")] clock: Option<&Clock>, - #[cfg(feature = "clip")] clip: Option<&Arc>>, - #[cfg(feature = "port")] midi_from: &[Connect], - #[cfg(feature = "port")] midi_to: &[Connect], - ) -> Usually { - let _name = name.as_ref(); - #[cfg(feature = "clock")] let clock = clock.cloned().unwrap_or_default(); - Ok(Self { - reset: true, - notes_in: RwLock::new([false;128]).into(), - notes_out: RwLock::new([false;128]).into(), - #[cfg(feature = "port")] midi_ins: vec![MidiInput::new(jack, &format!("M/{}", name.as_ref()), midi_from)?,], - #[cfg(feature = "port")] midi_outs: vec![MidiOutput::new(jack, &format!("{}/M", name.as_ref()), midi_to)?, ], - #[cfg(feature = "clip")] play_clip: clip.map(|clip|(Moment::zero(&clock.timebase), Some(clip.clone()))), - #[cfg(feature = "clock")] clock, - ..Default::default() - }) - } - fn process_rolling (&mut self, scope: &ProcessScope) -> Control { - self.process_clear(scope, false); - // Write chunk of clip to output, handle switchover - if self.process_playback(scope) { - self.process_switchover(scope); - } - // Monitor input to output - self.process_monitoring(scope); - // Record and/or monitor input - self.process_recording(scope); - // Emit contents of MIDI buffers to JACK MIDI output ports. - self.midi_outs_emit(scope); - Control::Continue - } - fn process_stopped (&mut self, scope: &ProcessScope) -> Control { - if self.monitoring() && self.midi_ins().len() > 0 && self.midi_outs().len() > 0 { - self.process_monitoring(scope) - } - Control::Continue - } - fn process_monitoring (&mut self, scope: &ProcessScope) { - let notes_in = self.notes_in().clone(); // For highlighting keys and note repeat - let monitoring = self.monitoring(); - for input in self.midi_ins.iter() { - for (sample, event, bytes) in input.parsed(scope) { - if let LiveEvent::Midi { message, .. } = event { - if monitoring { - self.midi_buf[sample].push(bytes.to_vec()); - } - // FIXME: don't lock on every event! - update_keys(&mut notes_in.try_write().unwrap(), &message); - } - } - } - } - /// Clear the section of the output buffer that we will be using, - /// emitting "all notes off" at start of buffer if requested. - fn process_clear (&mut self, scope: &ProcessScope, reset: bool) { - let n_frames = (scope.n_frames() as usize).min(self.midi_buf_mut().len()); - for frame in &mut self.midi_buf_mut()[0..n_frames] { - frame.clear(); - } - if reset { - all_notes_off(self.midi_buf_mut()); - } - for port in self.midi_outs_mut().iter_mut() { - // Clear output buffer(s) - port.buffer_clear(scope, false); - } - } - fn process_recording (&mut self, scope: &ProcessScope) { - if self.monitoring() { - self.monitor(scope); - } - if let Some((started, ref clip)) = self.play_clip.clone() { - self.record_clip(scope, started, clip); - } - if let Some((_start_at, _clip)) = &self.next_clip() { - self.record_next(); - } - } - fn process_playback (&mut self, scope: &ProcessScope) -> bool { - // If a clip is playing, write a chunk of MIDI events from it to the output buffer. - // If no clip is playing, prepare for switchover immediately. - if let Some((started, clip)) = &self.play_clip { - // Length of clip, to repeat or stop on end. - let length = clip.as_ref().map_or(0, |p|p.try_read().unwrap().length); - // Index of first sample to populate. - let offset = self.clock().get_sample_offset(scope, &started); - // Write MIDI events from clip at sample offsets corresponding to pulses. - for (sample, pulse) in self.clock().get_pulses(scope, offset) { - // If a next clip is enqueued, and we're past the end of the current one, - // break the loop here (FIXME count pulse correctly) - let past_end = if clip.is_some() { pulse >= length } else { true }; - // Is it time for switchover? - if self.next_clip().is_some() && past_end { - return true - } - // If there's a currently playing clip, output notes from it to buffer: - if let Some(clip) = clip { - // Source clip from which the MIDI events will be taken. - let clip = clip.try_read().unwrap(); - // Clip with zero length is not processed - if clip.length > 0 { - // Current pulse index in source clip - let pulse = pulse % clip.length; - // Output each MIDI event from clip at appropriate frames of output buffer: - for message in clip.notes[pulse].iter() { - for port in self.midi_outs.iter_mut() { - port.buffer_write(sample, LiveEvent::Midi { - channel: 0.into(), /* TODO */ - message: *message - }); - } - } - } - } - } - false - } else { - true - } - } - /// Handle switchover from current to next playing clip. - fn process_switchover (&mut self, scope: &ProcessScope) { - let _midi_buf = self.midi_buf_mut(); - let sample0 = scope.last_frame_time() as usize; - //let samples = scope.n_frames() as usize; - if let Some((start_at, clip)) = &self.next_clip() { - let start = start_at.sample.get() as usize; - let sample = self.clock().started.try_read().unwrap() - .as_ref().unwrap().sample.get() as usize; - // If it's time to switch to the next clip: - if start <= sample0.saturating_sub(sample) { - // Samples elapsed since clip was supposed to start - let _skipped = sample0 - start; - // Switch over to enqueued clip - let started = Moment::from_sample(self.clock().timebase(), start as f64); - // Launch enqueued clip - *self.play_clip_mut() = Some((started, clip.clone())); - // Unset enqueuement (TODO: where to implement looping?) - *self.next_clip_mut() = None; - // Fill in remaining ticks of chunk from next clip. - self.process_playback(scope); - } - } - } -} -impl HasMidiBuffers for Sequencer { - fn note_buf_mut (&mut self) -> &mut Vec { &mut self.note_buf } - fn midi_buf_mut (&mut self) -> &mut Vec>> { &mut self.midi_buf } -} -impl std::fmt::Debug for Sequencer { - fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("Sequencer") - .field("clock", &self.clock) - .field("play_clip", &self.play_clip) - .field("next_clip", &self.next_clip) - .finish() - } -} -impl MidiMonitor for Sequencer { - fn monitoring (&self) -> bool { self.monitoring } - fn monitoring_mut (&mut self) -> &mut bool { &mut self.monitoring } - fn notes_in (&self) -> &Arc> { &self.notes_in } -} -impl MidiRecord for Sequencer { - fn recording (&self) -> bool { self.recording } - fn recording_mut (&mut self) -> &mut bool { &mut self.recording } - fn overdub (&self) -> bool { self.overdub } - fn overdub_mut (&mut self) -> &mut bool { &mut self.overdub } -} -#[cfg(feature="clip")] impl HasPlayClip for Sequencer { - fn reset (&self) -> bool { self.reset } - fn reset_mut (&mut self) -> &mut bool { &mut self.reset } - fn play_clip (&self) -> &Option<(Moment, Option>>)> { - &self.play_clip - } - fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)> { - &mut self.play_clip - } - fn next_clip (&self) -> &Option<(Moment, Option>>)> { - &self.next_clip - } - fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option>>)> { - &mut self.next_clip - } -} -/// JACK process callback for a sequencer's clip sequencer/recorder. -impl Audio for Sequencer { - fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { - if self.clock().is_rolling() { - self.process_rolling(scope) - } else { - self.process_stopped(scope) - } - } -} - -impl Iterator for Ticker { - type Item = (usize, usize); - fn next (&mut self) -> Option { - loop { - if self.sample > self.end { return None } - let spp = self.spp; - let sample = self.sample as f64; - let start = self.start; - let end = self.end; - self.sample += 1; - //println!("{spp} {sample} {start} {end}"); - let jitter = sample.rem_euclid(spp); // ramps - let next_jitter = (sample + 1.0).rem_euclid(spp); - if jitter > next_jitter { // at crossing: - let time = (sample as usize) % (end as usize-start as usize); - let tick = (sample / spp) as usize; - return Some((time, tick)) - } - } - } -} - -pub fn to_key (note: usize) -> &'static str { - match note % 12 { - 11 | 9 | 7 | 5 | 4 | 2 | 0 => "████▌", - 10 | 8 | 6 | 3 | 1 => " ", - _ => unreachable!(), - } -} - -pub(crate) fn note_y_iter (note_lo: usize, note_hi: usize, y0: u16) - -> impl Iterator -{ - (note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n)) -} - -/// Returns the next shorter length -pub fn note_duration_prev (pulses: usize) -> usize { - for (length, _) in NOTE_DURATIONS.iter().rev() { if *length < pulses { return *length } } - pulses -} - -/// Returns the next longer length -pub fn note_duration_next (pulses: usize) -> usize { - for (length, _) in NOTE_DURATIONS.iter() { if *length > pulses { return *length } } - pulses -} - -pub fn note_duration_to_name (pulses: usize) -> &'static str { - for (length, name) in NOTE_DURATIONS.iter() { if *length == pulses { return name } } - "" -} - -pub fn note_pitch_to_name (n: usize) -> &'static str { - if n > 127 { - panic!("to_note_name({n}): must be 0-127"); - } - NOTE_NAMES[n] -} diff --git a/src/tek.edn b/src/tek.edn index c21ade5c..e530469e 100644 --- a/src/tek.edn +++ b/src/tek.edn @@ -27,7 +27,7 @@ (@down (axis (inc :y)))) (mode :arranger (name Arranger) (info Launch grid.) - (keys :clock :color :launch :scenes :tracks :global :history :saveload) + (keys :clock :color :launch :scenes :tracks :clip :global :history :saveload) (mode :editor (keys :editor)) (mode :dialog (keys :dialog)) (mode :message (keys :message)) @@ -40,17 +40,18 @@ (mode :scene (keys :scene)) (mode :mix (keys :mix)) (view - (bar/n 2 :status - (bar/s 8 - (bar/e 16 (align/nw (pad/x 1 :transport)) - (bar/w 16 (bar/s 2 (bar/w 4 :tracks/outputs/add :tracks/outputs/count) - (bar/s 2 (bar/w 4 :tracks/devices/add :tracks/devices/count) - (bar/s 2 (bar/w 4 :tracks/names/add :tracks/names/count) - (bar/w 4 :tracks/inputs/add :tracks/inputs/count)))) - (align/x (bar/s 2 :tracks/outputs/cells - (bar/s 2 :tracks/devices/cells - (bar/s 2 :tracks/names/cells - :tracks/inputs/cells)))))) + (bar/n 2 :status + (bar/s 8 + (bar/e 16 (align/nw (pad/x 1 :transport)) + (bar/w 16 (bar/s 2 (bar/w 4 :tracks/outputs/add :tracks/outputs/count) + (bar/s 2 (bar/w 4 :tracks/devices/add :tracks/devices/count) + (bar/s 2 (bar/w 4 :tracks/names/add :tracks/names/count) + (bar/w 4 :tracks/inputs/add :tracks/inputs/count)))) + (align/x + (bar/s 2 :tracks/outputs/cells + (bar/s 2 :tracks/devices/cells + (bar/s 2 :tracks/names/cells + :tracks/inputs/cells)))))) (bar/w 16 (text) (bar/e 16 :scenes/names :scenes/clips)))))) @@ -87,7 +88,8 @@ (@right (tracks next))) (keys :clip (see :color :launch :axis/z :axis/z2 :delete) - (@l (toggle :loop))) + (@l (toggle :loop)) + (@tab (toggle :edit))) (view :ports/out (bsp/s (align/w (text L-AUDIO-OUT)) diff --git a/src/tek.rs b/src/tek.rs index f062dd15..8974c5cc 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -40,7 +40,12 @@ mod range; pub use self::range::*; mod sampler; pub use self::sampler::*; mod scene; pub use self::scene::*; mod select; pub use self::select::*; -mod sequence; pub use self::sequence::*; +mod midi_clip; pub use self::midi_clip::*; +mod midi_clips; pub use self::midi_clips::*; +mod midi_mon; pub use self::midi_mon::*; +mod midi_play; pub use self::midi_play::*; +mod midi_rec; pub use self::midi_rec::*; +mod midi_seq; pub use self::midi_seq::*; mod ticker; pub use self::ticker::*; mod timebase; pub use self::timebase::*; mod track; pub use self::track::*; diff --git a/src/ticker.rs b/src/ticker.rs index 29f49a27..dc4347d5 100644 --- a/src/ticker.rs +++ b/src/ticker.rs @@ -11,3 +11,25 @@ use crate::*; pub start: usize, pub end: usize, } + +impl Iterator for Ticker { + type Item = (usize, usize); + fn next (&mut self) -> Option { + loop { + if self.sample > self.end { return None } + let spp = self.spp; + let sample = self.sample as f64; + let start = self.start; + let end = self.end; + self.sample += 1; + //println!("{spp} {sample} {start} {end}"); + let jitter = sample.rem_euclid(spp); // ramps + let next_jitter = (sample + 1.0).rem_euclid(spp); + if jitter > next_jitter { // at crossing: + let time = (sample as usize) % (end as usize-start as usize); + let tick = (sample / spp) as usize; + return Some((time, tick)) + } + } + } +} diff --git a/src/track.rs b/src/track.rs index 3dc84d74..16bb0f4a 100644 --- a/src/track.rs +++ b/src/track.rs @@ -158,6 +158,7 @@ def_sizes_iter!(TracksSizes => Track); impl_has!(Vec: |self: Project| self.tracks); impl_as_ref!(Vec: |self: App| self.project.as_ref()); impl_as_mut!(Vec: |self: App| self.project.as_mut()); +impl_has!(Sequencer: |self: Track| self.sequencer); impl> + AsMut> + HasClock + HasTrackScroll> HasTracks for T {}